maps: toll-free option

Hoping this is in the right sub-forum, if not, pls correct!

I am a happy user of the interactive graphhopper/maps, planning my routes across Europe and saving them as gpx for further processing.

It was nice to see the option “avoid toll roads” added (though it might well be extended to trucks and vans) but it doesn’t always work as expected. For a clear example, I requested a route from Burgos to Valladolid, and it sent me away from the motorway A62, although that is completely toll-free, and it is used if the option “avoid toll” is not selected.

Is this a bug in the software or is the OSM data at fault? I checked a few segments of the A62, but none said “toll=yes”. How to proceed?

(I would upload a screenshot but find no way to do so)

currently we have indeed the rule that sets MOTORWAY to Toll.ALL for spain (ESP):

case BGR, CZE, FRA, GRC, HRV, ITA, PRT, SRB, ESP -> {
                RoadClass roadClass = RoadClass.find(readerWay.getTag("highway", ""));
                if (roadClass == RoadClass.MOTORWAY)
                    return Toll.ALL;

So I guess this is incorrect :slight_smile: ?

So wdyt, for spain, is it better to default to Toll.NO for every road type (highway tag) or do most motorways have toll?

I cannot speak about “most motorways”, I can only state that some are toll-bound, others are not. For example, about the only route I know is the A62/E80 from the Portuguese border near Vilar Formosa to the French border near Hendaye. All of this is toll-free, except for some 40 kms north of Gasteiz(Vitoria) and a one-off toll gate very near the French border.

But on the stretch Madrid-Bilbao, certain parts are toll-bound. It is really a matter of case-by-case, certainly not “one country => one rule”.

Hoping this helps,

More generally, I think it is not a good idea to mark certain countries to have all of their motorways toll-bound. Even the much-cursed French motorways have toll-free sections, and so do the Italian ones. The only valid example would be some ex-socialist countries (I think CZ and HU are good examples) where a “vignette” is required for using any motorway.

From your list “BGR, CZE, FRA, GRC, HRV, ITA, PRT, SRB, ESP” I should say you ought to remove FRA (France), ITA (Italy), ESP (Spain), and PRT (Portugal?). In each of these four countries I can formally confirm that not all motorways are toll-bound, and only case-by-case annotation in OSM can be relevant.

For trucks, the matter is wholly different, though.

Note that this is just the default. So as soon as the coverage for the toll=* tag is good enough we’ll be hopefully notified from people like you and remove this default.

certainly not “one country => one rule”

Yes, we know. But the tagging quality for toll in OSM is not yet there to rely only on that. Hence our very rough default mapping.

Anyway, have removed Spain from this list :slight_smile:

i don’t build graph for spain, so not sure about the country itself, but i have france that is very similar with this respect.

but i think the test if a road is tall or no toll, in gh, is always taken from the osm tag
toll | yes,

or if no toll key present than in my graph it defaults to no toll, and that works correctly ?

i think the default toll = yes for any motorway in a country can be successfully applied to the austria, switz., and czechia, and other countries that use vignette, but with france, spain, italy, i think this should be requested from osm?

Yes. I meanwhile checked https://facilmap.org/#7/46.180/-0.764/Mpnk-Toll and found it accurately reporting the handful of places I am acquainted with.

So, thanks to Peter for removing Spain from the list, but I insist that also France and Portugal be removed, and likely Italy, too. On the other hand, Switzerland and Austria could indeed be added, since these indeed require a vignette for using ANY of their motorways.

And yes, if no “toll=” tag is given for a road segment, then “toll=no” should be the default assumption.

If no toll is mapped, then the default is country dependent: https://github.com/otbutz/graphhopper/blob/8e56cc75a68d4e2858dbc062b86e402f3eb5c36d/core/src/main/java/com/graphhopper/routing/util/parsers/OSMTollParser.java#L57

And yes, if no “toll=” tag is given for a road segment, then “toll=no” should be the default assumption.

Unfortunately this leads to worse results in many countries.

Portugal be removed

This is something which I checked via overpass turbo and the country is rather well mapped regarding toll and the default currently does more good than harm. At least at what I have seen.

We probably need a way to check the coverage somehow.

for my app, for map rendering, i use vtm-mapsforge
and as you see on the screenshot, i have some of the roads with green-ish colors and some with red-ish.
the data is taken from the openstreetmap, and if in osm there is toll=yes, than the app shows red-ish color, like AP-66 on the image, if toll=no, or no toll field/key at all, than the map shows green-ish, as A-66 or A-60 on the image.

when it comes to routing, than my case is a bit different.
because the app is fully offline and therefore it uses graphhopper v 1.0 as the latest.

and in my app i have separation of simple toll roads (italy, france, …), and also vignette roads (cz, ch, a), that in osm are simply marked as toll roads.
i often drive through switzerland, and because their vignette is cheap (40 euro / annum), i buy the vignette after 4th december almost every year.
but when i drive, for example, from dunkerque to brig i don’t want to go through the french toll roads because they are very expensive, but i want to go through swiss motorways because i have vignette.

if i chose the simple avoid tolls setup - i would have to go through non-toll roads in france or through belgium, but also through the local roads in switzerland - no good.

so in my app i took the idea from my outdated e-class (ntg4.7) satnav that allows me to choose to avoid tolls but use vignette.

to atchieve this with gh i had to do changes in gh 1.0 source, and then during graph preparation, which covers 14 countries/regions, i simply replace toll=yes with toll=vignette for austr. switz., and czechia.

so in the gh source i did changes to:
OSMTollParser.java, where i have:

    @Override
    public IntsRef handleWayTags(IntsRef edgeFlags, ReaderWay readerWay, boolean ferry, IntsRef relationFlags) {
        if (readerWay.hasTag("toll", "vignette"))
            tollEnc.setEnum(false, edgeFlags, Toll.VIGNETTE);
        if (readerWay.hasTag("toll", "yes"))
            tollEnc.setEnum(false, edgeFlags, Toll.ALL);
        else if (readerWay.hasTag("toll:hgv", "yes"))
            tollEnc.setEnum(false, edgeFlags, Toll.HGV);
        else if (readerWay.hasTag("toll:N2", "yes"))
            tollEnc.setEnum(false, edgeFlags, Toll.HGV);
        else if (readerWay.hasTag("toll:N3", "yes"))
            tollEnc.setEnum(false, edgeFlags, Toll.HGV);
        return edgeFlags;
    }
}

and i created
FastestNoTollsWithVignetteWeighting.java, as

package com.graphhopper.routing.weighting;

import com.graphhopper.routing.ev.EnumEncodedValue;
import com.graphhopper.routing.ev.Toll;
import com.graphhopper.routing.util.FlagEncoder;
import com.graphhopper.util.EdgeIteratorState;
import com.graphhopper.util.PMap;

public class FastestNoTollsWithVignetteWeighting extends PriorityWeighting {
    private final EnumEncodedValue<Toll> tollEnc;
    private static final double UNIVERSAL_TURN_PENALTY_SECONDS = 3.0;

    public FastestNoTollsWithVignetteWeighting(FlagEncoder encoder, PMap map, TurnCostProvider turnCostProvider, EnumEncodedValue<Toll> tollEnc) {
        super(encoder, map, turnCostProvider);
        this.tollEnc = tollEnc;
    }


    @Override
    public double calcEdgeWeight(EdgeIteratorState edgeState, boolean reverse) {
        // Check if the edge is a vignette road AND vignette avoidance is enabled.
        if (edgeState.get(tollEnc) == Toll.ALL) {
            // Avoid vignette roads by returning an infinite weight.
            return Double.POSITIVE_INFINITY;
        }
        // For all other roads (including regular tolls), use the standard weighting.
        return super.calcEdgeWeight(edgeState, reverse);
    }

    @Override
    public double calcTurnWeight(int inEdge, int viaNode, int outEdge) {
        // 1. Ask the "Rulebook" (DefaultTurnCostProvider) for the official cost.
        double basePenalty = super.calcTurnWeight(inEdge, viaNode, outEdge);
        // 2. The Rulebook will report back:
        //    - Infinity: if the Database had a "no turn" rule.
        //    - A high number: for U-turns.
        //    - 0: if the Database had no rule for this turn.
        if (Double.isInfinite(basePenalty)) {
            // If the turn is forbidden, our decision is easy: it's forbidden.
            return basePenalty;
        }
        // 3. This is our custom logic. If the turn was not forbidden,
        //    we add our own universal penalty to it.
        //    - A normal turn (basePenalty=0) becomes 20.0 seconds.
        //    - A U-turn (basePenalty=40) becomes 60.0 seconds.
        return basePenalty + UNIVERSAL_TURN_PENALTY_SECONDS;
    }
}

and
FastestNoTollsWeighting.java - similar
FastestWeighting.java

and then in GraphHopper.java i have:

            if (CustomWeighting.NAME.equalsIgnoreCase(weightingStr)) {
                if (!(profile instanceof CustomProfile))
                    throw new IllegalArgumentException("custom weighting requires a CustomProfile but was profile=" + profile.getName());
                CustomModel queryCustomModel = requestHints.getObject(CustomModel.KEY, null);
                CustomProfile customProfile = (CustomProfile) profile;
                queryCustomModel = queryCustomModel == null ?
                        customProfile.getCustomModel() : CustomModel.merge(customProfile.getCustomModel(), queryCustomModel);
                weighting = new CustomWeighting(encoder, encodingManager, turnCostProvider, queryCustomModel);
            } else if ("shortest".equalsIgnoreCase(weightingStr)) {
                weighting = new ShortestWeighting(encoder, turnCostProvider);
            } else if ("fastest_no_tolls".equalsIgnoreCase(weightingStr)) {
                EnumEncodedValue<Toll> tollEnc = this.encodingManager.getEnumEncodedValue(Toll.KEY, Toll.class);
                weighting = new FastestNoTollsWeighting(encoder, hints, turnCostProvider, tollEnc);
            } else if ("fastest_no_tolls_use_vignette".equalsIgnoreCase(weightingStr)) {
            // block for weighting no tolls but use vignette
                EnumEncodedValue<Toll> tollEnc = this.encodingManager.getEnumEncodedValue(Toll.KEY, Toll.class);
                weighting = new FastestNoTollsWithVignetteWeighting(encoder, hints, turnCostProvider, tollEnc);
            } else if ("fastest".equalsIgnoreCase(weightingStr)) {
                if (encoder.supports(PriorityWeighting.class))
                    weighting = new PriorityWeighting(encoder, hints, turnCostProvider);
                else
                    weighting = new FastestWeighting(encoder, hints, turnCostProvider);
            } else if ("curvature".equalsIgnoreCase(weightingStr)) {
                if (encoder.supports(CurvatureWeighting.class))
                    weighting = new CurvatureWeighting(encoder, hints, turnCostProvider);

            } else if ("short_fastest".equalsIgnoreCase(weightingStr)) {
                weighting = new ShortFastestWeighting(encoder, hints, turnCostProvider);
            }

            if (weighting == null)
                throw new IllegalArgumentException("Weighting '" + weightingStr + "' not supported");

            return weighting;
        }
    }

and in graph preparation i run the script, some part of it loops though my countries as:

    osmium tags-filter -v "osmpbf/${COUNTRYS[$i]}-latest.osm.pbf" \
        nw/route=ferry \
        w/operator \
        nw/highway=motorway,motorway_link,trunk,trunk_link,primary,primary_link,secondary,secondary_link,tertiary,tertiary_link,unclassified,residential,residential_link,living_street,escape,road,construction,give_way,mini_roundabout,motorway_junction,passing_place,parking,service,crossing,emergency_bay,phone,motorroad,mountain_pass,traffic_island,rest_area,services,traffic_signals,turning_circle,turning_loop,priority,junction,minor,unspecified,unclassified_link \
        nw/tunnel w/maxspeed w/motorroad w/turn w/access w/oneway w/traffic_calming,mountain_pass r/type=restriction --overwrite -o "osmpbf-gh/${COUNTRYS[$i]}.osm.pbf"

and then replace toll=yes with toll=vignette, as:

$HOME/AndroidStudioProjects/mapfiles/.venv/bin/python3 021-002-toll-to-vignette.py osmpbf-gh/austria-cc.osm.pbf osmpbf-gh/austria-cc-v.osm.pbf
$HOME/AndroidStudioProjects/mapfiles/.venv/bin/python3 021-002-toll-to-vignette.py osmpbf-gh/czech-republic-cc.osm.pbf osmpbf-gh/czechia-cc-v.osm.pbf
$HOME/AndroidStudioProjects/mapfiles/.venv/bin/python3 021-002-toll-to-vignette.py osmpbf-gh/switzerland-cc.osm.pbf osmpbf-gh/switzerland-cc-v.osm.pbf

and then merge all the country files with osmium and then create graph with the merged file as:

graphhopper:

  datareader.file: my-europe-gh.osm.pbf

  graph.location: aio

#  graph.dataaccess.default_type: DISK_STORE
  graph.dataaccess: mmap_byte_buffer

# car                         : will use all toll roads and vignette roads
# car_no_tolls_use_vignette   : will avoid tolls but will use vignette roads (ch + at)
# car_no_tolls_no_vignette    : will avoid tolls and will avoid vignette roads

  profiles:
    - name: car
      vehicle: car
      turn_costs: true
      weighting: fastest
    - name: car_no_tolls_use_vignette
      vehicle: car
      turn_costs: true
      weighting: fastest_no_tolls_use_vignette
    - name: car_no_tolls_no_vignette
      vehicle: car
      turn_costs: true
      weighting: fastest_no_tolls
    - name: car_shortest
      vehicle: car
      turn_costs: true
      weighting: shortest

  graph.encoded_values: toll,max_speed
  graph.flag_encoders: car|turn_costs=true

  profiles_ch:
    - profile: car
    - profile: car_no_tolls_use_vignette
    - profile: car_no_tolls_no_vignette
    - profile: car_shortest


#  profiles_lm: []

  prepare.ch.threads: 4

  prepare.min_network_size: 600
  prepare.min_one_way_network_size: 600

#  routing.ch.disabling_allowed: false

  import.osm.ignored_highways: footway,construction,cycleway,path,pedestrian,steps,elevator,street_lamp,bus_stop,speed_camera

#  graph.do_sort: true


# Dropwizard server configuration
server:
  application_connectors:
  - type: http
    port: 8989
    # for security reasons bind to localhost
    bind_host: localhost
  request_log:
      appenders: []
  admin_connectors:
  - type: http
    port: 8990
    bind_host: localhost

and it works very well

because the app is fully offline and therefore it uses graphhopper v 1.0 as the latest

this is a different topic but…

The most recent version should be possible with termux or similar.

But definitely more recent versions are even possible for “native” Android with some tweaking (only bigger problem is janino but you can just use the compiled classes).

(lets create a different topic if you want to discuss it)