Repository: graphhopper/graphhopper (java)

Issue:
Load CH preparations in parallel
We currently load LM preparations with `prepare.lm.threads` threads. For CH we load the preparations one by one (not in parallel). I did a quick check and loading in parallel actually seems faster (up to x2, but also large fluctuations, using four threads when I tried it on my laptop with four files of size 3GB). 

Since (at least edge-based) CH preparations are comparable to LM preparations shall we load CH in parallel as well? The reason I ran into this was, again, that I was trying to disentangle the loading from the import code and in `LMPreparationHandler` the same parallel loop does the loading and import. For CH it is different, because we load the CH preparations with the base graph before `CHPreparationHandler` is doing anything (for no actual reason).

Here is my testing code:

```java
package com.graphhopper.tools;

import com.graphhopper.storage.DAType;
import com.graphhopper.storage.DataAccess;
import com.graphhopper.storage.GHDirectory;
import com.graphhopper.util.StopWatch;

import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;

public class ParallelLoad {
    public static void main(String[] args) throws ExecutionException, InterruptedException {
        {
            StopWatch swWrite = StopWatch.started();
            GHDirectory dir = new GHDirectory("parallel-load-test-gh", DAType.RAM_STORE);
            dir.create();
            DataAccess[] das = new DataAccess[4];
            for (int i = 0; i < 4; i++) {
                das[i] = dir.create("file" + i);
                das[i].create(3_000_000_000L);
                das[i].setInt(13, 54);
                das[i].flush();
                das[i].close();
            }
            System.out.println("Writing took: " + swWrite.stop().getTimeString());
        }
        {
            StopWatch swLoad = StopWatch.started();
            GHDirectory dir = new GHDirectory("bla", DAType.RAM_STORE);
            DataAccess[] das = new DataAccess[4];
            List<Future<String>> futures = new ArrayList<>(4);
            ExecutorService executorService = Executors.newFixedThreadPool(4);
            for (int i = 0; i < 4; i++) {
                final int index = i;
                futures.add(executorService.submit(() -> {
                    String name = "file" + index;
                    das[index] = dir.create(name);
                    das[index].loadExisting();
                    return name;
                }));
            }
            executorService.shutdown();
            for (int i = 0; i < 4; i++)
                futures.get(i).get();
            // just a quick check
            for (int i = 0; i < 4; i++)
                if (das[i].getInt(13) != 54)
                    throw new IllegalStateException();
            System.out.println("Loading took: " + swLoad.stop().getTimeString());
        }
    }
}
```

Using four threads:

```
Writing took: 15.74s
Loading took: 6.35s
```

Without ExecutorService (just one thread)
```
Writing took: 15.53s
Loading took: 10.90s
```

Pull request description:
Load CH preparations in parallel
Fixes #2454 

Patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index a9c4ed2f4..a5ad94a62 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,5 +1,6 @@
 ### 5.0 [not yet released]
 
+- fixed handling of too large mtb:scale tags
 - use GraphHopper#setGraphHopperLocation before calling load() instead of GraphHopper#load(graphHopperLocation) (#2437)
 - barrier nodes at junctions are now ignored (#2433)
 - AbstractFlagEncoder#handleNodeTags was replaced by AbstractFlagEncoder#isBarrier (#2434)
diff --git a/config-example.yml b/config-example.yml
index fe95609f8..df550fa0c 100644
--- a/config-example.yml
+++ b/config-example.yml
@@ -18,7 +18,7 @@ graphhopper:
   # Add additional information to every edge. Used for path details (#1548), better instructions (#1844) and tunnel/bridge interpolation (#798).
   # Default values are: road_class,road_class_link,road_environment,max_speed,road_access (since #1805)
   # More are: surface,smoothness,max_width,max_height,max_weight,max_axle_load,max_length,hazmat,hazmat_tunnel,hazmat_water,toll,track_type,
-  #           mtb_rating, hiking_rating,horse_rating,lanes
+  #           mtb_rating, hike_rating,horse_rating,lanes
   # graph.encoded_values: surface,toll,track_type
 
   ##### Routing Profiles ####
diff --git a/core/src/main/java/com/graphhopper/routing/ch/CHPreparationHandler.java b/core/src/main/java/com/graphhopper/routing/ch/CHPreparationHandler.java
index 074f44182..2c2d5382f 100644
--- a/core/src/main/java/com/graphhopper/routing/ch/CHPreparationHandler.java
+++ b/core/src/main/java/com/graphhopper/routing/ch/CHPreparationHandler.java
@@ -22,14 +22,14 @@ import com.graphhopper.config.CHProfile;
 import com.graphhopper.storage.CHConfig;
 import com.graphhopper.storage.GraphHopperStorage;
 import com.graphhopper.storage.StorableProperties;
+import com.graphhopper.util.GHUtility;
 import com.graphhopper.util.PMap;
 import com.graphhopper.util.Parameters.CH;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
 import java.util.*;
-import java.util.concurrent.ExecutorCompletionService;
-import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Callable;
 
 import static com.graphhopper.util.Helper.createFormatter;
 import static com.graphhopper.util.Helper.getMemInfo;
@@ -47,7 +47,6 @@ public class CHPreparationHandler {
     // the actual Weightings)
     private final List<CHProfile> chProfiles = new ArrayList<>();
     private int preparationThreads;
-    private ExecutorService threadPool;
     private PMap pMap = new PMap();
 
     public CHPreparationHandler() {
@@ -116,37 +115,26 @@ public class CHPreparationHandler {
      */
     public void setPreparationThreads(int preparationThreads) {
         this.preparationThreads = preparationThreads;
-        this.threadPool = java.util.concurrent.Executors.newFixedThreadPool(preparationThreads);
     }
 
     public void prepare(final StorableProperties properties, final boolean closeEarly) {
-        ExecutorCompletionService<String> completionService = new ExecutorCompletionService<>(threadPool);
-        int counter = 0;
-        for (final PrepareContractionHierarchies prepare : preparations) {
-            LOGGER.info((++counter) + "/" + preparations.size() + " calling " +
+        List<Callable<String>> callables = new ArrayList<>(preparations.size());
+        for (int i = 0; i < preparations.size(); ++i) {
+            PrepareContractionHierarchies prepare = preparations.get(i);
+            LOGGER.info((i + 1) + "/" + preparations.size() + " calling " +
                     "CH prepare.doWork for profile '" + prepare.getCHConfig().getName() + "' " + prepare.getCHConfig().getTraversalMode() + " ... (" + getMemInfo() + ")");
-            final String name = prepare.getCHConfig().getName();
-            completionService.submit(() -> {
+            callables.add(() -> {
+                final String name = prepare.getCHConfig().getName();
                 // toString is not taken into account so we need to cheat, see http://stackoverflow.com/q/6113746/194609 for other options
                 Thread.currentThread().setName(name);
                 prepare.doWork();
                 if (closeEarly)
                     prepare.close();
-
                 properties.put(CH.PREPARE + "date." + name, createFormatter().format(new Date()));
-            }, name);
-        }
-
-        threadPool.shutdown();
-
-        try {
-            for (int i = 0; i < preparations.size(); i++) {
-                completionService.take().get();
-            }
-        } catch (Exception e) {
-            threadPool.shutdownNow();
-            throw new RuntimeException(e);
+                return name;
+            });
         }
+        GHUtility.runConcurrently(callables, preparationThreads);
         LOGGER.info("Finished CH preparation, {}", getMemInfo());
     }
 
diff --git a/core/src/main/java/com/graphhopper/routing/lm/LMPreparationHandler.java b/core/src/main/java/com/graphhopper/routing/lm/LMPreparationHandler.java
index 5d966d4c4..d039ec20b 100644
--- a/core/src/main/java/com/graphhopper/routing/lm/LMPreparationHandler.java
+++ b/core/src/main/java/com/graphhopper/routing/lm/LMPreparationHandler.java
@@ -21,11 +21,11 @@ import com.bedatadriven.jackson.datatype.jts.JtsModule;
 import com.fasterxml.jackson.databind.ObjectMapper;
 import com.graphhopper.GraphHopperConfig;
 import com.graphhopper.config.LMProfile;
-import com.graphhopper.routing.ch.CHPreparationHandler;
 import com.graphhopper.routing.util.AreaIndex;
 import com.graphhopper.storage.GraphHopperStorage;
 import com.graphhopper.storage.StorableProperties;
 import com.graphhopper.storage.index.LocationIndex;
+import com.graphhopper.util.GHUtility;
 import com.graphhopper.util.JsonFeatureCollection;
 import com.graphhopper.util.Parameters;
 import com.graphhopper.util.Parameters.Landmark;
@@ -38,8 +38,7 @@ import java.io.InputStreamReader;
 import java.io.Reader;
 import java.net.URL;
 import java.util.*;
-import java.util.concurrent.ExecutorCompletionService;
-import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Callable;
 import java.util.concurrent.atomic.AtomicBoolean;
 import java.util.stream.Collectors;
 
@@ -60,7 +59,6 @@ public class LMPreparationHandler {
     private int minNodes = -1;
     private final List<String> lmSuggestionsLocations = new ArrayList<>(5);
     private int preparationThreads;
-    private ExecutorService threadPool;
     private boolean logDetails = false;
     private AreaIndex<SplitArea> areaIndex;
 
@@ -117,7 +115,6 @@ public class LMPreparationHandler {
      */
     public void setPreparationThreads(int preparationThreads) {
         this.preparationThreads = preparationThreads;
-        this.threadPool = java.util.concurrent.Executors.newFixedThreadPool(preparationThreads);
     }
 
     public LMPreparationHandler setLMProfiles(LMProfile... lmProfiles) {
@@ -167,7 +164,6 @@ public class LMPreparationHandler {
      * This method calculates the landmark data for all profiles (optionally in parallel) or if already existent loads it.
      *
      * @return true if the preparation data for at least one profile was calculated.
-     * @see CHPreparationHandler#prepare(StorableProperties, boolean) for a very similar method
      */
     public boolean loadOrDoWork(List<LMConfig> lmConfigs, GraphHopperStorage ghStorage, LocationIndex locationIndex, final boolean closeEarly) {
         createPreparations(lmConfigs, ghStorage, locationIndex);
@@ -177,40 +173,38 @@ public class LMPreparationHandler {
             if (areaIndex != null)
                 prep.setAreaIndex(areaIndex);
         }
+        // load existing preparations and keep track of those that could not be loaded
+        List<PrepareLandmarks> preparationsToPrepare = Collections.synchronizedList(new ArrayList<>());
+        List<Callable<String>> loadingCallables = preparations.stream()
+                .map(preparation -> (Callable<String>) () -> {
+                    if (!preparation.loadExisting())
+                        preparationsToPrepare.add(preparation);
+                    return preparation.getLMConfig().getName();
+                })
+                .collect(Collectors.toList());
+        GHUtility.runConcurrently(loadingCallables, preparationThreads);
+
+        // prepare the preparations that could not be loaded
         StorableProperties properties = ghStorage.getProperties();
-        ExecutorCompletionService<String> completionService = new ExecutorCompletionService<>(threadPool);
-        int counter = 0;
         final AtomicBoolean prepared = new AtomicBoolean(false);
-        for (final PrepareLandmarks plm : preparations) {
-            counter++;
-            final int tmpCounter = counter;
-            final String name = plm.getLMConfig().getName();
-            completionService.submit(() -> {
-                if (plm.loadExisting())
-                    return;
-
-                LOGGER.info(tmpCounter + "/" + getPreparations().size() + " calling LM prepare.doWork for " + plm.getLMConfig().getWeighting() + " ... (" + getMemInfo() + ")");
+        List<Callable<String>> prepareCallables = new ArrayList<>();
+        for (int i = 0; i < preparationsToPrepare.size(); i++) {
+            PrepareLandmarks prepare = preparationsToPrepare.get(i);
+            final int count = i + 1;
+            final String name = prepare.getLMConfig().getName();
+            prepareCallables.add(() -> {
+                LOGGER.info(count + "/" + preparationsToPrepare.size() + " calling LM prepare.doWork for " + prepare.getLMConfig().getWeighting() + " ... (" + getMemInfo() + ")");
                 prepared.set(true);
                 Thread.currentThread().setName(name);
-                plm.doWork();
-                if (closeEarly) {
-                    plm.close();
-                }
+                prepare.doWork();
+                if (closeEarly)
+                    prepare.close();
                 LOGGER.info("LM {} finished {}", name, getMemInfo());
                 properties.put(Landmark.PREPARE + "date." + name, createFormatter().format(new Date()));
-            }, name);
-        }
-
-        threadPool.shutdown();
-
-        try {
-            for (int i = 0; i < preparations.size(); i++) {
-                completionService.take().get();
-            }
-        } catch (Exception e) {
-            threadPool.shutdownNow();
-            throw new RuntimeException(e);
+                return name;
+            });
         }
+        GHUtility.runConcurrently(prepareCallables, preparationThreads);
         LOGGER.info("Finished LM preparation, {}", getMemInfo());
         return prepared.get();
     }
diff --git a/core/src/main/java/com/graphhopper/routing/util/parsers/OSMMtbRatingParser.java b/core/src/main/java/com/graphhopper/routing/util/parsers/OSMMtbRatingParser.java
index ab484a787..ff8c16d53 100644
--- a/core/src/main/java/com/graphhopper/routing/util/parsers/OSMMtbRatingParser.java
+++ b/core/src/main/java/com/graphhopper/routing/util/parsers/OSMMtbRatingParser.java
@@ -34,7 +34,6 @@ import java.util.List;
  * @see <a href=""http://www.singletrail-skala.de/>Single Trail Scale</a>
  */
 public class OSMMtbRatingParser implements TagParser {
-
     private final IntEncodedValue mtbRatingEnc;
 
     public OSMMtbRatingParser() {
@@ -59,10 +58,8 @@ public class OSMMtbRatingParser implements TagParser {
             } catch (Exception ex) {
             }
         }
-
-        if (rating != 0)
+        if (rating > 0 && rating < 8)
             mtbRatingEnc.setInt(false, edgeFlags, rating);
-
         return edgeFlags;
     }
 }
diff --git a/core/src/main/java/com/graphhopper/storage/GraphHopperStorage.java b/core/src/main/java/com/graphhopper/storage/GraphHopperStorage.java
index 3d93aa7fb..ba2f4a69e 100644
--- a/core/src/main/java/com/graphhopper/storage/GraphHopperStorage.java
+++ b/core/src/main/java/com/graphhopper/storage/GraphHopperStorage.java
@@ -21,10 +21,7 @@ import com.graphhopper.routing.util.AllEdgesIterator;
 import com.graphhopper.routing.util.EdgeFilter;
 import com.graphhopper.routing.util.EncodingManager;
 import com.graphhopper.routing.weighting.Weighting;
-import com.graphhopper.util.Constants;
-import com.graphhopper.util.EdgeExplorer;
-import com.graphhopper.util.EdgeIteratorState;
-import com.graphhopper.util.Helper;
+import com.graphhopper.util.*;
 import com.graphhopper.util.shapes.BBox;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
@@ -33,6 +30,7 @@ import java.io.Closeable;
 import java.util.ArrayList;
 import java.util.Collection;
 import java.util.List;
+import java.util.concurrent.Callable;
 import java.util.stream.Collectors;
 
 /**
@@ -270,10 +268,14 @@ public final class GraphHopperStorage implements Graph, Closeable {
 
             checkIfConfiguredAndLoadedWeightingsCompatible();
 
-            chEntries.forEach(cg -> {
-                if (!cg.chStore.loadExisting())
-                    throw new IllegalStateException("Cannot load " + cg);
-            });
+            List<Callable<String>> callables = chEntries.stream().map(c -> (Callable<String>) () -> {
+                        if (!c.chStore.loadExisting())
+                            throw new IllegalStateException("Cannot load " + c);
+                        return c.chConfig.getName();
+                    })
+                    .collect(Collectors.toList());
+            int numThreads = Math.max(1, Math.min(4, callables.size()));
+            GHUtility.runConcurrently(callables, numThreads);
 
             return true;
         }
diff --git a/core/src/main/java/com/graphhopper/util/GHUtility.java b/core/src/main/java/com/graphhopper/util/GHUtility.java
index 3f8c03a70..7f54efb91 100644
--- a/core/src/main/java/com/graphhopper/util/GHUtility.java
+++ b/core/src/main/java/com/graphhopper/util/GHUtility.java
@@ -39,6 +39,10 @@ import java.io.Reader;
 import java.io.UncheckedIOException;
 import java.nio.charset.StandardCharsets;
 import java.util.*;
+import java.util.concurrent.Callable;
+import java.util.concurrent.ExecutorCompletionService;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
 import java.util.concurrent.atomic.AtomicInteger;
 import java.util.stream.Collectors;
 
@@ -723,6 +727,20 @@ public class GHUtility {
         }
     }
 
+    public static void runConcurrently(List<Callable<String>> callables, int threads) {
+        ExecutorService executorService = Executors.newFixedThreadPool(threads);
+        ExecutorCompletionService<String> completionService = new ExecutorCompletionService<>(executorService);
+        callables.forEach(completionService::submit);
+        executorService.shutdown();
+        try {
+            for (int i = 0; i < callables.size(); i++)
+                completionService.take().get();
+        } catch (Exception e) {
+            executorService.shutdownNow();
+            throw new RuntimeException(e);
+        }
+    }
+
     /**
      * This edge iterator can be used in tests to mock specific iterator behaviour via overloading
      * certain methods.
diff --git a/reader-gtfs/files/sample-feed/routes.txt b/reader-gtfs/files/sample-feed/routes.txt
index 1804a89f9..cf5c292a2 100644
--- a/reader-gtfs/files/sample-feed/routes.txt
+++ b/reader-gtfs/files/sample-feed/routes.txt
@@ -1,5 +1,5 @@
 route_id,agency_id,route_short_name,route_long_name,route_desc,route_type,route_url,route_color,route_text_color
-AB,DTA,10,Airport - Bullfrog,,3,,,
+AB,DTA,10,Airport - Bullfrog,,2,,,
 BFC,DTA,20,Bullfrog - Furnace Creek Resort,,3,,,
 STBA,DTA,30,Stagecoach - Airport Shuttle,,3,,,
 CITY,DTA,40,City,,3,,,
diff --git a/reader-gtfs/src/main/java/com/graphhopper/gtfs/GraphExplorer.java b/reader-gtfs/src/main/java/com/graphhopper/gtfs/GraphExplorer.java
index 31f60250e..8d2fd2780 100644
--- a/reader-gtfs/src/main/java/com/graphhopper/gtfs/GraphExplorer.java
+++ b/reader-gtfs/src/main/java/com/graphhopper/gtfs/GraphExplorer.java
@@ -129,6 +129,9 @@ public final class GraphExplorer {
                     if ((edgeType == GtfsStorage.EdgeType.ENTER_PT || edgeType == GtfsStorage.EdgeType.EXIT_PT) && (blockedRouteTypes & (1 << edgeIterator.get(validityEnc))) != 0) {
                         continue;
                     }
+                    if (edgeType == GtfsStorage.EdgeType.TRANSFER && routeTypeBlocked(edgeIterator)) {
+                        continue;
+                    }
                     if (edgeType == GtfsStorage.EdgeType.ENTER_PT && justExitedPt(label)) {
                         continue;
                     }
@@ -138,6 +141,20 @@ public final class GraphExplorer {
                 return false;
             }
 
+            private boolean routeTypeBlocked(EdgeIterator edgeIterator) {
+                GtfsStorageI.PlatformDescriptor platformDescriptor = realtimeFeed.getPlatformDescriptorByEdge().get(edgeIterator.getEdge());
+                int routeType = routeType(platformDescriptor);
+                return (blockedRouteTypes & (1 << routeType)) != 0;
+            }
+
+            private int routeType(GtfsStorageI.PlatformDescriptor platformDescriptor) {
+                if (platformDescriptor instanceof GtfsStorageI.RouteTypePlatform) {
+                    return ((GtfsStorageI.RouteTypePlatform) platformDescriptor).route_type;
+                } else {
+                    return gtfsStorage.getGtfsFeeds().get(platformDescriptor.feed_id).routes.get(((GtfsStorageI.RoutePlatform) platformDescriptor).route_id).route_type;
+                }
+            }
+
             private boolean justExitedPt(Label label) {
                 if (label.edge == -1)
                     return false;
diff --git a/reader-gtfs/src/main/java/com/graphhopper/gtfs/GraphHopperGtfs.java b/reader-gtfs/src/main/java/com/graphhopper/gtfs/GraphHopperGtfs.java
index ea24bc0e1..1f34363bc 100644
--- a/reader-gtfs/src/main/java/com/graphhopper/gtfs/GraphHopperGtfs.java
+++ b/reader-gtfs/src/main/java/com/graphhopper/gtfs/GraphHopperGtfs.java
@@ -18,7 +18,6 @@
 
 package com.graphhopper.gtfs;
 
-import com.conveyal.gtfs.GTFSFeed;
 import com.conveyal.gtfs.model.Transfer;
 import com.graphhopper.GraphHopper;
 import com.graphhopper.GraphHopperConfig;
@@ -31,8 +30,10 @@ import com.graphhopper.storage.GraphHopperStorage;
 import com.graphhopper.storage.RAMDirectory;
 import com.graphhopper.storage.index.LocationIndex;
 import com.graphhopper.storage.index.LocationIndexTree;
-import com.graphhopper.storage.index.Snap;
-import com.graphhopper.util.*;
+import com.graphhopper.util.EdgeExplorer;
+import com.graphhopper.util.EdgeIterator;
+import com.graphhopper.util.EdgeIteratorState;
+import com.graphhopper.util.PMap;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
@@ -42,7 +43,6 @@ import java.nio.file.Paths;
 import java.time.Duration;
 import java.time.Instant;
 import java.util.*;
-import java.util.stream.Stream;
 
 public class GraphHopperGtfs extends GraphHopper {
 
@@ -78,12 +78,6 @@ public class GraphHopperGtfs extends GraphHopper {
         return null;
     }
 
-    static class TransferWithTime {
-        public String id;
-        Transfer transfer;
-        long time;
-    }
-
     @Override
     protected void importPublicTransit() {
         gtfsStorage = new GtfsStorage(getGraphHopperStorage().getDirectory());
@@ -108,12 +102,6 @@ public class GraphHopperGtfs extends GraphHopper {
                     allTransfers.put(id, transfers);
                     GtfsReader gtfsReader = new GtfsReader(id, graphHopperStorage, graphHopperStorage.getEncodingManager(), getGtfsStorage(), streetNetworkIndex, transfers);
                     gtfsReader.connectStopsToStreetNetwork();
-                    getType0TransferWithTimes(id, gtfsFeed)
-                            .forEach(t -> {
-                                t.transfer.transfer_type = 2;
-                                t.transfer.min_transfer_time = (int) (t.time / 1000L);
-                                gtfsFeed.transfers.put(t.id, t.transfer);
-                            });
                     LOGGER.info("Building transit graph for feed {}", gtfsFeed.feedId);
                     gtfsReader.buildPtNetwork();
                     allReaders.put(id, gtfsReader);
@@ -186,50 +174,6 @@ public class GraphHopperGtfs extends GraphHopper {
         }
     }
 
-    private Stream<TransferWithTime> getType0TransferWithTimes(String id, GTFSFeed gtfsFeed) {
-        GraphHopperStorage graphHopperStorage = getGraphHopperStorage();
-        RealtimeFeed realtimeFeed = RealtimeFeed.empty(getGtfsStorage());
-        PtEncodedValues ptEncodedValues = PtEncodedValues.fromEncodingManager(graphHopperStorage.getEncodingManager());
-        Weighting transferWeighting = createWeighting(getProfile("foot"), new PMap());
-        return gtfsFeed.transfers.entrySet()
-                .parallelStream()
-                .filter(e -> e.getValue().transfer_type == 0)
-                .map(e -> {
-                    PointList points = new PointList(2, false);
-                    final int fromnode = getGtfsStorage().getStationNodes().get(new GtfsStorage.FeedIdWithStopId(id, e.getValue().from_stop_id));
-                    final Snap fromstation = new Snap(graphHopperStorage.getNodeAccess().getLat(fromnode), graphHopperStorage.getNodeAccess().getLon(fromnode));
-                    fromstation.setClosestNode(fromnode);
-                    points.add(graphHopperStorage.getNodeAccess().getLat(fromnode), graphHopperStorage.getNodeAccess().getLon(fromnode));
-
-                    final int tonode = getGtfsStorage().getStationNodes().get(new GtfsStorage.FeedIdWithStopId(id, e.getValue().to_stop_id));
-                    final Snap tostation = new Snap(graphHopperStorage.getNodeAccess().getLat(tonode), graphHopperStorage.getNodeAccess().getLon(tonode));
-                    tostation.setClosestNode(tonode);
-                    points.add(graphHopperStorage.getNodeAccess().getLat(tonode), graphHopperStorage.getNodeAccess().getLon(tonode));
-
-                    QueryGraph queryGraph = QueryGraph.create(graphHopperStorage, Collections.emptyList());
-                    final GraphExplorer graphExplorer = new GraphExplorer(queryGraph, transferWeighting, ptEncodedValues, getGtfsStorage(), realtimeFeed, false, true, false, 5.0, false, 0);
-
-                    MultiCriteriaLabelSetting router = new MultiCriteriaLabelSetting(graphExplorer, ptEncodedValues, false, false, false, 0, new ArrayList<>());
-                    Iterator<Label> iterator = router.calcLabels(fromnode, Instant.ofEpochMilli(0)).iterator();
-                    Label solution = null;
-                    while (iterator.hasNext()) {
-                        Label label = iterator.next();
-                        if (tonode == label.adjNode) {
-                            solution = label;
-                            break;
-                        }
-                    }
-                    if (solution == null) {
-                        throw new RuntimeException("Can't find a transfer walk route.");
-                    }
-                    TransferWithTime transferWithTime = new TransferWithTime();
-                    transferWithTime.id = e.getKey();
-                    transferWithTime.transfer = e.getValue();
-                    transferWithTime.time = solution.currentTime;
-                    return transferWithTime;
-                });
-    }
-
     @Override
     public void close() {
         getGtfsStorage().close();


Test patch:
diff --git a/core/src/test/java/com/graphhopper/routing/util/parsers/OSMMtbRatingParserTest.java b/core/src/test/java/com/graphhopper/routing/util/parsers/OSMMtbRatingParserTest.java
new file mode 100644
index 000000000..69abe0fdf
--- /dev/null
+++ b/core/src/test/java/com/graphhopper/routing/util/parsers/OSMMtbRatingParserTest.java
@@ -0,0 +1,70 @@
+/*
+ *  Licensed to GraphHopper GmbH under one or more contributor
+ *  license agreements. See the NOTICE file distributed with this work for
+ *  additional information regarding copyright ownership.
+ *
+ *  GraphHopper GmbH licenses this file to you under the Apache License,
+ *  Version 2.0 (the "License"); you may not use this file except in
+ *  compliance with the License. You may obtain a copy of the License at
+ *
+ *       http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *  Unless required by applicable law or agreed to in writing, software
+ *  distributed under the License is distributed on an "AS IS" BASIS,
+ *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ *  See the License for the specific language governing permissions and
+ *  limitations under the License.
+ */
+
+package com.graphhopper.routing.util.parsers;
+
+import com.graphhopper.reader.ReaderWay;
+import com.graphhopper.routing.ev.IntEncodedValue;
+import com.graphhopper.routing.ev.MtbRating;
+import com.graphhopper.routing.util.EncodingManager;
+import com.graphhopper.storage.IntsRef;
+import org.junit.jupiter.api.Test;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+
+class OSMMtbRatingParserTest {
+
+    @Test
+    void test() {
+        // 0 means missing (or invalid)
+        checkRating(0, null);
+        // rating=scale+1
+        checkRating(1, "0");
+        checkRating(6, "5");
+        checkRating(7, "6");
+        // we ignore + and - for now
+        checkRating(2, "1+");
+        checkRating(3, "2-");
+        // negative
+        checkRating(0, "-1");
+        // otherwise invalid
+        checkRating(0, "S1");
+        // too large value
+        checkRating(0, "8");
+        // also 'too large' even though it probably means "S1/2" etc. -> still ignored
+        checkRating(0, "12");
+        checkRating(0, "23");
+        // ... similar here
+        checkRating(0, "1-2");
+        checkRating(0, "0;1");
+        checkRating(0, "1;2");
+    }
+
+    private void checkRating(int expectedRating, String scaleString) {
+        OSMMtbRatingParser parser = new OSMMtbRatingParser();
+        EncodingManager em = new EncodingManager.Builder().add(parser).build();
+        IntEncodedValue ev = em.getIntEncodedValue(MtbRating.KEY);
+        IntsRef edgeFlags = em.createEdgeFlags();
+        ReaderWay way = new ReaderWay(0);
+        if (scaleString != null)
+            way.setTag("mtb:scale", scaleString);
+        parser.handleWayTags(edgeFlags, way, false, em.createRelationFlags());
+        assertEquals(expectedRating, ev.getInt(false, edgeFlags), "unexpected rating for mtb:scale=" + scaleString);
+    }
+
+}
\ No newline at end of file
diff --git a/reader-gtfs/src/test/java/com/graphhopper/GraphHopperGtfsIT.java b/reader-gtfs/src/test/java/com/graphhopper/GraphHopperGtfsIT.java
index 20b21e112..521b0c127 100644
--- a/reader-gtfs/src/test/java/com/graphhopper/GraphHopperGtfsIT.java
+++ b/reader-gtfs/src/test/java/com/graphhopper/GraphHopperGtfsIT.java
@@ -349,7 +349,36 @@ public class GraphHopperGtfsIT {
         GHResponse response = ptRouter.route(ghRequest);
         assertEquals(time(1, 20), response.getAll().get(0).getTime(), "Expected travel time == scheduled travel time");
         assertEquals(time(7, 20), response.getAll().get(1).getTime(), "Expected travel time == scheduled travel time");
-        assertThat(response.getHints().getInt("visited_nodes.sum", Integer.MAX_VALUE)).isLessThanOrEqualTo(903);
+        assertThat(response.getHints().getInt("visited_nodes.sum", Integer.MAX_VALUE)).isLessThanOrEqualTo(904);
+    }
+
+    @Test
+    public void testBlockRouteTypes() {
+        Request ghRequest = new Request(Arrays.asList(
+                new GHStationLocation("AMV"),
+                new GHStationLocation("FUR_CREEK_RES")),
+                LocalDateTime.of(2007, 1, 7, 9, 0).atZone(zoneId).toInstant());
+        GHResponse response = ptRouter.route(ghRequest);
+        ResponsePath mondayTrip = response.getBest();
+        assertEquals("AB", ((Trip.PtLeg) mondayTrip.getLegs().get(1)).route_id);
+
+        ghRequest = new Request(Arrays.asList(
+                new GHStationLocation("BEATTY_AIRPORT"),
+                new GHStationLocation("FUR_CREEK_RES")),
+                LocalDateTime.of(2007, 1, 7, 9, 0).atZone(zoneId).toInstant());
+        ghRequest.setBlockedRouteTypes(4);
+        response = ptRouter.route(ghRequest);
+        mondayTrip = response.getBest();
+        assertNotEquals("AB", ((Trip.PtLeg) mondayTrip.getLegs().get(0)).route_id);
+
+        ghRequest = new Request(Arrays.asList(
+                new GHStationLocation("AMV"),
+                new GHStationLocation("FUR_CREEK_RES")),
+                LocalDateTime.of(2007, 1, 7, 9, 0).atZone(zoneId).toInstant());
+        ghRequest.setBlockedRouteTypes(4);
+        response = ptRouter.route(ghRequest);
+        mondayTrip = response.getBest();
+        assertNotEquals("AB", ((Trip.PtLeg) mondayTrip.getLegs().get(1)).route_id);
     }
 
     @Test