diff --git a/openrouteservice/pom.xml b/openrouteservice/pom.xml
index c5b76f9cfc..c182699133 100644
--- a/openrouteservice/pom.xml
+++ b/openrouteservice/pom.xml
@@ -243,10 +243,14 @@
org.apache.maven.plugins
maven-surefire-plugin
- 2.22.0
+ 2.22.2
-Duser.language=en -Duser.region=US -Dillegal-access=permit ${surefireArgLine}
+
+ **/*Tests.java
+ **/*Properties.java
+
@@ -296,7 +300,23 @@
true
+
+
+ Sonatype Snapshots
+ https://s01.oss.sonatype.org/content/repositories/snapshots
+
+
+
+
+ org.junit
+ junit-bom
+ 5.8.2
+ pom
+ import
+
+
+
@@ -457,9 +477,23 @@
- junit
- junit
- 4.13.1
+ net.jqwik
+ jqwik
+ 1.6.5
+ test
+
+
+
+ org.assertj
+ assertj-core
+ 3.22.0
+ test
+
+
+
+ org.junit.vintage
+ junit-vintage-engine
+ test
diff --git a/openrouteservice/src/main/java/org/heigit/ors/matrix/algorithms/rphast/RPHASTMatrixAlgorithm.java b/openrouteservice/src/main/java/org/heigit/ors/matrix/algorithms/rphast/RPHASTMatrixAlgorithm.java
index d274296e08..1435e9244e 100644
--- a/openrouteservice/src/main/java/org/heigit/ors/matrix/algorithms/rphast/RPHASTMatrixAlgorithm.java
+++ b/openrouteservice/src/main/java/org/heigit/ors/matrix/algorithms/rphast/RPHASTMatrixAlgorithm.java
@@ -66,7 +66,8 @@ public MatrixResult compute(MatrixLocations srcData, MatrixLocations dstData, in
int[] srcIds = getValidNodeIds(srcData.getNodeIds());
int[] destIds = getValidNodeIds(dstData.getNodeIds());
- mtxResult.setGraphDate(graphHopper.getGraphHopperStorage().getProperties().get("datareader.import.date"));
+ if(graphHopper != null)
+ mtxResult.setGraphDate(graphHopper.getGraphHopperStorage().getProperties().get("datareader.import.date"));
algorithm.prepare(srcIds, destIds);
diff --git a/openrouteservice/src/main/java/org/heigit/ors/routing/algorithms/RPHASTAlgorithm.java b/openrouteservice/src/main/java/org/heigit/ors/routing/algorithms/RPHASTAlgorithm.java
index 1d63cebffb..44b612ede5 100644
--- a/openrouteservice/src/main/java/org/heigit/ors/routing/algorithms/RPHASTAlgorithm.java
+++ b/openrouteservice/src/main/java/org/heigit/ors/routing/algorithms/RPHASTAlgorithm.java
@@ -134,6 +134,7 @@ private boolean upwardSearch() {
return false;
currFrom = prioQueue.poll();
+ upwardEdgeFilter.updateHighestNode(currFrom.getAdjNode());
fillEdgesUpward(currFrom, prioQueue, bestWeightMap, outEdgeExplorer);
visitedCountFrom++;
@@ -217,7 +218,6 @@ private void fillEdgesUpward(MultiTreeSPEntry currEdge, PriorityQueue matrixScenario
+// ) throws Exception {
+//
+// GraphHopperStorage sampleGraph = matrixScenario.get1();
+// FlagEncoder encoder = sampleGraph.getEncodingManager().getEncoder("car");
+// weighting = new ShortestWeighting(encoder);
+// chConfig = sampleGraph.getCHConfig();
+// PrepareContractionHierarchies prepare = createPrepareContractionHierarchies(sampleGraph);
+// prepare.doWork();
+// routingCHGraph = sampleGraph.getRoutingCHGraph("c");
+//
+// MatrixLocations sources = matrixScenario.get2();
+// MatrixLocations destinations = matrixScenario.get3();
+// try {
+// float[] matrixDistances = computeDistancesFromRPHAST(sampleGraph, sources, destinations);
+// float[] coreDistances = computeDistancesFromCH(sources, destinations);
+//
+//// System.out.println(Arrays.toString(matrixDistances));
+//// System.out.println(Arrays.toString(coreDistances));
+//
+// assertDistancesAreEqual(matrixDistances, coreDistances, sources, destinations);
+// } finally {
+// sampleGraph.close();
+// }
+// }
+
+ private void assertDistancesAreEqual(
+ float[] matrixDistances,
+ float[] coreDistances,
+ MatrixLocations sources,
+ MatrixLocations destinations
+ ) {
+ Map edgesByIndex = buildEdgesIndex(sources, destinations);
+ assertEquals("number of distances", coreDistances.length, matrixDistances.length);
+ for (int i = 0; i < coreDistances.length; i++) {
+ String edge = edgesByIndex.get(i);
+ String errorMessage = String.format("Length mismatch for edge %s: ", edge);
+ assertEquals(errorMessage, coreDistances[i], matrixDistances[i], 0.1);
+ }
+ }
+
+ private Map buildEdgesIndex(MatrixLocations sources, MatrixLocations destinations) {
+ Map edgesByIndex = new HashMap<>();
+ int index = 0;
+ for (int sourceId : sources.getNodeIds()) {
+ for (int destinationId : destinations.getNodeIds()) {
+ edgesByIndex.put(index, String.format("%s->%s", sourceId, destinationId));
+ index += 1;
+ }
+ }
+ return edgesByIndex;
+ }
+
+ private float[] computeDistancesFromCH(MatrixLocations sources, MatrixLocations destinations) {
+ float[] coreDistances = new float[sources.size() * destinations.size()];
+ int index = 0;
+ for (int sourceId : sources.getNodeIds()) {
+ for (int destinationId : destinations.getNodeIds()) {
+ RoutingAlgorithm algo = new CHRoutingAlgorithmFactory(routingCHGraph).createAlgo(new PMap());
+ Path path = algo.calcPath(sourceId, destinationId);
+ coreDistances[index] = (float) path.getWeight();
+ // Matrix algorithm returns -1.0 instead of Infinity
+ if (Float.isInfinite(coreDistances[index])) {
+ coreDistances[index] = -1.0f;
+ }
+ index += 1;
+ }
+ }
+ return coreDistances;
+ }
+
+ private float[] computeDistancesFromRPHAST(GraphHopperStorage sampleGraph, MatrixLocations sources, MatrixLocations destinations) throws Exception {
+ RPHASTAlgorithm matrixAlgorithm = createAndPrepareRPHAST(sampleGraph.getRoutingCHGraph());
+ matrixAlgorithm.prepare(sources.getNodeIds(), destinations.getNodeIds());
+ MultiTreeSPEntry[] destTrees = matrixAlgorithm.calcPaths(sources.getNodeIds(), destinations.getNodeIds());
+ return extractValues(sampleGraph, sources, destinations, destTrees);
+ }
+
+ private float[] extractValues(GraphHopperStorage sampleGraph, MatrixLocations sources, MatrixLocations destinations, MultiTreeSPEntry[] destTrees) throws Exception {
+ MultiTreeMetricsExtractor pathMetricsExtractor = new MultiTreeMetricsExtractor(MatrixMetricsType.DISTANCE, sampleGraph.getRoutingCHGraph(), carEncoder, weighting, DistanceUnit.METERS);
+ int tableSize = sources.size() * destinations.size();
+
+ float[] distances = new float[tableSize];
+ float[] times = new float[tableSize];
+ float[] weights = new float[tableSize];
+ MultiTreeSPEntry[] originalDestTrees = new MultiTreeSPEntry[destinations.size()];
+
+ int j = 0;
+ for (int i = 0; i < destinations.size(); i++) {
+ if (destinations.getNodeIds()[i] != -1) {
+ originalDestTrees[i] = destTrees[j];
+ ++j;
+ } else {
+ originalDestTrees[i] = null;
+ }
+ }
+
+ pathMetricsExtractor.calcValues(originalDestTrees, sources, destinations, times, distances, weights);
+ return distances;
+
+ }
+
+ private RPHASTAlgorithm createAndPrepareRPHAST(RoutingCHGraph routingCHGraph) {
+ return new RPHASTAlgorithm(routingCHGraph, weighting, TraversalMode.NODE_BASED);
+ }
+}
\ No newline at end of file
diff --git a/openrouteservice/src/test/java/org/heigit/ors/pbt/GraphGenerator.java b/openrouteservice/src/test/java/org/heigit/ors/pbt/GraphGenerator.java
new file mode 100644
index 0000000000..970acc6fb9
--- /dev/null
+++ b/openrouteservice/src/test/java/org/heigit/ors/pbt/GraphGenerator.java
@@ -0,0 +1,139 @@
+package org.heigit.ors.pbt;
+
+import com.graphhopper.routing.weighting.ShortestWeighting;
+import com.graphhopper.routing.weighting.Weighting;
+import com.graphhopper.storage.CHConfig;
+import com.graphhopper.storage.GraphBuilder;
+import com.graphhopper.storage.GraphHopperStorage;
+import com.graphhopper.util.GHUtility;
+import net.jqwik.api.RandomGenerator;
+import net.jqwik.api.Shrinkable;
+import net.jqwik.api.Tuple;
+import net.jqwik.api.Tuple.Tuple2;
+
+import java.util.*;
+
+import static java.lang.Math.abs;
+import static java.lang.Math.sqrt;
+import static org.heigit.ors.pbt.GraphHopperDomain.carEncoder;
+import static org.heigit.ors.pbt.GraphHopperDomain.encodingManager;
+
+/**
+ * Simple graph generator for up to maxSize nodes and up to (nodes * (nodes-1))/2 edges
+ *
+ *
+ * - The number of nodes is between 2 and maxNodes
+ * - The average number of edges per node is <= AVERAGE_EDGES_PER_NODE
+ * - All edges are bidirectional
+ * - Distances are between 0 and MAX_DISTANCE
+ *
+ */
+class GraphGenerator implements RandomGenerator {
+ private final static int MAX_DISTANCE = 10;
+ private final static int AVERAGE_EDGES_PER_NODE = 2;
+ private final Weighting weighting = new ShortestWeighting(carEncoder);
+ private final CHConfig chConfig = CHConfig.nodeBased("c", weighting);
+
+
+ private final int maxNodes;
+
+ GraphHopperStorage createGHStorage() {
+ return new GraphBuilder(encodingManager).setCHConfigs(chConfig).create();
+ }
+
+ private static Map randomSeeds = new HashMap<>();
+
+ private static void rememberSeed(GraphHopperStorage storage, long randomSeed) {
+ randomSeeds.put(storage, randomSeed);
+ }
+
+ static long getSeed(GraphHopperStorage storage) {
+ return randomSeeds.get(storage);
+ }
+
+ public GraphGenerator(int maxNodes) {
+ this.maxNodes = maxNodes;
+ }
+
+ @Override
+ public Shrinkable next(Random random) {
+ long randomSeed = random.nextLong();
+ // Regenerating a graph on each request is necessary because the underlying
+ // graph storage will be closed after each try.
+ // TODO: this code uses an internal jqwik API Shrinkable.supplyUnshrinkable
+ // This will be unnecessary if graph generation is done using arbitrary combination
+ return Shrinkable.supplyUnshrinkable(() -> {
+ GraphHopperStorage sampleGraph = create(randomSeed);
+ rememberSeed(sampleGraph, randomSeed);
+ return sampleGraph;
+ });
+ }
+
+ // TODO: Make sure graph is fully connected
+ public GraphHopperStorage create(long randomSeed) {
+ GraphHopperStorage storage = createGHStorage();
+ Random random = new Random(randomSeed);
+
+ int nodes = random.nextInt(maxNodes - 1) + 2;
+
+ Set> setOfEdges = new HashSet<>();
+
+ for (int from = 0; from < nodes; from++) {
+ int maxDistance = 2;
+ Set neighbours = findNeighbours(nodes, from, maxDistance);
+ double probability = AVERAGE_EDGES_PER_NODE / Math.max(1.0, neighbours.size());
+ for (int to : neighbours) {
+ if (random.nextDouble() <= probability) {
+ if (!setOfEdges.contains(Tuple.of(to, from))) {
+ setOfEdges.add(Tuple.of(from, to));
+ }
+ }
+ }
+ }
+
+ for (Tuple2 edge : setOfEdges) {
+ double distance = random.nextInt(MAX_DISTANCE + 1);
+ GHUtility.setSpeed(60, true, true, carEncoder, storage.edge(edge.get1(), edge.get2()).setDistance(distance));
+// storage.edge(edge.get1(), edge.get2()).setDistance(distance);
+ }
+ storage.freeze();
+
+ return storage;
+ }
+
+ private Tuple2 rasterCoordinates(int rasterWidth, int node) {
+ int x = node % rasterWidth;
+ int y = node / rasterWidth;
+ Tuple2 coordinates = Tuple.of(x, y);
+ return coordinates;
+ }
+
+ /**
+ * Find neighbours in an approximated square raster
+ */
+ private Set findNeighbours(
+ int numberOfNodes,
+ int node,
+ double maxDistance
+ ) {
+ Set neighbours = new HashSet<>();
+ int rasterWidth = (int) Math.sqrt(numberOfNodes);
+
+ Tuple2 nodeLoc = rasterCoordinates(rasterWidth, node);
+ for (int candidate = 0; candidate < numberOfNodes; candidate++) {
+ if (candidate == node) {
+ continue;
+ }
+ Tuple2 candidateLoc = rasterCoordinates(rasterWidth, candidate);
+ int xDiff = abs(candidateLoc.get1() - nodeLoc.get1());
+ int yDiff = abs(candidateLoc.get2() - nodeLoc.get2());
+ double distance = sqrt(xDiff * xDiff + yDiff * yDiff);
+ if (distance <= maxDistance) {
+ neighbours.add(candidate);
+ }
+ }
+
+ return neighbours;
+ }
+
+}
\ No newline at end of file
diff --git a/openrouteservice/src/test/java/org/heigit/ors/pbt/GraphHopperDomain.java b/openrouteservice/src/test/java/org/heigit/ors/pbt/GraphHopperDomain.java
new file mode 100644
index 0000000000..61b9dc6101
--- /dev/null
+++ b/openrouteservice/src/test/java/org/heigit/ors/pbt/GraphHopperDomain.java
@@ -0,0 +1,136 @@
+package org.heigit.ors.pbt;
+
+import com.graphhopper.routing.util.AllEdgesIterator;
+import com.graphhopper.routing.util.CarFlagEncoder;
+import com.graphhopper.routing.util.EncodingManager;
+import com.graphhopper.routing.weighting.ShortestWeighting;
+import com.graphhopper.routing.weighting.Weighting;
+import com.graphhopper.storage.GraphHopperStorage;
+import net.jqwik.api.*;
+import net.jqwik.api.Tuple.Tuple2;
+import net.jqwik.api.Tuple.Tuple3;
+import net.jqwik.api.domains.DomainContextBase;
+import net.jqwik.api.providers.TypeUsage;
+import org.heigit.ors.matrix.MatrixLocations;
+
+import java.lang.annotation.*;
+import java.util.*;
+
+public class GraphHopperDomain extends DomainContextBase {
+
+ final static CarFlagEncoder carEncoder = new CarFlagEncoder(5, 5.0D, 1);
+ final static EncodingManager encodingManager = EncodingManager.create(carEncoder);
+ final static Weighting SHORTEST_WEIGHTING_FOR_CARS = new ShortestWeighting(carEncoder);
+
+ @Target({ElementType.ANNOTATION_TYPE, ElementType.PARAMETER, ElementType.TYPE_USE})
+ @Retention(RetentionPolicy.RUNTIME)
+ @Documented
+ public @interface MaxNodes {
+ int value();
+ }
+
+ public static final int DEFAULT_MAX_NODES = 500;
+
+ @Provide
+ Arbitrary> matrixScenarios(TypeUsage typeUsage) {
+ Arbitrary graphs = graphs(typeUsage);
+ return graphs.flatMap(graph -> {
+ Set nodes = getAllNodes(graph);
+ Arbitrary sources = Arbitraries.of(nodes).set().ofMinSize(1).map(this::locations);
+ Arbitrary destinations = Arbitraries.of(nodes).set().ofMinSize(1).map(this::locations);
+ return Combinators.combine(sources, destinations).as((s, d) -> Tuple.of(graph, s, d));
+ });
+ }
+
+ @Provide
+ Arbitrary>> routingScenarios(TypeUsage typeUsage) {
+ Arbitrary graphs = graphs(typeUsage);
+ return graphs.flatMap(graph -> {
+ Set nodes = getAllNodes(graph);
+ Arbitrary> pairsOfNodes = Arbitraries.of(nodes).tuple2().filter(t -> !t.get1().equals(t.get2()));
+ return pairsOfNodes.map(pair -> Tuple.of(graph, pair));
+ });
+ }
+
+
+ @Provide
+ Arbitrary graphs(TypeUsage typeUsage) {
+ Optional annotation = typeUsage.findAnnotation(MaxNodes.class);
+ int maxNodes = annotation.map(MaxNodes::value).orElse(DEFAULT_MAX_NODES);
+ return connectedBidirectionalGraph(maxNodes);
+ }
+
+ private Arbitrary connectedBidirectionalGraph(int maxNodes) {
+ return Arbitraries.fromGenerator(new GraphGenerator(maxNodes));
+ }
+
+ private Set getAllNodes(GraphHopperStorage graph) {
+ Set nodes = new HashSet<>();
+ AllEdgesIterator allEdges = graph.getAllEdges();
+ while (allEdges.next()) {
+ nodes.add(allEdges.getBaseNode());
+ nodes.add(allEdges.getAdjNode());
+ }
+ return nodes;
+ }
+
+ private MatrixLocations locations(Collection nodeIds) {
+ List nodes = new ArrayList<>(nodeIds);
+ MatrixLocations locations = new MatrixLocations(nodes.size());
+ for (int i = 0; i < nodes.size(); i++) {
+ locations.setData(i, nodes.get(i), null);
+ }
+ return locations;
+ }
+
+ static class MatrixLocationsFormat implements SampleReportingFormat {
+
+ @Override
+ public boolean appliesTo(Object o) {
+ return o instanceof MatrixLocations;
+ }
+
+ @Override
+ public Object report(Object o) {
+ return ((MatrixLocations) o).getNodeIds();
+ }
+ }
+
+ static class GraphFormat implements SampleReportingFormat {
+
+ @Override
+ public boolean appliesTo(Object o) {
+ return o instanceof GraphHopperStorage;
+ }
+
+ @Override
+ public Optional label(Object value) {
+ return Optional.of("Graph");
+ }
+
+ @Override
+ public Object report(Object o) {
+ GraphHopperStorage graph = (GraphHopperStorage) o;
+ Map attributes = new HashMap<>();
+ attributes.put("seed", GraphGenerator.getSeed(graph));
+ attributes.put("nodes", graph.getNodes());
+ int edgesCount = graph.getEdges();
+ attributes.put("edges count", edgesCount);
+ if (edgesCount < 20) {
+ Map edges = new HashMap<>();
+ AllEdgesIterator edgesIterator = graph.getAllEdges();
+ while (edgesIterator.next()) {
+ String edgeString = String.format(
+ "%s->%s: %s",
+ edgesIterator.getBaseNode(),
+ edgesIterator.getAdjNode(),
+ edgesIterator.getDistance()
+ );
+ edges.put(edgesIterator.getEdge(), edgeString);
+ }
+ attributes.put("edges", edges);
+ }
+ return attributes;
+ }
+ }
+}