Many readers know that I’m involved in our open source OwnTracks offerings – apps which permit you to keep track of your own location, and you might also know that Christoph and I created OwnTracks.de to provide affordable devices and services for tracking vehicles. It’s a very modest business, too much so considering the time and effort we invest, but we enjoy it and are quite involved. Our customers particularly appreciate owning the data their vehicles produce and having a partner willing to go the extra mile to provide specialized service.

A few years ago we were asked by a client whether it would be possible that they are informed when a tracked vehicle stops at a fueling station. They wanted to be able to cross check the fueling of their tour buses with chits delivered by drivers at the end of the tour.

In theory this is pretty easy: we know where a vehicle is located so all we have to do is to check whether there’s a fueling station at that particular location. In essence what we’ll do is pre-compute where the stations are and then, for each position reported by a vehicle, verify it maps to one of the station locations.

where are the fueling stations?

The first order of the day was to obtain data on geographic coordinates of fueling stations. For Germany this is quite easy, for example the 14000+ stations by Tankerkoenig, but what’s with the rest of Europe? We wanted to be sure to provide adequate information for the continent, as most of our clientele travels internationally. I was pointed at the data provided by OpenStreetMap, and long may they live! (And yes, we donate a modest amount to them yearly.)

The first task was to understand the data. GeoFabrik offers smaller extracts, and we started with the manageable 36MB file of Hamburg, but quickly moved to using the full Europe dump.

The OpenStreetMap PBF (Protocolbuffer Binary Format) files are about half the size of the XML files which is reason enough to use them. They contain data composed of nodes, ways, and relations. A single place (e.g. amenity=hospital or amenity=fuel) can be specified as a <node/> or a <way/> or a <relation/>. It took me a bit to understand how this all works, and I decided that in order to find all petrol stations, I would extract all nodes and ways with amenity=fuel (recently adding charging_station to the selection; there are many more features) and somehow store these in a CDB. We thought “constant” database because new fueling stations would likely not appear frequently and considered rebuilding this dataset maybe every six months.

The following examples use a charging station on the Autobahn A7 at Malsfeld, Germany. You can partially follow along by right-clicking on the map and selecting Query features. The node describing this charging station is marked with an orange circle.

OSM node

We obtain these specific nodes by running osmium (the fastest utility I’ve found) to create an OSM XML output. (As reference, osmium on europe-latest.osm.pbf runs 12 minutes whereas osmosis runs 190 minutes.)

$ osmium tags-filter europe-latest.osm.pbf nw/amenity=fuel,charging_station -o europe.merged.osm

The result is an OSM XML file with the nodes we selected. Here’s one, and what we most need are the lat and lon elements of the <node/>:

<node id="3263128134" version="4" timestamp="2016-10-28T19:32:21Z" lat="51.0872282" lon="9.4851045">
  <tag k="car" v="yes"/>
  <tag k="name" v="Tesla Supercharger Malsfeld"/>
  <tag k="access" v="customers"/>
  <tag k="amenity" v="charging_station"/>
  <tag k="bicycle" v="no"/>
  <tag k="capacity" v="8"/>
  <tag k="operator" v="Tesla Motors Inc."/>
  <tag k="opening_hours" v="24/7"/>
  <tag k="socket:tesla_supercharger" v="6"/>
</node>

We then use a custom utility which reads the XML and, for each <node/> therein, creates a JSON object with tags copied from the OSM node data, the calculated geohash, and an address field obtained from data graciously given to us by our friends at OpenCageData.

{
  "nodeid": "3263128134",
  "lat": 51.0872282,
  "lon": 9.4851045,
  "ghash": "u1ndxu4u",
  "addr": "Autohof Malsfeld, Dr.-Reimer-Straße 2, 34323 Elfershausen, Germany",
  "tags": {
    "car": "yes",
    "name": "Tesla Supercharger Malsfeld",
    "access": "customers",
    "amenity": "charging_station",
    "bicycle": "no",
    "capacity": "8",
    "operator": "Tesla Motors Inc.",
    "opening_hours": "24/7",
    "socket:tesla_supercharger": "6"
  }
}

So what we now have is a CDB indexed by an 8-character geohash with the above JSON as the data. The 340,036 records in the database require 108MB of space.

regarding geohashes

What’s with the geohash? A geohash is a method of expressing a location anywhere in the world using a short alphanumeric string, with greater precision using longer strings. As can be seen on the very practical geohashes page, geohashes identify rectangular cells; the 8 characters we use here define an area of roughly 38m x 19m.

So how to we determine that a vehicle is at a fueling station?

On each ignition event received from Traccar we convert the latitude, longitude coordinates of a vehicle’s position to an 8 character geohash and check in the CDB whether the key exists. If it does we’re at a station; if the key doesn’t exist, we’re not.

That sounds easy, and it turns out it was too simplistic. Recall our geohash maps to a small rectangle of roughly 38mx19m, a very small area for a fueling station, and most are much larger than that, so what we do is check all the neighbours of the geohash which we receive from a vehicle position. In doing so we hope to cover an area which encompasses the actual fueling area, ignoring parking, etc. This isn’t fool proof, but it’s close enough for the requirements.

visualizing geohashes

From the above image we see that a vehicle is located at coordinates 51.0873396,9.48528275 which translates to geohash u1ndxu4v. We also see that the area delimited by the geohash (the blueish rectangle) is not the fueling (here: charging) station but that it’s north of it. So in checking all the neighbors of the geohash we stab around and see if the fueling/charging station is at one of those positions, and if so we have a hit. (Alternatively we could have used a shorter geohash string (which maps to a larger rectangle), but we found that was too coarse: we had too many false positives.)

This small custom utility shows what’s happening: we invoke it with our coordinates, and it prints out the corresponding geohash and each of its neighbors. It obtains a hit on the S (southern) neighbor (u1ndxu4u) containing the charging station.

$ geoh 51.0873396,9.48528275
51.087370,9.485149
u1ndxu4v **
u1ndxu4y N
u1ndxu5n NE
u1ndxu5j E
u1ndxu5h SE
u1ndxu4u S  {"nodeid": "3263128134", "lat": 51.0872282, "lon": 9.4851045, "ghash": "u1ndxu4u", "addr": "Autohof Malsfeld, Dr.-Reimer-Stra\u00dfe 2, 34323 Elfershausen, Germany", "tags": {"car": "yes", "name": "Tesla Supercharger Malsfeld", "access": "customers", "amenity": "charging_station", "bicycle": "no", "capacity": "8", "operator": "Tesla Motors Inc.", "opening_hours": "24/7", "socket:tesla_supercharger": "6"}}
u1ndxu4s SW
u1ndxu4t W
u1ndxu4w NW

If a query in the database for the geohash or any of its neighbors finds a record, we consider the vehicle is at a fueling / charging station and report a “hit”.

As demonstrated, we’re actually a bit off mark, but we can a) live with the inaccuracy at the moment, and b) in order to be as precise as possible we use these small geohashes (i.e. long strings).

Here’s an example where that counts: we detected the vehicle at the blue pin, i.e. the actual center of the petrol station is west of the vehicle.

West of direct match

and in the following example we matched the center of the station with the first geohash the software checked.

direct match on station

This final example shows nicely how important it is to “reach around” to see if we’re still at the station; the geohash ending in y8 covers only a fraction of the huge station of the Aire de Wasserbillig in Luxemburg, but the enclosing way would be too large an area.

Wasserbillig

publishing data

Once a hit is obtained, we publish an enriched payload over MQTT, and this is consumed by subscribers which record the fact in a database, or alert in some way. (Who knows, maybe one day we’ll wire up an MQTT version of Jeff Geerling’s Bell Slapper to hear a ding whenever a vehicle stops at a fueling station.)

{
  "_type": "fuelstop",
  "_position_id": 5016671,
  "_uuid": "2498c02e-f885-4921-bcbb-c5c3dffeee21",
  "_neighbor": "S",
  "tst": 1624542898,
  "ignition": false,
  "addr": "Autohof Malsfeld, Dr.-Reimer-Str 2, 34323 Elfershausen, Germany",
  "lat": 51.0873396,
  "lon": 9.48528275,
  "osm_url": "https://www.openstreetmap.org/?mlat=51.087340&mlon=9.485283&zoom=18",
  "loc": "51.08733960,9.48528275",
  "ghash": "u1ndxu4v",
  "name": "AA-BB-CC",
  "u": "owntracks/qtripp/123456789244442",
  "station": "Tesla Supercharger Malsfeld",
  "amenity": "charging_station",
  "nodeid": "3263128134"
}

OwnTracks.de customers appreciate that we are small enough to actually enjoy thinking about and implementing this kind of feature, and more importantly we realize that this wouldn’t be possible without the amazing work done by OpenStreetMap and the thousands of contributors to its incredibly rich data set.

owntracks, openstreetmap, and data :: 24 Jun 2021 :: e-mail