Repository: graphhopper/graphhopper (java)

Issue:
Rare bug for stall on demand
Currently I'm facing an issue when stall_on_demand=true (if false it disappears). It is very rare and hard to reproduce in a unit test.

The reason is likely in DijkstraBidirectionCH.entryIsStallable:
```
adjNode.weight + weighting.calcWeight(iter, !reverse, getIncomingEdge(entry)) < entry.weight
```

It returns true although it should return false. In my case it is the 4th decimal place: `35.643+50.2975 < 50.297522`

So, when I round to the precision we currently use in CHGraphImpl:
`Math.round(entry.weight / 1000f) * 1000f`
the problem disappears. But maybe it is related to virtual nodes because the special case is a QueryGraph with 3 locations where the fastest route B->C crosses point A.

Pull request description:
Fixes wrong routing occurring when stall on demand is used with virtual via-nodes.
Fixes #1574.

I added a test that creates random routing situations including ones that lead to wrong routing results (sometimes connection not found but also wrong weights). This PR also adds a failing integration test and a specific test with only 10 nodes that shows the problem in detail (also see comments in #1574).

This PR fixes all these tests and also the random test did not fail after running it for more than 15 minutes. 

The performance measurements on the Germany map showed that the query performance before and after this fix is the same.

When I was experimenting with the the random test I ran into two more (probably very rare) error situations: #1582, #1583 

Patch:
diff --git a/core/src/main/java/com/graphhopper/routing/DijkstraBidirectionCH.java b/core/src/main/java/com/graphhopper/routing/DijkstraBidirectionCH.java
index 09391def2..cfc3a31cc 100644
--- a/core/src/main/java/com/graphhopper/routing/DijkstraBidirectionCH.java
+++ b/core/src/main/java/com/graphhopper/routing/DijkstraBidirectionCH.java
@@ -26,12 +26,14 @@ import com.graphhopper.util.EdgeIterator;
 
 /**
  * Uses a very simple version of stall-on-demand (SOD) for CH queries to prevent exploring nodes that can not be part
- * of a shortest path. When a node that is about to be settled is stallable it is not expanded, but no further search
- * for neighboring stallable nodes is performed.
+ * of a shortest path. When a node that is about to be settled is stallable it is not expanded. However, no further search
+ * for neighboring stallable nodes is performed (sometimes called 'aggressive' stalling in the literature). Some experimenting
+ * showed that due to the overhead for such aggressive stalling the routing does not become faster, see #240.
  *
  * @author easbar
  */
 public class DijkstraBidirectionCH extends DijkstraBidirectionCHNoSOD {
+
     public DijkstraBidirectionCH(Graph graph, Weighting weighting) {
         super(graph, weighting);
     }
@@ -62,10 +64,16 @@ public class DijkstraBidirectionCH extends DijkstraBidirectionCHNoSOD {
         // reached via a suboptimal path. We do this regardless of the CH level of the adjacent nodes.
         EdgeIterator iter = edgeExplorer.setBaseNode(entry.adjNode);
         while (iter.next()) {
+            // no need to inspect the edge we are coming from
+            if (iter.getEdge() == entry.adjNode) {
+                continue;
+            }
             int traversalId = traversalMode.createTraversalId(iter, reverse);
             SPTEntry adjNode = bestWeightMap.get(traversalId);
+            // we have to be careful because of rounded shortcut weights in combination with virtual via nodes, see #1574
+            final double precision = 0.001;
             if (adjNode != null &&
-                    adjNode.weight + weighting.calcWeight(iter, !reverse, getIncomingEdge(entry)) < entry.weight) {
+                    adjNode.weight + weighting.calcWeight(iter, !reverse, getIncomingEdge(entry)) - entry.weight < -precision) {
                 return true;
             }
         }
diff --git a/core/src/main/java/com/graphhopper/routing/VirtualEdgeIterator.java b/core/src/main/java/com/graphhopper/routing/VirtualEdgeIterator.java
index c873f90e5..039369650 100644
--- a/core/src/main/java/com/graphhopper/routing/VirtualEdgeIterator.java
+++ b/core/src/main/java/com/graphhopper/routing/VirtualEdgeIterator.java
@@ -213,7 +213,11 @@ class VirtualEdgeIterator implements EdgeIterator, CHEdgeIteratorState {
 
     @Override
     public String toString() {
-        return edges.toString();
+        if (current >= 0 && current < edges.size()) {
+            return "virtual edge: " + edges.get(current) + ", all: " + edges.toString();
+        } else {
+            return "virtual edge: (invalid)" + ", all: " + edges.toString();
+        }
     }
 
     @Override
diff --git a/core/src/main/java/com/graphhopper/storage/index/QueryResult.java b/core/src/main/java/com/graphhopper/storage/index/QueryResult.java
index ed68a50c1..fe8bd4baa 100644
--- a/core/src/main/java/com/graphhopper/storage/index/QueryResult.java
+++ b/core/src/main/java/com/graphhopper/storage/index/QueryResult.java
@@ -164,7 +164,7 @@ public class QueryResult {
     @Override
     public String toString() {
         if (closestEdge != null)
-            return closestEdge.getBaseNode() + "-" + closestEdge.getAdjNode() + "  " + snappedPoint;
+            return closestEdge.getBaseNode() + "-" + closestEdge.getAdjNode() + "  " + snappedPoint + ", " + queryPoint;
         return closestNode + ", " + queryPoint + ", " + wayIndex;
     }
 
diff --git a/core/src/main/java/com/graphhopper/util/GHUtility.java b/core/src/main/java/com/graphhopper/util/GHUtility.java
index e4596d1b9..6b3df1be3 100644
--- a/core/src/main/java/com/graphhopper/util/GHUtility.java
+++ b/core/src/main/java/com/graphhopper/util/GHUtility.java
@@ -198,7 +198,7 @@ public class GHUtility {
                 graph.getNodes(), graph.getAllEdges().length(), minDist, maxDist));
     }
 
-    private static double getDistance(int from, int to, NodeAccess nodeAccess) {
+    public static double getDistance(int from, int to, NodeAccess nodeAccess) {
         double fromLat = nodeAccess.getLat(from);
         double fromLon = nodeAccess.getLon(from);
         double toLat = nodeAccess.getLat(to);


Test patch:
diff --git a/core/src/test/java/com/graphhopper/routing/QueryGraphTest.java b/core/src/test/java/com/graphhopper/routing/QueryGraphTest.java
index 5010b7cd2..0405a339e 100644
--- a/core/src/test/java/com/graphhopper/routing/QueryGraphTest.java
+++ b/core/src/test/java/com/graphhopper/routing/QueryGraphTest.java
@@ -162,9 +162,9 @@ public class QueryGraphTest {
                 super.fillVirtualEdges(node2Edge, towerNode, mainExpl);
                 // ignore nodes should include baseNode == 1
                 if (towerNode == 3)
-                    assertEquals("[3->4]", node2Edge.get(towerNode).toString());
+                    assertEquals("virtual edge: (invalid), all: [3->4]", node2Edge.get(towerNode).toString());
                 else if (towerNode == 1)
-                    assertEquals("[1->4, 1 1-0]", node2Edge.get(towerNode).toString());
+                    assertEquals("virtual edge: (invalid), all: [1->4, 1 1-0]", node2Edge.get(towerNode).toString());
                 else
                     throw new IllegalStateException("not allowed " + towerNode);
             }
diff --git a/core/src/test/java/com/graphhopper/routing/RandomCHRoutingTest.java b/core/src/test/java/com/graphhopper/routing/RandomCHRoutingTest.java
new file mode 100644
index 000000000..c61111e03
--- /dev/null
+++ b/core/src/test/java/com/graphhopper/routing/RandomCHRoutingTest.java
@@ -0,0 +1,184 @@
+package com.graphhopper.routing;
+
+import com.graphhopper.RepeatRule;
+import com.graphhopper.routing.ch.PrepareContractionHierarchies;
+import com.graphhopper.routing.profiles.DecimalEncodedValue;
+import com.graphhopper.routing.util.CarFlagEncoder;
+import com.graphhopper.routing.util.EdgeFilter;
+import com.graphhopper.routing.util.EncodingManager;
+import com.graphhopper.routing.util.TraversalMode;
+import com.graphhopper.routing.weighting.FastestWeighting;
+import com.graphhopper.routing.weighting.Weighting;
+import com.graphhopper.storage.*;
+import com.graphhopper.storage.index.LocationIndexTree;
+import com.graphhopper.storage.index.QueryResult;
+import com.graphhopper.util.EdgeIteratorState;
+import com.graphhopper.util.GHUtility;
+import com.graphhopper.util.PMap;
+import com.graphhopper.util.shapes.BBox;
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Random;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.fail;
+
+public class RandomCHRoutingTest {
+    private final TraversalMode traversalMode = TraversalMode.NODE_BASED;
+    private Directory dir;
+    private CarFlagEncoder encoder;
+    private Weighting weighting;
+    private GraphHopperStorage graph;
+    private LocationIndexTree locationIndex;
+    private CHGraph chGraph;
+
+    @Rule
+    public RepeatRule repeatRule = new RepeatRule();
+
+    @Before
+    public void init() {
+        dir = new RAMDirectory();
+        encoder = new CarFlagEncoder();
+        EncodingManager em = EncodingManager.create(encoder);
+        weighting = new FastestWeighting(encoder);
+        graph = new GraphBuilder(em).setCHGraph(weighting).create();
+        chGraph = graph.getGraph(CHGraph.class);
+    }
+
+
+    /**
+     * Runs random routing queries on a random query/CH graph with random speeds and adding random virtual edges and
+     * nodes.
+     */
+    @Test
+    public void issue1574_random() {
+        // you might have to keep this test running in an infinite loop for several minutes to find potential routing
+        // bugs (e.g. use intellij 'run until stop/failure').
+        int numNodes = 50;
+        long seed = System.nanoTime();
+        // for example these used to fail before fixing #1574.
+//        seed = 9348906923700L;
+//        seed = 9376976930825L;
+//        seed = 9436934744695L;
+//        seed = 10093639220394L;
+
+        System.out.println("seed: " + seed);
+        Random rnd = new Random(seed);
+        // todo: allowing loops also produces another error (may not read speed in wrong direction...), e.g. with this seed: 10785899964423
+        boolean allowLoops = false;
+        buildRandomGraph(rnd, numNodes, 2.5, allowLoops, true, 0.9);
+        locationIndex = new LocationIndexTree(graph, dir);
+        locationIndex.prepareIndex();
+
+        graph.freeze();
+        PrepareContractionHierarchies pch = new PrepareContractionHierarchies(chGraph, weighting, traversalMode);
+        pch.doWork();
+
+        int numQueryGraph = 50;
+        for (int j = 0; j < numQueryGraph; j++) {
+            QueryGraph queryGraph = new QueryGraph(graph);
+            QueryGraph chQueryGraph = new QueryGraph(chGraph);
+            addVirtualNodesAndEdges(rnd, queryGraph, chQueryGraph);
+
+            int numQueries = 100;
+            int numPathsNotFound = 0;
+            for (int i = 0; i < numQueries; i++) {
+                assertEquals("queryGraph and chQueryGraph should have equal number of nodes", queryGraph.getNodes(), chQueryGraph.getNodes());
+                int from = rnd.nextInt(queryGraph.getNodes());
+                int to = rnd.nextInt(queryGraph.getNodes());
+                DijkstraBidirectionRef refAlgo = new DijkstraBidirectionRef(queryGraph, weighting, TraversalMode.NODE_BASED);
+                Path refPath = refAlgo.calcPath(from, to);
+                if (!refPath.isFound()) {
+                    numPathsNotFound++;
+                    continue;
+                }
+
+                RoutingAlgorithm algo = pch.createAlgo(chQueryGraph, AlgorithmOptions.start().hints(new PMap().put("stall_on_demand", true)).build());
+                Path path = algo.calcPath(from, to);
+                if (!path.isFound()) {
+                    fail("path not found for for " + from + "->" + to + ", expected weight: " + path.getWeight());
+                }
+
+                double weight = path.getWeight();
+                double refWeight = refPath.getWeight();
+                if (Math.abs(refWeight - weight) > 1) {
+                    fail("wrong weight: " + from + "->" + to + ", dijkstra: " + refWeight + " vs. ch: " + path.getWeight());
+                }
+            }
+            if (numPathsNotFound > 0.9 * numQueries) {
+                fail("Too many paths not found: " + numPathsNotFound + "/" + numQueries);
+            }
+        }
+    }
+
+    private void addVirtualNodesAndEdges(Random rnd, QueryGraph queryGraph, QueryGraph chQueryGraph) {
+        BBox bbox = graph.getBounds();
+        int numVirtualNodes = 20;
+        int count = 0;
+        List<QueryResult> qrs = new ArrayList<>(numVirtualNodes);
+        while (qrs.size() < numVirtualNodes) {
+            if (count > numVirtualNodes * 100) {
+                throw new IllegalArgumentException("Could not create enough virtual edges");
+            }
+            QueryResult qr = findQueryResult(rnd, bbox);
+            if (qr.getSnappedPosition().equals(QueryResult.Position.EDGE)) {
+                qrs.add(qr);
+            }
+            count++;
+        }
+        queryGraph.lookup(qrs);
+        chQueryGraph.lookup(qrs);
+    }
+
+    private QueryResult findQueryResult(Random rnd, BBox bbox) {
+        return locationIndex.findClosest(
+                randomDoubleInRange(rnd, bbox.minLat, bbox.maxLat),
+                randomDoubleInRange(rnd, bbox.minLon, bbox.maxLon),
+                EdgeFilter.ALL_EDGES
+        );
+    }
+
+    private double randomDoubleInRange(Random rnd, double min, double max) {
+        return min + rnd.nextDouble() * (max - min);
+    }
+
+    private void buildRandomGraph(Random random, int numNodes, double meanDegree, boolean allowLoops, boolean allowZeroDistance, double pBothDir) {
+        for (int i = 0; i < numNodes; ++i) {
+            double lat = 49.4 + (random.nextDouble() * 0.0001);
+            double lon = 9.7 + (random.nextDouble() * 0.0001);
+            graph.getNodeAccess().setNode(i, lat, lon);
+        }
+        double minDist = Double.MAX_VALUE;
+        double maxDist = Double.MIN_VALUE;
+        int numEdges = (int) (0.5 * meanDegree * numNodes);
+        for (int i = 0; i < numEdges; ++i) {
+            int from = random.nextInt(numNodes);
+            int to = random.nextInt(numNodes);
+            if (!allowLoops && from == to) {
+                continue;
+            }
+            double distance = GHUtility.getDistance(from, to, graph.getNodeAccess());
+            if (!allowZeroDistance) {
+                distance = Math.max(0.001, distance);
+            }
+            // add some random offset for most cases, but also allow duplicate edges with same weight
+            if (random.nextDouble() < 0.8)
+                distance += random.nextDouble() * distance * 0.01;
+            minDist = Math.min(minDist, distance);
+            maxDist = Math.max(maxDist, distance);
+            // using bidirectional edges will increase mean degree of graph above given value
+            boolean bothDirections = random.nextDouble() < pBothDir;
+            EdgeIteratorState edge = graph.edge(from, to, distance, bothDirections);
+            double fwdSpeed = 10 + random.nextDouble() * 120;
+            double bwdSpeed = 10 + random.nextDouble() * 120;
+            DecimalEncodedValue speedEnc = encoder.getAverageSpeedEnc();
+            edge.set(speedEnc, fwdSpeed);
+            edge.setReverse(speedEnc, bwdSpeed);
+        }
+    }
+}
+
diff --git a/core/src/test/java/com/graphhopper/routing/ch/PrepareContractionHierarchiesTest.java b/core/src/test/java/com/graphhopper/routing/ch/PrepareContractionHierarchiesTest.java
index 39d988964..a15d0b3fc 100644
--- a/core/src/test/java/com/graphhopper/routing/ch/PrepareContractionHierarchiesTest.java
+++ b/core/src/test/java/com/graphhopper/routing/ch/PrepareContractionHierarchiesTest.java
@@ -19,21 +19,20 @@ package com.graphhopper.routing.ch;
 
 import com.carrotsearch.hppc.IntArrayList;
 import com.carrotsearch.hppc.IntIndexedContainer;
-import com.graphhopper.routing.AlgorithmOptions;
-import com.graphhopper.routing.Dijkstra;
-import com.graphhopper.routing.Path;
-import com.graphhopper.routing.RoutingAlgorithm;
+import com.graphhopper.routing.*;
 import com.graphhopper.routing.util.*;
 import com.graphhopper.routing.weighting.FastestWeighting;
 import com.graphhopper.routing.weighting.ShortestWeighting;
 import com.graphhopper.routing.weighting.Weighting;
 import com.graphhopper.storage.*;
+import com.graphhopper.storage.index.QueryResult;
 import com.graphhopper.util.*;
 import org.junit.Before;
 import org.junit.Test;
 
 import java.util.*;
 
+import static com.graphhopper.routing.AbstractRoutingAlgorithmTester.updateDistancesFor;
 import static com.graphhopper.util.Parameters.Algorithms.DIJKSTRA_BI;
 import static org.junit.Assert.*;
 
@@ -122,7 +121,11 @@ public class PrepareContractionHierarchiesTest {
     }
 
     GraphHopperStorage createGHStorage() {
-        return new GraphBuilder(encodingManager).setCHGraph(weighting).create();
+        return createGHStorage(weighting);
+    }
+
+    GraphHopperStorage createGHStorage(Weighting w) {
+        return new GraphBuilder(encodingManager).setCHGraph(w).create();
     }
 
     GraphHopperStorage createExampleGraph() {
@@ -419,6 +422,108 @@ public class PrepareContractionHierarchiesTest {
         return new HashSet<>(Arrays.asList(values));
     }
 
+    @Test
+    public void testStallOnDemandViaVirtuaNode_issue1574() {
+        // this test reproduces the issue that appeared in issue1574 and the problem is very intricate
+        // the problem is a combination of all these things:
+        // * contraction hierarchies
+        // * stall-on-demand (without sod there is no problem, at least in this test)
+        // * shortcuts weight rounding
+        // * via nodes/virtual edges and the associated weight precision (without virtual nodes between source and target
+        //   there is no problem, but this can happen for via routes
+        // * the fact that the LevelEdgeFilter always accepts virtual nodes
+
+        // use fastest weighting in this test to be able to fine-tune some weights via the speed (see below)
+        Weighting fastestWeighting = new FastestWeighting(carEncoder);
+        final GraphHopperStorage g = createGHStorage();
+        CHGraph lg = g.getGraph(CHGraph.class);
+        // the following graph reproduces the issue. note that we will use the node ids as ch levels, so there will
+        // be a shortcuts from 1->3 and 2->3 (not the other way around, because of shortcut disconnections!)
+        // since the shortest path is strictly upward only the forward search runs here, this is also important!
+        // start 0 - 3 - x - 1 - 2
+        //             \         |
+        //               sc ---- 4 - 5 - 6 - 7 finish
+        g.edge(0, 3, 1, true);
+        EdgeIteratorState edge31 = g.edge(3, 1, 1, true);
+        g.edge(1, 2, 1, true);
+        EdgeIteratorState edge24 = g.edge(2, 4, 1, true);
+        g.edge(4, 5, 1, true);
+        g.edge(5, 6, 1, true);
+        g.edge(6, 7, 1, true);
+        updateDistancesFor(g, 0, 0.001, 0.0000);
+        updateDistancesFor(g, 3, 0.001, 0.0001);
+        updateDistancesFor(g, 1, 0.001, 0.0002);
+        updateDistancesFor(g, 2, 0.001, 0.0003);
+        updateDistancesFor(g, 4, 0.000, 0.0003);
+        updateDistancesFor(g, 5, 0.000, 0.0004);
+        updateDistancesFor(g, 6, 0.000, 0.0005);
+        updateDistancesFor(g, 7, 0.000, 0.0006);
+
+        // we use the speed to fine tune some weights:
+        // the weight of edge 3-1 must be such that node 2 gets stalled in the forward search via the incoming shortcut
+        // at node 2 coming from 3. this happens because due to the virtual node x between 3 and 1, the spt entries
+        // calculated on the query graph (using the virtual edges) use different floating point rounding / arithmetics.
+        edge31.set(carEncoder.getAverageSpeedEnc(), 22);
+
+        // just stalling node 2 due to the differently calculated weights for the virtual edges would be no problem, yet
+        // because the shortcut 3-4 still finds node 4. however node 4 might also get stalled via node 2. 'normally' this
+        // would not happen, because node 2 would not even be explored in the forward search, but because of the virtual
+        // node the strict upward search is modified and goes like 0-3-1-2 (i.e. it finds node 2).
+        // so no we fine tune the weight for 2-4 such that node 4 gets also stalled
+        edge24.setReverse(carEncoder.getAverageSpeedEnc(), 27.5);
+
+        // prepare ch, use node ids as levels
+        PrepareContractionHierarchies pch = createPrepareContractionHierarchies(g, lg, fastestWeighting);
+        pch.useFixedNodeOrdering(new NodeOrderingProvider() {
+            @Override
+            public int getNodeIdForLevel(int level) {
+                return level;
+            }
+
+            @Override
+            public int getNumNodes() {
+                return g.getNodes();
+            }
+        }).doWork();
+        assertEquals("there should be exactly two shortcuts (3->2) and (3->4)", 2, lg.getEdges() - lg.getOriginalEdges());
+
+        // insert virtual node and edges
+        QueryResult qr = new QueryResult(0.0001, 0.0015);
+        qr.setClosestEdge(edge31);
+        qr.setSnappedPosition(QueryResult.Position.EDGE);
+        qr.setClosestNode(8);
+        qr.setWayIndex(0);
+        qr.calcSnappedPoint(new DistanceCalc2D());
+        QueryGraph queryGraph = new QueryGraph(lg);
+        queryGraph.lookup(Collections.singletonList(qr));
+
+        // we make sure our weight fine tunings do what they are supposed to
+        double weight03 = getWeight(queryGraph, fastestWeighting, 0, 3);
+        double scWeight23 = weight03 + ((CHEdgeIteratorState) getEdge(lg, 2, 3)).getWeight();
+        double scWeight34 = weight03 + ((CHEdgeIteratorState) getEdge(lg, 3, 4)).getWeight();
+        double sptWeight2 = weight03 + getWeight(queryGraph, fastestWeighting, 3, 8) + getWeight(queryGraph, fastestWeighting, 8, 1) + getWeight(queryGraph, fastestWeighting, 1, 2);
+        double sptWeight4 = sptWeight2 + getWeight(queryGraph, fastestWeighting, 2, 4);
+        assertTrue("incoming shortcut weight 3->2 should be smaller than sptWeight at node 2 to make sure 2 gets stalled", scWeight23 < sptWeight2);
+        assertTrue("sptWeight at node 4 should be smaller than shortcut weight 3->4 to make sure node 4 gets stalled", sptWeight4 < scWeight34);
+
+        Path path = pch.createAlgo(queryGraph, AlgorithmOptions.start().build()).calcPath(0, 7);
+        assertEquals("wrong or no path found", IntArrayList.from(0, 3, 8, 1, 2, 4, 5, 6, 7), path.calcNodes());
+    }
+
+    private double getWeight(Graph graph, Weighting w, int from, int to) {
+        return w.calcWeight(getEdge(graph, from, to), false, -1);
+    }
+
+    private EdgeIteratorState getEdge(Graph graph, int from, int to) {
+        EdgeIterator iter = graph.createEdgeExplorer().setBaseNode(from);
+        while (iter.next()) {
+            if (iter.getAdjNode() == to) {
+                return iter;
+            }
+        }
+        throw new IllegalArgumentException("Could not find edge from: " + from + " to: " + to);
+    }
+
     @Test
     public void testCircleBug() {
         GraphHopperStorage g = createGHStorage();
diff --git a/web/src/test/java/com/graphhopper/http/resources/RouteResourceIssue1574Test.java b/web/src/test/java/com/graphhopper/http/resources/RouteResourceIssue1574Test.java
new file mode 100644
index 000000000..828733d97
--- /dev/null
+++ b/web/src/test/java/com/graphhopper/http/resources/RouteResourceIssue1574Test.java
@@ -0,0 +1,74 @@
+/*
+ *  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.http.resources;
+
+import com.fasterxml.jackson.databind.JsonNode;
+import com.graphhopper.http.GraphHopperApplication;
+import com.graphhopper.http.GraphHopperServerConfiguration;
+import com.graphhopper.util.CmdArgs;
+import com.graphhopper.util.Helper;
+import io.dropwizard.testing.junit.DropwizardAppRule;
+import org.junit.AfterClass;
+import org.junit.BeforeClass;
+import org.junit.ClassRule;
+import org.junit.Test;
+
+import javax.ws.rs.core.Response;
+import java.io.File;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+
+/**
+ * @author Peter Karich
+ */
+public class RouteResourceIssue1574Test {
+    private static final String DIR = "./target/andorra-1574-gh/";
+
+    private static final GraphHopperServerConfiguration config = new GraphHopperServerConfiguration();
+
+    static {
+        // this is the reason we put this test into an extra file: we can only reproduce the bug of issue 1574 by increasing the one-way-network size
+        config.getGraphHopperConfiguration().merge(new CmdArgs().
+                put("graph.flag_encoders", "car").
+                put("prepare.ch.weightings", "fastest").
+                put("prepare.min_network_size", "0").
+                put("prepare.min_one_way_network_size", "12").
+                put("datareader.file", "../core/files/andorra.osm.pbf").
+                put("graph.location", DIR));
+    }
+
+    @ClassRule
+    public static final DropwizardAppRule<GraphHopperServerConfiguration> app = new DropwizardAppRule<>(GraphHopperApplication.class, config);
+
+    @BeforeClass
+    @AfterClass
+    public static void cleanUp() {
+        Helper.removeDir(new File(DIR));
+    }
+
+    @Test
+    public void testStallOnDemandBug_issue1574() {
+        final Response response = app.client().target("http://localhost:8080/route?point=42.486984,1.493152&point=42.481863,1.491297&point=42.49697,1.501265&&vehicle=car&weighting=fastest&stall_on_demand=true").request().buildGet().invoke();
+        JsonNode json = response.readEntity(JsonNode.class);
+        assertFalse("there should be no error, but: " + json.get("message"), json.has("message"));
+        System.out.println(json);
+        assertEquals(200, response.getStatus());
+    }
+
+}
diff --git a/web/src/test/java/com/graphhopper/http/resources/RouteResourceTest.java b/web/src/test/java/com/graphhopper/http/resources/RouteResourceTest.java
index 3ad37ae96..72e831b3c 100644
--- a/web/src/test/java/com/graphhopper/http/resources/RouteResourceTest.java
+++ b/web/src/test/java/com/graphhopper/http/resources/RouteResourceTest.java
@@ -32,6 +32,7 @@ import com.graphhopper.util.exceptions.PointOutOfBoundsException;
 import com.graphhopper.util.shapes.GHPoint;
 import io.dropwizard.testing.junit.DropwizardAppRule;
 import org.junit.AfterClass;
+import org.junit.BeforeClass;
 import org.junit.ClassRule;
 import org.junit.Test;
 
@@ -62,9 +63,9 @@ public class RouteResourceTest {
     }
 
     @ClassRule
-    public static final DropwizardAppRule<GraphHopperServerConfiguration> app = new DropwizardAppRule(
-            GraphHopperApplication.class, config);
+    public static final DropwizardAppRule<GraphHopperServerConfiguration> app = new DropwizardAppRule<>(GraphHopperApplication.class, config);
 
+    @BeforeClass
     @AfterClass
     public static void cleanUp() {
         Helper.removeDir(new File(DIR));