Routing for public transport on own graphhopper instance?

sorry if this was discussed and there’s a solution (i’m coming back to this forum after a while).
Graphhopper is a great tool and we’re using it on our own hosting with own GTFS editing software to auto-create shapes for trip patterns of routes. Using standard setup, where car profile is used to generate those. Of course struggling in cases of bus terminals where cars are forbidden. When switching to bike profile, it’s better, but in other places it’s using incorrect roads.
Please advise and again sorry if I missed the obvious solutions…

Did you have a look into a custom model for bus? See e.g. this discussion.

1 Like

@karussell Thanks for pointers. I will check it out. Do we need to build ourselves, or the jar files of graphhopper we download, have this and we just need to configure in the config.yml ?

The best example of the street that we can’t route (while auto-drawing shape for pt bus) is this:

Yes, you should be able to use the jar as it is and just the custom model here: graphhopper/bus.json at master · graphhopper/graphhopper · GitHub

(note: you need the latest version 7.0 to use this)

ok, so i downloaded 7.0 jar,
modifyed config file to this:

and because this command was fussing about missing bus.json file, i downloaded it from here:

and ended up with:

2023-03-30 11:16:24.452 [main] INFO  org.eclipse.jetty.util.log - Logging initialized @2014ms to org.eclipse.jetty.util.log.Slf4jLog
2023-03-30 11:16:24.542 [main] INFO  i.d.server.DefaultServerFactory - Registering jersey handler with root path prefix: /
2023-03-30 11:16:24.545 [main] INFO  i.d.server.DefaultServerFactory - Registering admin handler with root path prefix: /
cdjava.lang.RuntimeException: Cannot load custom_model from location bus.json for profile bus
        at com.graphhopper.http.GraphHopperManaged.resolveCustomModelFiles(
        at com.graphhopper.http.GraphHopperManaged.<init>(
        at com.graphhopper.application.GraphHopperApplication.main(
Caused by: com.fasterxml.jackson.core.JsonParseException: Unexpected character ('<' (code 60)): expected a valid value (JSON String, Number, Array, Object or token 'null', 'true' or 'false')
 at [Source: (StringReader); line: 8, column: 2]
        at com.fasterxml.jackson.core.JsonParser._constructError(
        at com.fasterxml.jackson.core.base.ParserMinimalBase._reportError(
        at com.fasterxml.jackson.core.base.ParserMinimalBase._reportUnexpectedChar(
        at com.fasterxml.jackson.core.json.ReaderBasedJsonParser._handleOddValue(
        at com.fasterxml.jackson.core.json.ReaderBasedJsonParser.nextToken(
        at com.fasterxml.jackson.databind.ObjectMapper._initForReading(
        at com.fasterxml.jackson.databind.ObjectMapper._readMapAndClose(
        at com.fasterxml.jackson.databind.ObjectMapper.readValue(
        at com.fasterxml.jackson.databind.ObjectMapper.readValue(
        at com.graphhopper.http.GraphHopperManaged.resolveCustomModelFiles(
        ... 9 more

[1]+  Exit 1                  java -D"dw.graphhopper.datareader.file=wielkopolskie-latest.osm.pbf" -jar graphhopper-web-7.0.jar server config-bus.yml  (wd: /home/wojciech/graphhopper)

my command to start is:
java -D"dw.graphhopper.datareader.file=wielkopolskie-latest.osm.pbf" -jar graphhopper-web-7.0.jar server config-bus.yml &

ok, i made some progress, as I cloned the whole repo to that folder.
used the new config-example.yml and added those new elements
graph.vehicles: roads|transportation_mode=BUS,car
graph.encoded_values: max_width,max_height
edited the profiles section with bus rather than car and foot rather than vehicle.

when starting the server i got:

2023-03-30 11:53:29.337 [main] ERROR io.dropwizard.cli.ServerCommand - Unable to start server, shutting down
java.lang.IllegalArgumentException: CH profile references unknown profile 'car'

Do i remove the:

    - profile: car

from config.yml ?

EDIT: i changed that profiles_ch from car to bus as well. This started the server correctly and I can route to that place !
Woohoo @karussell thanks !

1 Like

@karussell Just wanted to report on the performance of the bus profile. It’s been doing a great job, but I noticed that it does not take into consideration the one-way or correct road (if there are two roads for each direction available).
Is it wrong configuration of graphhopper on my side or is this a bug ?

I noticed that if I move the green starting marker a bit north, it will route correctly (it will go along the red line i drawn), but since this bus station is one way only and this roundabout to the east from that bus station is also clockwise and the street leading to it is also against the traffic ?

Settings in the request are as follows if that helps:

	"algorithm": "alternative_route",
	"alternative_route.max_paths": 3,
	"debug": false,
	"details": [
	"elevation": true,
	"instructions": true,
	"locale": "pl_PL",
	"optimize": "false",
	"points": [
	"points_encoded": true,
	"profile": "bus",
	"snap_preventions": [

Could I provide more info that would help diagnoze ?

What final configuration did you use?

This is my config.yml


  # OpenStreetMap input file PBF or XML, can be changed via command line -Ddw.graphhopper.datareader.file=some.pbf
  datareader.file: ""
  # Local folder used by graphhopper to store its data
  graph.location: graph-cache

  ##### Routing Profiles ####

  # Routing can be done only for profiles listed below. For more information about profiles and custom profiles have a
  # look into the documentation at docs/core/ or the examples under web/src/test/java/com/graphhopper/application/resources/
  # or the CustomWeighting class for the raw details.
  # In general a profile consists of the following
  # - name (required): a unique string identifier for the profile
  # - vehicle (required): refers to the `graph.vehicles` used for this profile
  # - weighting (required): the weighting used for this profile like custom,fastest,shortest or short_fastest
  # - turn_costs (true/false, default: false): whether or not turn restrictions should be applied for this profile.
  # Depending on the above fields there are other properties that can be used, e.g.
  # - distance_factor: 0.1 (can be used to fine tune the time/distance trade-off of short_fastest weighting)
  # - u_turn_costs: 60 (time-penalty for doing a u-turn in seconds (only possible when `turn_costs: true`)).
  #   Note that since the u-turn costs are given in seconds the weighting you use should also calculate the weight
  #   in seconds, so for example it does not work with shortest weighting.
  # - custom_model_file: when you specified "weighting: custom" you need to set a json file inside your
  #   or working directory that defines the custom_model. If you want an empty model you can also set "custom_model_file: empty".
  #   You can also use th e`custom_model` field instead and specify your custom model in the profile directly.
  # To prevent long running routing queries you should usually enable either speed or hybrid mode for all the given
  # profiles (see below). Or at least limit the number of `routing.max_visited_nodes`.

    - name: bus
      vehicle: roads
      weighting: custom
      custom_model_file: bus.json
        distance_influence: 70
#      turn_costs: true
#      u_turn_costs: 60

#    - name: bike
#      # to use the bike vehicle make sure to not ignore cycleways etc., see import.osm.ignored_highways below
#      vehicle: bike
#      weighting: custom
#      # the custom model in bike.json is defined to avoid hills
#      custom_model_file: bike.json

  # specify the folder where to find the custom model files custom_models

  # Speed mode:
  # Its possible to speed up routing by doing a special graph preparation (Contraction Hierarchies, CH). This requires
  # more RAM/disk space for holding the prepared graph but also means less memory usage per request. Using the following
  # list you can define for which of the above routing profiles such preparation shall be performed. Note that to support
  # profiles with `turn_costs: true` a more elaborate preparation is required (longer preparation time and more memory
  # usage) and the routing will also be slower than with `turn_costs: false`.
    - profile: bus

  # Hybrid mode:
  # Similar to speed mode, the hybrid mode (Landmarks, LM) also speeds up routing by doing calculating auxiliary data
  # in advance. Its not as fast as speed mode, but more flexible.
  # Advanced usage: It is possible to use the same preparation for multiple profiles which saves memory and preparation
  # time. To do this use e.g. `preparation_profile: my_other_profile` where `my_other_profile` is the name of another
  # profile for which an LM profile exists. Important: This only will give correct routing results if the weights
  # calculated for the profile are equal or larger (for every edge) than those calculated for the profile that was used
  # for the preparation (`my_other_profile`)
  profiles_lm: []

  #### Vehicles ####

  # The vehicle defines the base for how the routing of a profile behaves. It can be fine tuned using the options:
  # name=mycustomvehicle,block_private=true,turn_costs=true,transportation_mode=MOTOR_VEHICLE (only for the roads vehicle)
  # Still, it is recommended to avoid changing the vehicle settings and change the custom model instead.
  # graph.vehicles: car|block_fords=true,turn_costs=true,bike|turn_costs=true
  # Other standard vehicles: foot,bike,mtb,racingbike,motorcycle,wheelchair
  graph.vehicles: roads|transportation_mode=BUS,car
  #### Encoded Values ####

  # Add additional information to every edge. Used for path details (#1548) and custom models (docs/core/
  # Default values are: road_class,road_class_link,road_environment,max_speed,road_access
  # More are: surface,smoothness,max_width,max_height,max_weight,hgv,max_axle_load,max_length,hazmat,hazmat_tunnel,hazmat_water,
  #           lanes,osm_way_id,toll,track_type,mtb_rating,hike_rating,horse_rating
  # graph.encoded_values: surface,toll,track_type
  graph.encoded_values: max_width,max_height
  #### Speed, hybrid and flexible mode ####

  # To make CH preparation faster for multiple profiles you can increase the default threads if you have enough RAM.
  # Change this setting only if you know what you are doing and if the default worked for you.
  # 1

  # To tune the performance vs. memory usage for the hybrid mode use
  # prepare.lm.landmarks: 16

  # Make landmark preparation parallel if you have enough RAM. Change this only if you know what you are doing and if
  # the default worked for you.
  # prepare.lm.threads: 1

  #### Elevation ####

  # To populate your graph with elevation data use SRTM, default is noop (no elevation). Read more about it in docs/core/
  # graph.elevation.provider: srtm

  # default location for cache is /tmp/srtm
  # graph.elevation.cache_dir: ./srtmprovider/

  # If you have a slow disk or plenty of RAM change the default MMAP to:
  # graph.elevation.dataaccess: RAM_STORE

  # To enable bilinear interpolation when sampling elevation at points (default uses nearest neighbor):
  # graph.elevation.interpolate: bilinear

  # Reduce ascend/descend per edge without changing the maximum slope:
  # graph.elevation.edge_smoothing: ramer
  # removes elevation fluctuations up to max_elevation (in meter) and replaces the elevation with a value based on the average slope
  # graph.elevation.edge_smoothing.ramer.max_elevation: 5
  # A potentially bigger reduction of ascend/descend is possible, but maximum slope will often increase (do not use when average_slope and maximum_slope shall be used in a custom_model)
  # graph.elevation.edge_smoothing: moving_average

  # To increase elevation profile resolution, use the following two parameters to tune the extra resolution you need
  # against the additional storage space used for edge geometries. You should enable bilinear interpolation when using
  # these features (see #1953 for details).
  # - first, set the distance (in meters) at which elevation samples should be taken on long edges
  # graph.elevation.long_edge_sampling_distance: 60
  # - second, set the elevation tolerance (in meters) to use when simplifying polylines since the default ignores
  #   elevation and will remove the extra points that long edge sampling added
  # graph.elevation.way_point_max_distance: 10

  #### Urban density (built-up areas) ####

  # This feature allows classifying roads into 'rural', 'residential' and 'city' areas (encoded value 'urban_density')
  # Use 1 or more threads to enable the feature
  # graph.urban_density.threads: 8
  # Use higher/lower sensitivities if too little/many roads fall into the according categories.
  # Using smaller radii will speed up the classification, but only change these values if you know what you are doing.
  # If you do not need the (rather slow) city classification set city_radius to zero.
  # graph.urban_density.residential_radius: 300
  # graph.urban_density.residential_sensitivity: 60
  # graph.urban_density.city_radius: 2000
  # graph.urban_density.city_sensitivity: 30

  #### Subnetworks ####

  # In many cases the road network consists of independent components without any routes going in between. In
  # the most simple case you can imagine an island without a bridge or ferry connection. The following parameter
  # allows setting a minimum size (number of edges) for such detached components. This can be used to reduce the number
  # of cases where a connection between locations might not be found.
  prepare.min_network_size: 200
  prepare.subnetworks.threads: 1

  #### Routing ####

  # You can define the maximum visited nodes when routing. This may result in not found connections if there is no
  # connection between two points within the given visited nodes. The default is Integer.MAX_VALUE. Useful for flexibility mode
  # routing.max_visited_nodes: 1000000

  # Control how many active landmarks are picked per default, this can improve query performance
  # routing.lm.active_landmarks: 4

  # You can limit the max distance between two consecutive waypoints of flexible routing requests to be less or equal
  # the given distance in meter. Default is set to 1000km.
  routing.non_ch.max_waypoint_distance: 1000000

  #### Storage ####

  # Excludes certain types of highways during the OSM import to speed up the process and reduce the size of the graph.
  # A typical application is excluding 'footway','cycleway','path' and maybe 'pedestrian' and 'track' highways for
  # motorized vehicles. This leads to a smaller and less dense graph, because there are fewer ways (obviously),
  # but also because there are fewer crossings between highways (=junctions).
  # Another typical example is excluding 'motorway', 'trunk' and maybe 'primary' highways for bicycle or pedestrian routing.
  import.osm.ignored_highways: footway,cycleway,path,pedestrian,steps # typically useful for motorized-only routing
  # import.osm.ignored_highways: motorway,trunk # typically useful for non-motorized routing

  # configure the memory access, use RAM_STORE for well equipped servers (default and recommended)
  graph.dataaccess.default_type: RAM_STORE

  # will write way names in the preferred language (language code as defined in ISO 639-1 or ISO 639-2):
  # datareader.preferred_language: en

  # Sort the graph after import to make requests roughly ~10% faster. Note that this requires significantly more RAM on import.
  # graph.do_sort: true

  #### Custom Areas ####

  # GraphHopper reads GeoJSON polygon files including their properties from this directory and makes them available
  # to all tag parsers, vehicles and custom models. All GeoJSON Features require to have the "id" property.
  # Country borders are included automatically (see countries.geojson).
  # path/to/custom_areas

  #### Country Rules ####

  # GraphHopper applies country-specific routing rules during import (not enabled by default).
  # You need to redo the import for changes to take effect.
  # country_rules.enabled: true

# Dropwizard server configuration
  - type: http
    port: 8989
    # for security reasons bind to localhost
    #bind_host: localhost
      appenders: []
  - type: http
    port: 8990
    bind_host: localhost
# See
  - type: file
    time_zone: UTC
    current_log_filename: logs/graphhopper.log
    log_format: "%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n"
    archive: true
    archived_log_filename_pattern: ./logs/graphhopper-%d.log.gz
    archived_file_count: 30
    never_block: true
  - type: console
    time_zone: UTC
    log_format: "%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n"

I have to have a bad config, since it suggests left-side driving - check this roundabout (but it doesn’t happen on each of the roundabouts and road crossings (would country rules help somehow?) :

Sorry, I meant the bus.json. This is the important bit. How does it look?

It looks like this:

// to use this custom model you need to set the following option in the config.yml
// graph.vehicles: roads|transportation_mode=BUS,car
// graph.encoded_values: max_width,max_height
// profiles:
//    - name: bus
//      vehicle: roads
//      weighting: custom
//      custom_model_files: [bus.json]

  "distance_influence": 90,
  "priority": [
    { "if": "car_access == false || max_width < 3 || max_height < 4", "multiply_by": "0" }
  "speed": [
    { "if": "true", "limit_to": "car_average_speed * 0.9" },
    { "if": "true", "limit_to": "120" }

@karussell What is strange, is that the roundabouts are sometimes passed clockwise and sometimes anticlockwise, so it’s not a country setting i think - i think it’s just as quick as possible - which is not the case, especially for buses, that can’t travel like that…

Could it be that the ETAs are completely wrong too? Can you remove the custom_model from the config:

    custom_model_file: bus.json
        distance_influence: 70

We should probably throw an error I guess.

You mean comment out the bus.json from the config.yml ?

Instead of

  custom_model_file: bus.json
        distance_influence: 70

You must use

  custom_model_file: bus.json

Currently your config uses all roads (vehicle: roads) without any restrictions

I will push a fix that avoids this config mistake.

I commented out and this fragment looks like this now:

      custom_model_file: bus.json
#      custom_model:
#        distance_influence: 70

Cleard the cache and restarted the graphhopper.

I think it helped now. Should i leave it like that in all my graphhopper deployments or wait for some changes at your end ?

Please check the discussion at this pull request. Currently your bus profile is not more than a bigger, slightly slower car and e.g. does not route through bus-only roads.

Yes, i read that discussion. I can see it doesn’t route through bus-only roads, but the previous config (before i commented out) i believe it did ?

Yes, but the previous config basically said: ignore the bus.json and allow any road including roads that are only for pedestrian or bikes and including reverse oneway roads … even on a highway

The config “vehicle: roads” allows the profile to use all roads and you need to restrict it using the custom model either specified via an embedded custom_model or via external custom models via the custom_model_file attribute (in next releases it will be a list and custom_model_files)

And your ETAs will also return non-sense.

So what would be your suggestion? I like the fact it could route as bus (allow to enter bus only roads like bus terminal) but would cause to use right side of the road for “normally” accessible roads.