How to read values from multiple relations in one way

Hello!

I’m trying to encode trail colors into Graphhopper. The colors are typically part of relations, and it worked perfectly when there was only one relation. However, I have encountered a problem with ways that are part of multiple relations, each with its own color. For example, this way is part of two relations, both of which have a color:

OSM Way 28126961

In my code, I can see that Graphhopper is reading both relations and their respective colors:

override fun handleRelationTags(relFlags: IntsRef, relation: ReaderRelation) {
    val relIntAccess = IntsRefEdgeIntAccess(relFlags)

    val colour = relation.getTag("colour")
    val osmcSymbol = relation.getTag("osmcSymbol")
    val colors = when {
        colour != null -> RouteColors.fromColourTag(colour)
        osmcSymbol != null -> RouteColors.fromOsmcSymbolTag(osmcSymbol)
        else -> null
    }

    if (relation.id == 3360362L) {
        println("Colors $colors")
    }

    if (relation.id == 3352332L) {
        println("Colors $colors")
    }

    if (colors != null) {
        transformerRouteTypeEnc.setInt(false, -1, relIntAccess, colors.encode())
    }
}

In this snippet, the two if statements are there to confirm that both relations are being read correctly and that I get colors from them.

However, when I read the color value in the way, I only see one of them (the second value that was printed):

override fun handleWayTags(edgeId: Int, edgeIntAccess: EdgeIntAccess, way: ReaderWay, relationFlags: IntsRef) {
    val relIntAccess = IntsRefEdgeIntAccess(relationFlags)
    val colors = RouteColors.decode(transformerRouteTypeEnc.getInt(false, -1, relIntAccess))
    if (way.id == 28126961L) {
        println("For way ${way.id} colors $colors") // Only RED color here, from the second relation
    }
}

I was under the impression that handleWayTags would be called once for each relation associated with the way. However, it is only being called once. I was expecting I could merge the results from both relations, it is not possible if handleWayTags is called once.

How can I achieve the desired result and merge the colors from both relations?

I have not looked a longer time into the relation code, but in general it should work even if a way is part of multiple relations. What does not work is the storage. I.e. you can only store a single (color) value per edge using the approach with encoded values.

You could create multiple boolean encoded values like blue_color, green_color, … but that probably does not scale properly.

Or you could store a color list in the KVStorage (edge.setKeyValues()), but as this storage is populated only once, at a different place in OSMReader, this is also a bit tricky.

I have store feature figure out, I map colors to close set of colors (enum) and then I encode list of enum values into integer using byteshift with something like this:

data class RouteColors(val colors: List<OSMColors>) {
    fun encode(): Int {
        var encodedValue = 0
        colors.forEach { color ->
            encodedValue = (encodedValue shl 6) or (color.ordinal + 1) // Adjust ordinal by +1
        }
        return encodedValue
    }

This way I can store up to 5 colors on edge. Can you elaborate about “it should work even if a way is part of multiple relations”? I can’t figure out how transformation encoded values works between relations and way. If I read relation with id 3360362 and save colors on encoded value and then I read relation with id 3352332 and also save colors then how in “handleWayTags” I can read both of them? I will provide whole code for my parser because it is really simple:

class RouteColorRelationParser(private val wayColorEncodedValue: IntEncodedValue, relConfig: EncodedValue.InitializerConfig) : RelationTagParser {
    val transformerRouteTypeEnc = IntEncodedValueImpl("transformer_route_type", 30, false)

    init {
        transformerRouteTypeEnc.init(relConfig)
    }

    companion object {
        val KEY = "route_colors"
    }

    override fun handleRelationTags(relFlags: IntsRef, relation: ReaderRelation) {
        val relIntAccess = IntsRefEdgeIntAccess(relFlags)

        val colour = relation.getTag("colour")
        val osmcSymbol = relation.getTag("osmcSymbol")
        val colors = when {
            colour != null -> RouteColors.fromColourTag(colour)
            osmcSymbol != null -> RouteColors.fromOsmcSymbolTag(osmcSymbol)
            else -> null
        }

        if (colors != null) {
            transformerRouteTypeEnc.setInt(false, -1, relIntAccess, colors.encode())
        }
    }

    override fun handleWayTags(edgeId: Int, edgeIntAccess: EdgeIntAccess, way: ReaderWay, relationFlags: IntsRef) {
        val relIntAccess = IntsRefEdgeIntAccess(relationFlags)
        val colors = RouteColors.decode(transformerRouteTypeEnc.getInt(false, -1, relIntAccess))
        wayColorEncodedValue.setInt(false, edgeId, edgeIntAccess, colors.encode())
    }
}

GraphHopper reads the OSM file twice and the OSM file contains nodes, ways and relations in this order. The first time it reads the OSM file OSMReader#preprocessRelations(ReaderRelation relation) is called for every relation. The second time OSMReader#processRelation(ReaderRelation relation, LongToIntFunction getIdForOSMNodeId) is called for every relation, but this is after the edges are created (after handleWayTags is called for the different parsers). To assign the relation colors to their corresponding ways you could for example maintain a HashSet of way IDs in preprocessRelations. You could then use it in handleWayTags to find all colors for the way ID of the current edge.
If you don’t want to make these changes to OSMReader#preprocessRelations directly you can probably also rely on the RelationTagParser mechanism. The handleRelationTags(IntsRef relFlags, ReaderRelation relation) method allows you to store arbitrary information in relFlags that will be associated with a way ID and is made available as relationFlags in handleWayTags the second time the OSM file is read. You need a data layout that lets you set colors independently, because the method will be called only for one relation (one color) at a time. For example you could create one BooleanEncodedValue for relFlags for each color. I never tried this myself, please let us know if you can make it work.

Yes it worked :wink: I can’t say that I understand why my first attempt would not work, but using boolean per color it is working, thanks!

Just to make sure, I had to store colors independently because first Graphhopper called handleRelationTags with relationFlags which were “gateway” to way and then it called handleRelationTags again with the same relationFlags for other relation? So in this “relationFlags” object there is encoded wayId and given that one way had two relations second relation did override data from first?

1 Like

I’m not sure I understand the question :smiley: There will be one relation flags object for each way and it will be the same if a way appears in multiple relations. So the first time it occurs in a relation it is basically empty and you get to modify it in handleRelationTags. The next time it occurs it will already be modified, so you need to make sure to not simply overwrite the value you wrote the first time (for example by using separate booleans per color). Finally it will be presented in handleWayTags.

Okay, great! Now I fully understand. :wink:

My initial approach was actually correct, but when I tried to merge colors from multiple relations, it crashed because some ways have more than five colors—like this one:
Way: ‪Ignacego Daszyńskiego‬ (‪153050730‬) | OpenStreetMap.

I realized that I don’t need multiple BooleanEncodedValues—a single integer is enough:

 val transformerRouteTypeEnc = IntEncodedValueImpl("transformer_route_type", 30, false)

    override fun handleRelationTags(relFlags: IntsRef, relation: ReaderRelation) {
        val relIntAccess = IntsRefEdgeIntAccess(relFlags)

        val colour = relation.getTag("colour")
        val osmcSymbol = relation.getTag("osmcSymbol")
        val colors = when {
            colour != null -> RouteColors.fromColourTag(colour)
            osmcSymbol != null -> RouteColors.fromOsmcSymbolTag(osmcSymbol)
            else -> null
        }

        if (colors != null) {
            val currentValue = transformerRouteTypeEnc.getInt(false, -1, relIntAccess)
            val merged = RouteColors.decode(currentValue).mergeWith(colors)
            transformerRouteTypeEnc.setInt(false, -1, relIntAccess, merged.encode())
        }
    }

All I had to do was retrieve the value from relIntAccess, decode it into a list of colors, add the new ones, and encode it back into an integer.

Thanks for your help! The feature works perfectly, and I now have a much better understanding of relation parsers.

1 Like