Overpass API > Blog >

Counting roundabouts

Published by Antoine Riche from Carto’Cité under CC-by-sa 4.0 license: 2021-03-10

Table of content

Step 1 – Display all roundabouts
Step 2 – A complete roundabout
Step 3 – Identifying roundabouts
Step 4 – Skip out visited ways
Step 5 – Implementing a counter
Step 6 – Putting it all together

In France the first modern roundabout (where the priority rule states that entering traffic gives way to vehicles on the ring) was built in Quimper in 1976. Since then the number of roundabouts has grown rapidly all over french cities. How many are there ? Let's try and count them using my favourite OpenStreetMap tool – Overpass queries. The full query is a bit complex and uses several features that have been added in versions 0.7.54 and 0.7.55. Let's build the code together, step by step.

Step 1 – Display all roundabouts

A roundabout is a highway with the tag junction=roundabout. Let's write a query to find the roundabouts in Quimper:

area
  [admin_level=8]
  [name="Quimper"];
way
  [highway]
  [junction=roundabout]
  (area);
out geom;

This is a standard request where the first statement finds out the city of Quimper as an area, and the second produces all ways tagged as a roundabout in that area. Overpass Turbo shows just what we want on the map.

The number of ways at the bottom right corner says 205. You can actually get that number just by replacing the last statement by out count: the response to the request is then the size of the result set rather than the result set itself, and Turbo shows the count in the Data tab.

Do we really have that many roundabouts in Quimper ? The map seems to show fewer than that, and looking closely shows that several roundabouts are made of several ways. This occurs for various reasons, for instance a roundabout where only a section is covered with paving stones, includes a cycleway lane or is a bridge over another road. Some mappers also split roundabouts when a bus route goes through a section of it – I won't argue for or against this approach, this is not the purpose of this blog post ;)

So how can we count roundabouts regardless of whether they are defined by a single way or by multiple ways ?

Step 2 – A complete roundabout

Our next step is to find all the ways making up one roundabout, starting from any of its ways – for instance this section of roundabout in Quimper. The following request does just that:

way(id:627424118);
complete {
  node(w);
  way(bn)[junction=roundabout];
}
out geom;

Line 3 recurses down the nodes of the way then line 4 recurses up the ways that reference these nodes and are also part of a roundabout – most likely the very same roundabout. Including these two statements in a complete loop (see post Loop and Group) lets this recurse down then up until the set of ways (the result set produced by the last instruction in the loop) no longer changes, that is until the result set contains all roundabout sections.

Step 3 – Identifying roundabouts

Let's see how we can combine the two requests produced so far: the code that finds all sections of all roundabouts in Quimper, and the code that finds all sections of a single roundabout.

In fact we're not interested in the actual ways nor their geometries. What we want to do is to identify complete roundabouts so that we can count them. We can do so by returning the list of way ids that define each roundabout. We use the make statement together with the set() aggregator:

way(id:627424118);
complete {
  node(w);
  way(bn)[junction=roundabout];
}
make roundabout ids = set(id());
out;

Line 6 creates an object with the tag "ids" defined as the set of ids of the OSM objects in the result set. Both the object type "roundabout" and the tag "ids" are named arbitrarily.

Before considering all roundabouts in Quimper let's focus on a small area with a mix of split and unsplit roundabouts, and combine our two requests into one:

way
  [highway]
  [junction=roundabout]
  (47.986,-4.131,47.992,-4.120);
out geom; // Useful for debugging
foreach {
  complete {
    node(w);
    way(bn)[junction=roundabout];
  }
  make roundabout ids = set(id());
  out;
}

Note there are 2 out statements. Line 5 is here for convenience : this outputs the ways with their geometries so that the result is displayed on the map – feel free to comment it out. Line 12 outputs, for each complete roundabout, the list of way ids that define it. These lists are appended to the result set and can be viewed at the bottom of the Data tab.

Let's have a look : we count 22 "roundabouts" even though the map shows just 5. Looking closely we notice that some roundabouts are identical : they use the same set of ids. Looking more closely it appears that each roundabout is returned as many times as the number of ids it contains. This is no surprise, as the foreach loop goes through each way of the set we started from, and builds the complete roundabout for each of them. So a roundabout made of 5 ways is produced 5 times by the complete loop.

Step 4 – Skip out visited ways

We want to make sure we visit each roundabout once and only once. To do so we build up the list of all ids that have been visited so far during the loop execution, and skip ways that have already been visited to avoid producing the same roundabout twice. The request uses some interesting features.

way
  [highway]
  [junction=roundabout]
  (47.986,-4.131,47.992,-4.120);
foreach {
  if (! lrs_in(u(id()), visited_ids.set(t['ids'])))
  {
    complete {
      node(w);
      way(bn)[junction=roundabout];
    }
    make roundabout ids = set(id()) -> .roundabout_ids;
    .roundabout_ids out; // debug
    make visited
      ids = lrs_union(roundabout_ids.set(t['ids']),
                      visited_ids.set(t['ids']))
      -> .visited_ids;
    // .visited_ids out; // debug
  }
}

The first make statement is the same as above, we just store the result into a named set for convenience. The second make statement builds up the list of all visited ids, by creating the union of the ids making up the current roundabout together with the ids visited so far. lrs_union() is one of the List Represented Set operators, that manipulate lists of values (ids in our case).

Another LRS operator is used in line 6: lrs_in() checks whether the first argument is contained in the second argument (the ids of all ways visited so far). The first argument is the id of the current way, which we retrieve using the u() aggregator. This aggregator is designed for sets containing a single element, here the current element of the foreach loop. If that is not the case (statement if() with the test negated by the sign '!'), the code that builds up the roundabout is run. In other words, if we have visited a roundabout we do not build it again.

We're nearly there. Our request produces the list of roundabouts, where each roundabout appears once and only once. We can count them ourselves, we can also let Overpass count them for us...

Step 5 – Implementing a counter

Let's see how we can count things for ourselves. To start with let's get back to our first request and count the number of ways without using the out count statement. We need to loop through the ways, and increment a counter inside that loop. Looping through the result set is easily done using a foreach loop.

Implementing a counter is slightly awkward. To do so we use the make and convert statements:

area[admin_level=8][name="Quimper"];
way[highway][junction=roundabout](area);
make counter num = 0 -> .count;
foreach {
  .count convert counter num = t['num'] + 1 -> .count;
}
.count out;

Line 3 creates an object called counter, with the tag num initialized to 0, and puts that object in the set named count. Overpass handles sets: count is a named set that we know contains a single element. This enables us to use convert in line 5, to derive a new object (also named counter for the sake of clarity) with the tag num incremented. We then overwrite the named set count with that new object.

Note that all names are arbitrary: the object counter, its tag num and the set count. Each could be named differently, however the tag and the set must be named identiquely inside and outside the loop. We can check our counter works by running the request and comparing the result with the number we obtained in step 1.

Step 6 – Putting it all together

We now have all the bits to implement the request. All we need to do is to inject the counter code into the request we obtained in step 4, and consider the city of Quimper again.

area[admin_level=8][name="Quimper"];
way[highway][junction=roundabout](area);
make counter num = 0 -> .count;
foreach {
  if (! lrs_in(u(id()), visited_ids.set(t['ids'])))
  {
    complete {
      node(w);
      way(bn)[junction=roundabout];
    }
    make roundabout ids = set(id()) -> .roundabout_ids;
    // .roundabout_ids out; // debug
    make visited
      ids = lrs_union(roundabout_ids.set(t['ids']), 
                      visited_ids.set(t['ids']))
      -> .visited_ids;
    .count convert counter num = t['num'] + 1 -> .count;
  }
}
.count out;

The only thing to take care of is to increment the counter inside the if block, so that the increment is done once for each roundabout, not for each way.

This request does the job but is rather slow. The performance glitch comes from the instruction way(bn)[junction=roundabout] in the complete loop, which reads data from the disk at each iteration. However we are only interested in the roundabout sections in Quimper, which is precisely what the instruction in line 2 produces. So the trick is to put these ways into a named set, so that they are kept in RAM, then restrict the instruction way(bn) to that set of ways. We no longer need the tag filter so line 9 turns into way.all(bn), and we adjust line 4 to fetch data from the named set. Here is our final request:

area[admin_level=8][name="Quimper"];
way[highway][junction=roundabout](area) -> .all;
make counter num = 0 -> .count;
foreach.all {
  if (! lrs_in(u(id()), visited_ids.set(t['ids'])))
  {
    complete {
      node(w);
      way.all(bn);
    }
    make visited
      ids = lrs_union(set(id()), visited_ids.set(t['ids']))
      -> .visited_ids;
    .count convert counter num = t['num'] + 1 -> .count;
  }
}
.count out;

Note we also simplified the code a little bit by dropping the use of a named set for the current roundabout – using a named set is not necessary here.

A big Thank You to Roland for suggesting this highly valuable optimization, as well as making the counter implementation simpler. Also of course for letting me publish this post on his blog :-)

Exercise for the reader: modify the request to count the number of roundabouts made of a single way and those made of multiple ways – preferably in a single request !