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; + } + } +}