How to understand the following function excepted from OpenStreetMapX package?

Hi, I’m reading the code from this great OpenStreetMapX package, and the following function confused me a little bit. I would really appreciate it if anyone can help me clarify some doubts.

function get_edges(nodes::Dict{Int,T}, roadways::Vector{OpenStreetMapX.Way}) where T<: Union{OpenStreetMapX.ENU, OpenStreetMapX.ECEF}
         oneway_roads = map(OpenStreetMapX.oneway, roadways)
         reverse_roads = map(OpenStreetMapX.reverseway, roadways)
         classes = OpenStreetMapX.classify_roadways(roadways)
         edges = Dict{Tuple{Int, Int}, Int}()
         
         for i = 1:length(roadways)
              for j = 2: length(roadways[I].nodes)
                   n0 = roadways[i].nodes[j-1]
                   n1 = roadways[i].nodes[j]
                   start = n0 * !reverse_roads[i] + n1 * reverse_roads[i]
                   fin = n0 * reverse_roads[i] + n1 * !reverse_roads[i] 
                   edges[(start, fin)] = classes[roadways[i].id]
                   oneway_roads[i] || (edges[(fin,start)] = classes[roadways[i].id])
              end
         end
       return collect(keys(edges)), collect(values(edges))
end

what confused me are the two lines to assign values to the variables start and fin. Any comments are greatly appreciated.

Hi,

To answer this question let me clarify what is the way in the context of the Open Street Map and how it is stored and used in OpenStreetMapX. Ways are the basic elements of the OSM files; they are polylines used to describe objects such as roads, buildings, etc. They are stored as an ordered list of nodes, each with unique ID and additional tags describing its function and attributes. OpenStreetMapX maintains this structure, storing ways as objects of type Way.

Let’s move to the function itself. In OpenStreetMapX road network is represented as a directed graph and the function get_edges is desing to create the list of tuples, which is essentially a list of the edges in this graph.

In order to do it properly function get_edges must filter the essential atributes of the roadways from their tags. reverse is one of them. It is boolean variable and it describes a special case when one-way segment of the road is stored in the Way object in the wrong order - from successor to predecessor. The line:

reverse_roads = map(OpenStreetMapX.reverseway, roadways)

extract this information from all roadways and returns a vector of booleans.

Two lines you are asking about are used to assign the values of the start and final node of the segment, taking into account if it is in reverse or non-reverse order.

For example assume that segment i is non-reversed; then value of !reverse_roads[i] and reverse_roads[i]will be equal to 1 and 0, respectively. So when we assign the start value it will be:

n0 * 1 + n1 * 0 = n0

so we got the proper result; our choosen start node match the order in which it is stored in Way object. The variable fin works in the same way, but it is created to get the final node of the segment; in this case it will be equal to n1.

Obviously, when the segment is in the reversed order, the boolean values will turn around so the start will be equal to n1 and fin will be equal to n0, which is again, the solution we are looking for.

I hope my answer may help you.

4 Likes

Thank you a lot for your detailed explanations. it’s actually a simple trick math people like to use, right? :wink:

@Bartosz_Pankratz, Hi, I have one more question / doubt if you do not mind, I was wondering, why is there not a mutable struct with name Node in the package OpenStreetMapX? Is it because it is not necessary, since a node is basically just a coordinate, and there are three different coordinate systems with names LLA, ENU and ECEF already? But a node in .osm file also has the tag attribute, right? Take the following except from an xml file as an example.

<osm version="0.6">
<node lat="50.9", lon="-1.4", id="12345">
<tag k="place" v="city">
<tag k="name" v="Southampton"> 
</node>
<way id="223">
<nd id="1234">
<nd id="1235">
<tag k="highway" v="motorway" />
<tag k="ref" v="M27" />
</way>

Thank you so much!

Hi,

Yeah, it’s just a kind of fancy trick :wink:

You already answer the question - it is kinda unnecessary and basically adding additional struct Node will only worsen the working experience with the library. We already have the types corresponding with different coordinates systems and it is much more convenient to keep nodes as a dictionaries or pairs, which are mapping: node_id => coordinates.

It is especially a better solution when you want to get the coordinates by the node ID, you only need to use ID as a key of the dictionary rather than fiddle with filtering the collection by the struct’s attribute ID.

However, there are also tags relevant to the particular nodes, which often are important for some applications (like finding some special object like stores, amenities, etc.). We are storing them as an attribute of the main map object OSMData called features. They are another dictionary with exactly the same nodes IDs as a keys and the most relevant tags (those corresponding to the categiories of objects) as a values; there are also plenty of function which might be used to filter those features and so on.

In our opinion it was the best solution for this problem and doing about 5 projects with this library have confirmed it. But if you have any comments and ideas which might improve the work of the library, we will be glad to hear them!

1 Like

@Bartosz_Pankratz, Hi, I’m reading the file classification.jl, and it seems like in the process of filtering out different osm ways (except features, all the others are ways in essence), only the functions filter_highways and filter_buildings use .visible to check whether it is visible or not. I was wondering, is there a specific reason? Thank you a lot.

@Bartosz_Pankratz, I think it’s because of the logical design of the code. Since all the ways in osm data are highways, it makes sense to only require the highways and buildings are visible. Is it so?

Hi,

Sorry for not responding earlier. The reason why we use this tag to filter ways is caused by the fact that osm files contain data about non-open acessible objects, such as military grounds or airports runways. And basically .visible is used to filter this kind of ways. It is useful when you want to build a a traffic model - obviously drivers cannot use the runways in the same ways as regular roads, so you want to throw them away from the model.

1 Like

Thank you so much for your time and attention! I have another question if you do not mind. :wink:

function crop!(nodes::Dict, bounds::OpenStreetMapX.Bounds, ways::Vector{OpenStreetMapX.Way},relations::Vector{OpenStreetMapX.Relation}, 
	relation::OpenStreetMapX.Relation)
	
	valid = falses(length(relation.members))
	for i = 1:length(relation.members)
		ref = parse(Int,relation.members[i]["ref"])
		if relation.members[i]["type"] == "node" && haskey(nodes,ref)
			valid[i] = OpenStreetMapX.inbounds(nodes[ref],bounds)
		elseif relation.members[i]["type"] == "way"
			way_index = findfirst(way -> way.id == ref, ways)
			!isa(way_index,Nothing) && (valid[i] = !OpenStreetMapX.crop!(nodes, bounds, ways[way_index])) 
		else
			relation_index = findfirst(relation -> relation.id == ref, relations)
			!isa(relation_index,Nothing) && (valid[i] = !OpenStreetMapX.crop!(nodes,bounds, ways, relations, relations[relation_index])) 
		end
	end
	relation.members = relation.members[valid]
	if sum(valid) == 0
		return true
	else
		return false
	end
end

I thought I got the logic of cropping a way yesterday. Then, the above function confuses me a bit.
More specifically, is there a self-referencing call in the following line:
!isa(relation_index,Nothing) && (valid[i] = !OpenStreetMapX.crop!(nodes,bounds, ways, relations, relations[relation_index]))
If so, how to make sure that it will eventually end then?

Hi,

Sorry for not replying earlier. The answer is simple, relations in osm are basically containers storing some other objects which are relating one to another. relation might also be a member of another relation and thats the reason why we self-referencing this function. We know that this loop won’t be infinte because of the internal logic of the relation. If let’s say relation_1 is a member of relation_2 then the relation_2 might be a member of relation_1 only if they are indentical. And it is impossible because the OSMData object keeps only unique elements.

1 Like

Thank you for your detailed reply. One more question if you do not mind. :wink:

function find_route(m::OpenStreetMapX.MapData, node0::Int, node1::Int, node2::Int, 
                    weights::SparseArrays.SparseMatrixCSC{Float64,Int64};
                    routing::Symbol = :astar, heuristic::Function = (u,v) -> zero(Float64), 
                    get_distance::Bool = false, get_time::Bool = false)
    result = Any[]
    route1 = OpenStreetMapX.find_route(m, node0, node1, weights,
                                        routing = routing, heuristic = heuristic,
                                        get_distance = get_distance, get_time = get_time)
    route2 = OpenStreetMapX.find_route(m, node1, node2, weights,
                                        routing = routing, heuristic = heuristic,
                                        get_distance = get_distance, get_time = get_time)
    push!(result,vcat(route1[1],route2[1]))
    for i = 2:length(route1)
        push!(result,route1[i] + route2[i])
    end
    return result
end

I understand most of the above code. The only thing, is the two push functions. I’m a bit confused about it…

Hi,

Answer is simple - at the end this functions returns a two element vector, that’s why there are two pushes. First one push the concatenate list nodes of both routes , second one is pushing the sum of the routes costs (eg. time, distance).