Overpass API > Blog >
Published: 2017-03-20, updated 2019-10-31
Levels are already a well-established paradigm in OpenStreetMap. This can be seen in maps like OpenLevelUp and OpenStationMap.
Nonetheless, our tools are geared towards a 2D-representation of the world. For a reason: for by far most of the world only the surface is interesting.
This has a downside where in reality locations are packed on multiple stories: We have piles of data there that are difficult to edit or even view.
It is possible to get level-by-level through the data. In a first step, we can look which levels exist:
( node({{bbox}})[level]; way({{bbox}})[level]; rel({{bbox}})[level]; ); make levels_here level_list=set("{" + t["level"] + "}"); out;
This works similar to the power line example for a list of all values.
We can then easily fetch the data level-by-level:
( node({{bbox}})[level={{value}}]; way({{bbox}})[level={{value}}]; rel({{bbox}})[level={{value}}]; ); (._; >;); out;
Please replace the {{value}} with the actual level. Let us briefly walk through the query: The first three lines employ two rather well-known features:
But what if we have an element and would like to get only those elements on the same level? In our example: you would like to know everything about the "RER D" part of the station.
To prepare the answer, I would like to use a slightly fancier variant of the last query:
make level_param val={{value}} ->.param; ( node({{bbox}})[level](if:t["level"]==param.set(t["val"])); way({{bbox}})[level](if:t["level"]==param.set(t["val"])); rel({{bbox}})[level](if:t["level"]==param.set(t["val"])); ); (._; >;); out;
The value now needs to be set only once. We have a short look at the details. The first line is a make statement. The only uncommon thing is that it ends in ->.param. This stores the result instead of the standard place to a new place param.
In the second to fourth line this result is taken as input to the aggregator set, hence written as param.set(...). This is the aggregator set as known before but reading from the previous result param.
Now you can use a tag value from a previous result as search criteria:
node({{bbox}})[name="Gare de Lyon RER D"]->.param; ( node({{bbox}})[level](if:t["level"]==param.set(t["level"])); way({{bbox}})[level](if:t["level"]==param.set(t["level"])); rel({{bbox}})[level](if:t["level"]==param.set(t["level"])); ); (._; >;); out;
This finds not only the node representing part "RER D" but also all the elements on the same level.
There is more than one tag to express vertical position in OpenStreetMap. The oldest one is layer. We are curious if there are elements where the tags level and layer differ:
( node({{bbox}})[level][layer](if:t["level"]!=t["layer"])); way({{bbox}})[level][layer](if:t["level"]!=t["layer"]); rel({{bbox}})[level][layer](if:t["level"]!=t["layer"]); ); (._; >;); out;
This query doesn't contain anything new but reuses features we already know. The only notable feature is the non-equality operator. Nonetheless this generalizes to a standard approach to compare object values within the same element.
That an element has different values for the tags level and layer is not necessary the error to fix. According to the documentation of the layer tag, a level tag belongs to indoor elements while a layer tags belongs to outdoor elements. Hence, it is rather suspect if an element has both tags. For convenience, a query to find elements with both tags:
( node({{bbox}})[level][layer]; way({{bbox}})[level][layer]; rel({{bbox}})[level][layer]; ); (._; >;); out;
Are in a complex scenario like this neighbourhood in Paris the layers properly set? We cannot check whether elements are tagged in the wrong vertical order, not without local knowledge. But we can check whether elements with the same layer value intersect - this is always an error.
The complete approach is admittedly slow:
way({{bbox}})[layer]->.all; foreach.all( node(w)->.children; ( way.all(around:0)(if:t["layer"]==u(t["layer"])); - way.all(bn.children); ); (._; .result;)->.result; ); .result out geom;
Also, the result is too dense to make sense of it. We will find a workaround in the next step. Nonetheless, I would like to explain how it works:
The first line is a standard query for ways in the given {{bbox}} with an value for [layer]. Notable is the syntax to keep the result in a named bucket all. It is ->.all.
In the second line begins a foreach block. It loops over the elements from the just-filled bucket all. The loop variable is the standard bucket _. Thus we have inside the loop both the loop element (in _) and the set of elements as a whole (in all) adressable.
We skip the third line for a moment. The fourth to seventh line is a difference block. In total, we want to keep only those ways that are close to the loop element but do not share a node with it. For this purpose we first collect in line five all ways that are close to the loop element. Then in line six, we cut out all elements that share a common node with the loop element. We need for line six the set of of nodes that are members of the loop element. We cannot easily collect them right here because that would interfere with the difference statement. Thus we collect them before the difference statement in line three and store them to another named bucket, children.
Line five is a query with multiple conditions:
Finally, the line (._; .result;)->.result; is a sequence of three statements that together collect all the results in a common result bucket named result. To print the result (instead of printing a single random loop element) we prepend out geom by the name of the bucket to print from, i.e. .result.
We can get the whole thing both faster and less packed by inspecting layer-by-layer:
make param val=-2 ->.param; way({{bbox}})(if:t["layer"]==param.u(t["val"]))->.all; foreach.all( node(w)->.children; ( way.all(around:0)(if:t["layer"]==param.u(t["val"])); - way.all(bn.children); ); (._; .result;)->.result; ); .result out geom;
This query yields all results for layer equals -2. You can adjust the value easily in line one by exchanging -2 to something different. According to the wiki, integral values from -5 to 5 shall be used. You can check will values really exist if you adapt a query like the value listing for level.
Let us return to the level tag. What about elements which have multiple levels?
Unfortunately, we do not have any features that break up strings so far. Thus the best I can offer is the regular expression query to at least find where they are:
( node({{bbox}})[level~";"]; way({{bbox}})[level~";"]; rel({{bbox}})[level~";"]; ); out geom;
A further analysis of the situation shows that only level-changing features like steps and elevators have more than one level here. Excellent tagging!
This is one of the three kinds of almost-number values I have addressed last week. The other two are numeric values with units and those with special syntax. I still would very much appreciate use cases for those kinds of values. This is necessary to figure out which features to develop next.