/******************************************************************************* * Copyright (c) 2014 Open Door Logistics (www.opendoorlogistics.com) * All rights reserved. This program and the accompanying materials * are made available under the terms of the GNU Lesser Public License v3 * which accompanies this distribution, and is available at http://www.gnu.org/licenses/lgpl.txt ******************************************************************************/ package com.opendoorlogistics.graphhopper; import java.util.ArrayList; import java.util.Arrays; import java.util.HashMap; import java.util.List; import java.util.PriorityQueue; import com.graphhopper.GHRequest; import com.graphhopper.GHResponse; import com.graphhopper.GraphHopper; import com.graphhopper.routing.Path; import com.graphhopper.routing.PathBidirRef; import com.graphhopper.routing.QueryGraph; import com.graphhopper.routing.ch.PreparationWeighting; import com.graphhopper.routing.util.BikeFlagEncoder; import com.graphhopper.routing.util.CarFlagEncoder; import com.graphhopper.routing.util.DefaultEdgeFilter; import com.graphhopper.routing.util.EdgeFilter; import com.graphhopper.routing.util.EncodingManager; import com.graphhopper.routing.util.FlagEncoder; import com.graphhopper.routing.util.FootFlagEncoder; import com.graphhopper.routing.util.LevelEdgeFilter; import com.graphhopper.routing.util.MotorcycleFlagEncoder; import com.graphhopper.routing.util.Weighting; import com.graphhopper.routing.util.WeightingMap; import com.graphhopper.storage.CHGraph; import com.graphhopper.storage.Directory; import com.graphhopper.storage.EdgeEntry; import com.graphhopper.storage.Graph; import com.graphhopper.storage.RAMDirectory; import com.graphhopper.storage.StorableProperties; import com.graphhopper.storage.index.QueryResult; import com.graphhopper.util.EdgeExplorer; import com.graphhopper.util.EdgeIterator; import com.graphhopper.util.PMap; import com.graphhopper.util.shapes.GHPoint; import gnu.trove.map.hash.TIntObjectHashMap; import gnu.trove.procedure.TIntObjectProcedure; public class CHMatrixGeneration { /** * The location of these strings in graphhopper changes from 0.5 to latest code * so we keep a common reference to them here for the rest of the code to use */ public static final String VEHICLE_TYPE_CAR = EncodingManager.CAR; public static final String VEHICLE_TYPE_BIKE = EncodingManager.BIKE; public static final String VEHICLE_TYPE_FOOT = EncodingManager.FOOT; public static final String VEHICLE_TYPE_BIKE2 = EncodingManager.BIKE2; public static final String VEHICLE_TYPE_MOTORCYCLE = EncodingManager.MOTORCYCLE; public static final String VEHICLE_TYPE_RACINGBIKE = EncodingManager.RACINGBIKE; public static final String VEHICLE_TYPE_MOUNTAINBIKE = EncodingManager.MOUNTAINBIKE; protected final GraphHopper hopper; private final EncodingManager encodingManager; protected final CHGraph chGraph; private final LevelEdgeFilter levelEdgeFilter; private final boolean useExpansionCache = true; private final boolean outputText = false; private final boolean ownsHopper; private final FlagEncoder flagEncoder; private final EdgeFilter edgeFilter; private final Weighting prepareWeighting; public static interface CHProcessingApi { boolean isCancelled(); void postStatusMessage(String s); } public static GraphHopper createHopper( String graphFolder) { return createHopper(false, graphFolder); } public static GraphHopper createHopper(boolean memoryMapped, String graphFolder) { GraphHopper ret = null; ret = new GraphHopper().forDesktop(); // initialise the encoders ourselves as we can use multiple // encoders for same vehicle type corresponding to different // times of day (i.e. rush hours) ret.setEncodingManager(createEncodingManager(graphFolder)); // don't need to write so disable the lock file (allows us to run out of program files) ret.setAllowWrites(false); if (memoryMapped) { ret.setMemoryMapped(); } ret.setGraphHopperLocation(graphFolder); ret.importOrLoad(); return ret; } /** * Encoding manager which understands the convention that car_day, * car_night and car should all use the car encoder, but returning * the relevant id in toString(). * Method is public as its called from unit tests * @param directory * @return */ public static EncodingManager createEncodingManager(String directory) { Directory dir = new RAMDirectory(directory, true); StorableProperties properties = new StorableProperties(dir); if (!properties.loadExisting()) { properties.close(); throw new RuntimeException("Cannot find properties file"); } // check encoding for compatiblity properties.checkVersions(false); // get all the flag encoders, with correct ids in their toString method String allEncoderString= properties.get("graph.flagEncoders"); List<FlagEncoder> encoders = new ArrayList<>(); for(String encoderString: allEncoderString.split(",")){ encoderString = encoderString.trim().toLowerCase(); String typeWithVariant = encoderString.split("\\|")[0]; String [] splitTypeVariant = typeWithVariant.split("_"); String type = splitTypeVariant[0].trim(); String variant = splitTypeVariant.length>1? splitTypeVariant[1].trim():""; PMap propertiesMap = new PMap(encoderString); class ToStringHelper{ String id(String baseId){ return variant.length()>0?baseId+"_"+variant:baseId; } } ToStringHelper h= new ToStringHelper(); FlagEncoder encoder = null; if (type.equals(EncodingManager.CAR)){ encoder= new CarFlagEncoder(propertiesMap){ @Override public String toString(){ return h.id(super.toString()); } }; } else if (type.equals(EncodingManager.BIKE)){ encoder= new BikeFlagEncoder(propertiesMap){ @Override public String toString(){ return h.id(super.toString()); } }; } else if (type.equals(EncodingManager.FOOT)){ encoder= new FootFlagEncoder(propertiesMap){ @Override public String toString(){ return h.id(super.toString()); } }; } else if (type.equals(EncodingManager.MOTORCYCLE)){ encoder= new MotorcycleFlagEncoder(propertiesMap){ @Override public String toString(){ return h.id(super.toString()); } }; } else{ properties.close(); throw new RuntimeException("Unsupported vehicle type"); } if (propertiesMap.has("version") && encoder.getVersion()!=propertiesMap.getInt("version", -1)){ properties.close(); throw new RuntimeException("Graph was built with wrong encoder version - probably built using an older version of the graphhopper library?"); } encoders.add(encoder); } int bytesForFlags = 4; if ("8".equals(properties.get("graph.bytesForFlags"))) { bytesForFlags = 8; } // closing properties is probably unneccessary properties.close(); return new EncodingManager(encoders, bytesForFlags); } /** * Load the graph and use it in this class * @param graphFolder */ public CHMatrixGeneration(String graphFolder) { this(graphFolder, false); } /** * Load the graph and use it in this class * @param graphFolder * @param memoryMapped */ public CHMatrixGeneration(String graphFolder, boolean memoryMapped) { this( createHopper(memoryMapped, graphFolder), true, null); } public CHMatrixGeneration(String graphFolder, boolean memoryMapped, String vehicleType) { this( createHopper(memoryMapped, graphFolder), true, vehicleType); } /** * Wrap the instance of hopper but don't own it (i.e. don't dispose of it later) * @param hopper * @param namedFlagEncoder */ public CHMatrixGeneration(GraphHopper hopper, String namedFlagEncoder) { this(hopper,false,namedFlagEncoder); } /** * Get the possible vehicle types. Just because a vehicle type is possible, it doesn't mean its supported * in the input graph * @return */ public static String [] getPossibleVehicleTypes(){ return new String[] { VEHICLE_TYPE_CAR, VEHICLE_TYPE_BIKE, VEHICLE_TYPE_FOOT, VEHICLE_TYPE_BIKE2, VEHICLE_TYPE_MOTORCYCLE, VEHICLE_TYPE_RACINGBIKE, VEHICLE_TYPE_MOUNTAINBIKE }; } /** * * @param graphFolder * @param memoryMapped * @param hopper * @param ownsHopper * Whether this class owns the graphhopper graph (and wrapper object) and should dispose of it later. * @param namedFlagEncoder */ private CHMatrixGeneration( GraphHopper hopper, boolean ownsHopper, String namedFlagEncoder) { this.hopper = hopper; this.ownsHopper = ownsHopper; encodingManager = hopper.getEncodingManager(); if (namedFlagEncoder == null) { // Pick the first supported encoder from a standard list, ordered by most commonly used first. // This allows the user to build the graph for the speed profile they want and it just works... FlagEncoder foundFlagEncoder = null; for (String vehicleType : getPossibleVehicleTypes()) { if (encodingManager.supports(vehicleType)) { foundFlagEncoder = encodingManager.getEncoder(vehicleType); break; } } if (foundFlagEncoder == null) { throw new RuntimeException("The road network graph does not support any of the standard vehicle types"); } flagEncoder = foundFlagEncoder; } else { namedFlagEncoder = namedFlagEncoder.toLowerCase().trim(); flagEncoder = encodingManager.getEncoder(namedFlagEncoder); if (flagEncoder == null) { throw new RuntimeException("Vehicle type is unsuported in road network graph: " + namedFlagEncoder); } } edgeFilter = new DefaultEdgeFilter(flagEncoder); WeightingMap weightingMap = new WeightingMap("fastest"); // Weighting weighting = hopper.createWeighting(weightingMap, flagEncoder); // prepareWeighting = new PreparationWeighting(weighting); // get correct weighting for flag encoder Weighting weighting = hopper.getWeightingForCH(weightingMap, flagEncoder); prepareWeighting = new PreparationWeighting(weighting); // save reference to the correct CH graph chGraph = hopper.getGraphHopperStorage().getGraph(CHGraph.class,weighting); // and create a level edge filter to ensure we (a) accept virtual (snap-to) edges and (b) don't descend into the // base graph levelEdgeFilter = new LevelEdgeFilter(chGraph); } public void dispose() { if (ownsHopper) { hopper.close(); } } public GHResponse getResponse(GHPoint from, GHPoint to) { // The flag encoder's toString method returns the vehicle type GHRequest req = new GHRequest(from, to).setVehicle(flagEncoder.toString()); GHResponse rsp = hopper.route(req); if (rsp.hasErrors()) { return null; } return rsp; } public MatrixResult calculateMatrixOneByOne(GHPoint[] points) { int n = points.length; MatrixResult ret = new MatrixResult(n); for (int fromIndex = 0; fromIndex < n; fromIndex++) { // Loop over TO in reverse order so the first A-B we process doesn't have the same // location for FROM and TO - this makes it quicker to debug as the first call is no longer a 'dummy' one. for (int toIndex = n - 1; toIndex >= 0; toIndex--) { GHPoint from = points[fromIndex]; GHPoint to = points[toIndex]; GHResponse rsp = getResponse(from, to); if (rsp == null) { continue; } ret.setDistanceMetres(fromIndex, toIndex, rsp.getDistance()); ret.setTimeMilliseconds(fromIndex, toIndex, rsp.getTime()); } } return ret; } public CHGraph getCHGraph() { return chGraph; } public EdgeExplorer createBackwardsEdgeExplorer(Graph graph) { return graph.createEdgeExplorer(new DefaultEdgeFilter(flagEncoder, true, false)); } private QueryResult[] queryPositions(GHPoint[] points, List<QueryResult> validResults) { QueryResult[] queryResults = new QueryResult[points.length]; for (int i = 0; i < points.length; i++) { queryResults[i] = createSnapToResult(points[i].getLat(), points[i].getLon()); if (queryResults[i].isValid()) { validResults.add(queryResults[i]); } } return queryResults; } public QueryResult createSnapToResult(double latitude, double longitude) { return hopper.getLocationIndex().findClosest(latitude, longitude, edgeFilter); } public EdgeExplorer createForwardsEdgeExplorer(Graph graph) { return graph.createEdgeExplorer(new DefaultEdgeFilter(flagEncoder, false, true)); } /** * A map of node id to EdgeEntries generated for a single shortest path query * * @author Phil * */ public static class ShortestPathTree extends TIntObjectHashMap<EdgeEntry> { public final int startNodeId; public final boolean reverseQuery; ShortestPathTree(int nodeId, boolean reverseQuery) { this.startNodeId = nodeId; this.reverseQuery = reverseQuery; } } public ShortestPathTree search(int startNode, EdgeExplorer edgeExplorer, boolean isBackwards) { return search(startNode, edgeExplorer, levelEdgeFilter, isBackwards); } public ShortestPathTree search(int startNode, EdgeExplorer edgeExplorer, EdgeFilter edgeFilter, boolean isBackwards) { PriorityQueue<EdgeEntry> openSet = new PriorityQueue<>(); ShortestPathTree shortestWeightMap = new ShortestPathTree(startNode, isBackwards); EdgeEntry firstEdge = new EdgeEntry(EdgeIterator.NO_EDGE, startNode, 0); shortestWeightMap.put(startNode, firstEdge); openSet.add(firstEdge); // int nodeCount = 0; while (openSet.size() > 0) { // The node at the adjacent edge is now settled. EdgeEntry currEdge = openSet.poll(); int currNode = currEdge.adjNode; EdgeIterator iter = edgeExplorer.setBaseNode(currNode); // nodeCount++; // System.out.println("" + nodeCount + ": " + currEdge.edge + // " weight=" + currEdge.weight + " opencount=" + openSet.size()); while (iter.next()) { int adjNode = iter.getAdjNode(); // Filter out the base (no CH) graph if (!edgeFilter.accept(iter)) { continue; } // As turn restrictions aren't enabled at the moment we should be safe putting // a non-existent edge for the previous or next edge, though we should fix this in the future... int previousOrNextEdge = -1; double tmpWeight = prepareWeighting.calcWeight(iter, isBackwards, previousOrNextEdge) + currEdge.weight; EdgeEntry de = shortestWeightMap.get(adjNode); if (de == null) { de = new EdgeEntry(iter.getEdge(), adjNode, tmpWeight); de.parent = currEdge; shortestWeightMap.put(adjNode, de); openSet.add(de); } else if (de.weight > tmpWeight) { // Update the weight (i.e. travel cost) on the node. // This should never be called for a settled node as the // existing weight will be lower than the tmpWeight openSet.remove(de); de.edge = iter.getEdge(); de.weight = tmpWeight; de.parent = currEdge; openSet.add(de); } } } return shortestWeightMap; } public GraphHopper getGraphhopper() { return hopper; } public FlagEncoder getFlagEncoder() { return flagEncoder; } public MatrixResult calculateMatrix(GHPoint[] points, CHProcessingApi processingApi) { if (outputText) { System.out.println("Starting calculate matrix"); } // query positions List<QueryResult> validResults = new ArrayList<QueryResult>(points.length); QueryResult[] snapToResults = queryPositions(points, validResults); if (processingApi != null && processingApi.isCancelled()) { return null; } // Create a query graph from the snap-to results, this is a graph including virtual // edges based on the snapped-to locations. The closest node in the QueryResults will // be changed to a virtual node in the QueryGraph. if (outputText) { System.out.println("Creating query graph"); } if (processingApi != null) { processingApi.postStatusMessage("Querying positions against graph"); } final QueryGraph queryGraph = new QueryGraph(chGraph); queryGraph.lookup(validResults); if (processingApi != null && processingApi.isCancelled()) { return null; } // run the search forward individually from each point final ShortestPathTree[] forwardTrees = new ShortestPathTree[points.length]; final TIntObjectHashMap<List<FromIndexEdge>> visitedByNodeId = new TIntObjectHashMap<>(); if (processingApi != null) { processingApi.postStatusMessage("Performing forward search"); } searchAllForward(snapToResults, queryGraph, forwardTrees, visitedByNodeId, processingApi); if (processingApi != null && processingApi.isCancelled()) { return null; } // run the search backward for all MatrixResult ret = searchAllBackward(snapToResults, queryGraph, forwardTrees, visitedByNodeId, processingApi); if (processingApi != null && processingApi.isCancelled()) { return null; } if (outputText) { System.out.println("Finished calculate matrix"); } return ret; } private MatrixResult searchAllBackward(QueryResult[] snapToResults, final QueryGraph snapToGraph, final ShortestPathTree[] forwardTrees, final TIntObjectHashMap<List<FromIndexEdge>> visitedByNodeId, CHProcessingApi processingApi) { if (outputText) { System.out.println("Running backward searches and extracting matrix results"); } // instantiate return object final int n = snapToResults.length; MatrixResult ret = new MatrixResult(n); // create a cache of expanded edge results final HashMap<EdgeExpansionCacheKey, DistanceTime> expansionCache; if (useExpansionCache) { expansionCache = new HashMap<>(); } else { expansionCache = null; } // now query all in a reverse direction, building up the final matrix // for each one EdgeExplorer inEdgeExplorer = createBackwardsEdgeExplorer(snapToGraph); // UpdateTimer timer = new UpdateTimer(100); long lastUpdateTime = System.currentTimeMillis(); for (int toIndex = 0; toIndex < n; toIndex++) { // check for user quitting if (processingApi != null && processingApi.isCancelled()) { return null; } if (snapToResults[toIndex].isValid()) { // run query ShortestPathTree reverseTree = search(snapToResults[toIndex].getClosestNode(), inEdgeExplorer, true); // This reverse tree is used to find all results going TO the current point. // Parse all nodes of the reverse tree finding the minimum cost meeting node for each from final double[] minCost = new double[n]; Arrays.fill(minCost, Double.POSITIVE_INFINITY); final int[] minCostNode = new int[n]; Arrays.fill(minCostNode, -1); reverseTree.forEachEntry(new TIntObjectProcedure<EdgeEntry>() { @Override public boolean execute(int meetingPointNode, EdgeEntry reverseEdge) { // Use list of all FROM trees which encountered this node List<FromIndexEdge> list = visitedByNodeId.get(meetingPointNode); if (list == null) { return true; } int size = list.size(); for (int i = 0; i < size; i++) { FromIndexEdge fie = list.get(i); int fromIndex = fie.fromIndex; EdgeEntry forwardEdge = fie.edge; // see if this meeting point has a lower cost than the other double cost = forwardEdge.weight + reverseEdge.weight; if (cost < minCost[fromIndex]) { minCost[fromIndex] = cost; minCostNode[fromIndex] = meetingPointNode; } } return true; } }); // extract the path for each one so we can get the distance and time for (int fromIndex = 0; fromIndex < n; fromIndex++) { int meetingPointNode = minCostNode[fromIndex]; if (meetingPointNode != -1) { // use a cache of expanded CH edges for performance reasons PathBidirRef pathCh = new CacheablePath4CH(snapToGraph, getFlagEncoder(), expansionCache); // PathBidirRef pathCh = new Path4CH(snapToGraph, snapToGraph.getBaseGraph(),getFlagEncoder()); pathCh.setSwitchToFrom(false); EdgeEntry edgeEntry = forwardTrees[fromIndex].get(meetingPointNode); pathCh.setEdgeEntry(edgeEntry); EdgeEntry edgeEntryTo = reverseTree.get(meetingPointNode); pathCh.setEdgeEntryTo(edgeEntryTo); Path path = pathCh.extract(); ret.setTimeMilliseconds(fromIndex, toIndex, path.getTime()); ret.setDistanceMetres(fromIndex, toIndex, path.getDistance()); } } } if (System.currentTimeMillis() - lastUpdateTime > 100 && processingApi != null) { lastUpdateTime = System.currentTimeMillis(); processingApi.postStatusMessage("Performed backwards search for " + (toIndex + 1) + "/" + n + " points"); } } return ret; } private void searchAllForward(QueryResult[] snapToResults, final QueryGraph queryGraph, final ShortestPathTree[] forwardTrees, final TIntObjectHashMap<List<FromIndexEdge>> visitedByNodeId, CHProcessingApi continueCB) { if (outputText) { System.out.println("Running forward searches"); } EdgeExplorer outEdgeExplorer = createForwardsEdgeExplorer(queryGraph); for (int fromIndex = 0; fromIndex < snapToResults.length; fromIndex++) { // check for user quitting if (continueCB != null && continueCB.isCancelled()) { return; } final int finalFromIndx = fromIndex; if (snapToResults[fromIndex].isValid()) { forwardTrees[fromIndex] = search(snapToResults[fromIndex].getClosestNode(), outEdgeExplorer, false); forwardTrees[fromIndex].forEachEntry(new TIntObjectProcedure<EdgeEntry>() { @Override public boolean execute(int nodeId, EdgeEntry edge) { List<FromIndexEdge> visited = visitedByNodeId.get(nodeId); if (visited == null) { visited = new ArrayList<>(1); visitedByNodeId.put(nodeId, visited); } visited.add(new FromIndexEdge(finalFromIndx, edge)); return true; } }); } } } private static class FromIndexEdge { private final int fromIndex; private final EdgeEntry edge; FromIndexEdge(int fromIndex, EdgeEntry edge) { super(); this.fromIndex = fromIndex; this.edge = edge; } } }