/*
* 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;
import com.carrotsearch.hppc.IntSet;
import com.carrotsearch.hppc.predicates.IntObjectPredicate;
import com.graphhopper.coll.GHIntHashSet;
import com.graphhopper.coll.GHIntObjectHashMap;
import com.graphhopper.routing.util.TraversalMode;
import com.graphhopper.routing.weighting.WeightApproximator;
import com.graphhopper.routing.weighting.Weighting;
import com.graphhopper.storage.Graph;
import com.graphhopper.util.EdgeIterator;
import com.graphhopper.util.EdgeIteratorState;
import com.graphhopper.util.GHUtility;
import com.graphhopper.util.Parameters;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.List;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicInteger;
/**
* This class implements the alternative paths search using the "plateau" and partially the
* "penalty" method described in the following papers.
*
*
* - Choice Routing Explanation - Camvit 2009:
* http://www.camvit.com/camvit-technical-english/Camvit-Choice-Routing-Explanation-english.pdf
* - and refined in: Alternative Routes in Road Networks 2010:
* http://www.cs.princeton.edu/~rwerneck/papers/ADGW10-alternatives-sea.pdf
* - other ideas 'Improved Alternative Route Planning', 2013:
* https://hal.inria.fr/hal-00871739/document
* - via point 'storage' idea 'Candidate Sets for Alternative Routes in Road Networks', 2013:
* https://algo2.iti.kit.edu/download/s-csarrn-12.pdf
* - Alternative route graph construction 2011:
* http://algo2.iti.kit.edu/download/altgraph_tapas_extended.pdf
*
*
*
* @author Peter Karich
*/
public class AlternativeRoute implements RoutingAlgorithm {
private final Logger logger = LoggerFactory.getLogger(getClass());
private static final Comparator ALT_COMPARATOR = new Comparator() {
@Override
public int compare(AlternativeInfo o1, AlternativeInfo o2) {
return Double.compare(o1.sortBy, o2.sortBy);
}
};
private final Graph graph;
private final Weighting weighting;
private final TraversalMode traversalMode;
private int visitedNodes;
private int maxVisitedNodes = Integer.MAX_VALUE;
private double maxWeightFactor = 1.4;
// the higher the maxWeightFactor the higher the explorationFactor needs to be
// 1 is default for bidir Dijkstra, 0.8 seems to be a very similar value for bidir A* but roughly 1/2 of the nodes explored
private double maxExplorationFactor = 0.8;
private double maxShareFactor = 0.6;
private double minPlateauFactor = 0.2;
private int maxPaths = 2;
private WeightApproximator weightApproximator;
public AlternativeRoute(Graph graph, Weighting weighting, TraversalMode traversalMode) {
if (weighting.hasTurnCosts() && !traversalMode.isEdgeBased())
throw new IllegalStateException("Weightings supporting turn costs cannot be used with node-based traversal mode");
this.graph = graph;
this.weighting = weighting;
this.traversalMode = traversalMode;
logger.info("graph info: ",this.graph.toString());
logger.info("weighting info: ",this.weighting.toString());
logger.info("traversalmode info: ",this.traversalMode.toString());
}
/**
* This method sets the approximation used for the internal bidirectional A*.
*/
public void setApproximation(WeightApproximator weightApproximator) {
this.weightApproximator = weightApproximator;
}
static List getAltNames(Graph graph, SPTEntry ee) {
if (ee == null || !EdgeIterator.Edge.isValid(ee.edge))
return Collections.emptyList();
EdgeIteratorState iter = graph.getEdgeIteratorState(ee.edge, Integer.MIN_VALUE);
if (iter == null)
return Collections.emptyList();
String str = iter.getName();
if (str.isEmpty())
return Collections.emptyList();
return Collections.singletonList(str);
}
static double calcSortBy(double weightInfluence, double weight,
double shareInfluence, double shareWeight,
double plateauInfluence, double plateauWeight) {
return 1;
//return weightInfluence * weight + shareInfluence * shareWeight + plateauInfluence * plateauWeight;
}
@Override
public void setMaxVisitedNodes(int numberOfNodes) {
this.maxVisitedNodes = numberOfNodes;
}
/**
* Increasing this factor results in returning more alternatives. E.g. if the factor is 2 than
* all alternatives with a weight 2 times longer than the optimal weight are return. (default is
* 1)
*/
public void setMaxWeightFactor(double maxWeightFactor) {
this.maxWeightFactor = maxWeightFactor;
}
/**
* This parameter is used to avoid alternatives too similar to the best path. Specify 0.5 to
* force a same paths of maximum 50%. The unit is the 'weight' returned in the Weighting.
*/
public void setMaxShareFactor(double maxShareFactor) {
this.maxShareFactor = maxShareFactor;
}
/**
* This method sets the minimum plateau portion of every alternative path that is required.
*/
public void setMinPlateauFactor(double minPlateauFactor) {
this.minPlateauFactor = minPlateauFactor;
}
/**
* This method sets the graph exploration percentage for alternative paths. Default for bidirectional A*
* is 0.8 (80%). Specify a higher value to get more alternatives (especially if maxWeightFactor is higher than
* 1.5) and a lower value to improve query time but reduces the possibility to find alternatives.
*/
public void setMaxExplorationFactor(double explorationFactor) {
this.maxExplorationFactor = explorationFactor;
}
/**
* Specifies how many paths (including the optimal) are returned. (default is 2)
*/
public void setMaxPaths(int maxPaths) {
this.maxPaths = maxPaths;
if (this.maxPaths < 2)
throw new IllegalArgumentException("Use normal algorithm with less overhead instead if no alternatives are required");
}
/**
* This method calculates best paths (alternatives) between 'from' and 'to', where maxPaths-1
* alternatives are searched and they are only accepted if they are not too similar but close to
* the best path.
*/
public List calcAlternatives(int from, int to) {
AlternativeBidirSearch altBidirDijktra = new AlternativeBidirSearch(
graph, weighting, traversalMode, maxExplorationFactor * 2);
altBidirDijktra.setMaxVisitedNodes(maxVisitedNodes);
if (weightApproximator != null) {
altBidirDijktra.setApproximation(weightApproximator);
}
Path bestPath = altBidirDijktra.searchBest(from, to);
visitedNodes = altBidirDijktra.getVisitedNodes();
return altBidirDijktra.
calcAlternatives(bestPath, maxPaths, maxWeightFactor, 7, maxShareFactor, 0.8, minPlateauFactor, -0.2);
}
@Override
public Path calcPath(int from, int to) {
return calcPaths(from, to).get(0);
}
@Override
public List calcPaths(int from, int to) {
List alts = calcAlternatives(from, to);
logger.info("alternative paths",alts.toString());
List paths = new ArrayList<>(alts.size());
logger.info("paths info",paths.toString());
for (AlternativeInfo a : alts) {
paths.add(a.getPath());
}
return paths;
}
@Override
public String getName() {
return Parameters.Algorithms.ALT_ROUTE;
}
@Override
public int getVisitedNodes() {
return visitedNodes;
}
public static class AlternativeInfo {
private final double sortBy;
private final Path path;
private final SPTEntry shareStart;
private final SPTEntry shareEnd;
private final double shareWeight;
private final List names;
public AlternativeInfo(double sortBy, Path path, SPTEntry shareStart, SPTEntry shareEnd,
double shareWeight, List altNames) {
this.names = altNames;
this.sortBy = sortBy;
this.path = path;
this.path.setDescription(names);
this.shareStart = shareStart;
this.shareEnd = shareEnd;
this.shareWeight = shareWeight;
}
public Path getPath() {
return path;
}
public SPTEntry getShareStart() {
return shareStart;
}
public SPTEntry getShareEnd() {
return shareEnd;
}
public double getShareWeight() {
return shareWeight;
}
public double getSortBy() {
return sortBy;
}
@Override
public String toString() {
return names + ", sortBy:" + sortBy + ", shareWeight:" + shareWeight + ", " + path;
}
}
/**
* Helper class to find alternatives and alternatives for round trip.
*/
public static class AlternativeBidirSearch extends AStarBidirection {
private final double explorationFactor;
private final Logger logger = LoggerFactory.getLogger(getClass());
public AlternativeBidirSearch(Graph graph, Weighting weighting, TraversalMode tMode,
double explorationFactor) {
super(graph, weighting, tMode);
this.explorationFactor = explorationFactor;
}
@Override
public boolean finished() {
// we need to finish BOTH searches identical to CH
if (finishedFrom && finishedTo)
return true;
if (isMaxVisitedNodesExceeded())
return true;
// The following condition is necessary to avoid traversing the full graph if areas are disconnected
// but it is only valid for non-CH e.g. for CH it can happen that finishedTo is true but the from-SPT could still reach 'to'
if (finishedFrom || finishedTo)
return true;
// increase overlap of both searches:
return currFrom.weight + currTo.weight > explorationFactor * (bestWeight + stoppingCriterionOffset);
// This is more precise but takes roughly 20% longer: return currFrom.weight > bestWeight && currTo.weight > bestWeight;
// For bidir A* and AStarEdge.getWeightOfVisitedPath see comment in AStarBidirection.finished
}
public Path searchBest(int from, int to) {
init(from, 0, to, 0);
// init collections and bestPath.getWeight properly
runAlgo();
return extractPath();
}
/**
* @return the information necessary to handle alternative paths. Note that the paths are
* not yet extracted.
*/
public List calcAlternatives(final Path bestPath, final int maxPaths,
double maxWeightFactor, final double weightInfluence,
final double maxShareFactor, final double shareInfluence,
final double minPlateauFactor, final double plateauInfluence) {
final double maxWeight = maxWeightFactor * bestWeight;
final GHIntObjectHashMap traversalIdMap = new GHIntObjectHashMap<>();
final AtomicInteger startTID = addToMap(traversalIdMap, bestPath);
// find all 'good' alternatives from forward-SPT matching the backward-SPT and optimize by
// small total weight (1), small share and big plateau (3a+b) and do these expensive calculations
// only for plateau start candidates (2)
logger.info("max path info",maxPaths);
final List alternatives = new ArrayList<>(maxPaths);
logger.info("alternatives info",alternatives);
double bestPlateau = bestWeight;
double bestShare = 0;
double sortBy = calcSortBy(weightInfluence, bestWeight,
shareInfluence, bestShare,
plateauInfluence, bestPlateau);
final AlternativeInfo bestAlt = new AlternativeInfo(sortBy, bestPath,
bestFwdEntry, bestBwdEntry, bestShare, getAltNames(graph, bestFwdEntry));
alternatives.add(bestAlt);
final List bestPathEntries = new ArrayList<>(2);
bestWeightMapFrom.forEach(new IntObjectPredicate() {
@Override
public boolean apply(final int traversalId, final SPTEntry fromSPTEntry) {
SPTEntry toSPTEntry = bestWeightMapTo.get(traversalId);
if (toSPTEntry == null)
return true;
if (traversalMode.isEdgeBased()) {
if (toSPTEntry.parent != null)
// move to parent for two reasons:
// 1. make only turn costs missing in 'weight' and not duplicating current edge.weight
// 2. to avoid duplicate edge in Path
toSPTEntry = toSPTEntry.parent;
// TODO else if fromSPTEntry.parent != null fromSPTEntry = fromSPTEntry.parent;
} else // The alternative path is suboptimal when both entries are parallel
if (fromSPTEntry.edge == toSPTEntry.edge)
return true;
// (1) skip too long paths
final double weight = fromSPTEntry.getWeightOfVisitedPath() + toSPTEntry.getWeightOfVisitedPath();
// if (weight > maxWeight)
// return true;
// (2) Use the start traversal ID of a plateau as ID for the alternative path.
// Accept from-EdgeEntries only if such a start of a plateau
// i.e. discard if its parent has the same edgeId as the next to-SPTEntry.
// Ignore already added best path
if (isBestPath(fromSPTEntry))
return true;
// For edge based traversal we need the next entry to find out the plateau start
SPTEntry tmpFromEntry = traversalMode.isEdgeBased() ? fromSPTEntry.parent : fromSPTEntry;
if (tmpFromEntry == null || tmpFromEntry.parent == null) {
// we can be here only if edge based and only if entry is not part of the best path
// e.g. when starting point has two edges and one is part of the best path the other edge is path of an alternative
assert traversalMode.isEdgeBased();
} else {
int nextToTraversalId = traversalMode.createTraversalId(tmpFromEntry.adjNode,
tmpFromEntry.parent.adjNode, tmpFromEntry.edge, true);
SPTEntry tmpNextToSPTEntry = bestWeightMapTo.get(nextToTraversalId);
if (tmpNextToSPTEntry == null)
return true;
if (traversalMode.isEdgeBased())
tmpNextToSPTEntry = tmpNextToSPTEntry.parent;
// skip if on plateau
if (fromSPTEntry.edge == tmpNextToSPTEntry.edge)
return true;
}
// (3a) calculate plateau, we know we are at the beginning of the 'from'-side of
// the plateau A-B-C and go further to B
// where B is the next-'from' of A and B is also the previous-'to' of A.
//
// *<-A-B-C->*
// / \
// start end
//
// extend plateau in only one direction necessary (A to B to ...) as we know
// that the from-SPTEntry is the start of the plateau or there is no plateau at all
//
double plateauWeight = 0;
SPTEntry prevToSPTEntry = toSPTEntry;
// List plateauEdges = new ArrayList();
while (prevToSPTEntry.parent != null) {
int nextFromTraversalId = traversalMode.createTraversalId(prevToSPTEntry.adjNode, prevToSPTEntry.parent.adjNode,
prevToSPTEntry.edge, false);
SPTEntry nextFromSPTEntry = bestWeightMapFrom.get(nextFromTraversalId);
// end of a plateau
if (nextFromSPTEntry == null)
break;
// is the next from-SPTEntry on the plateau?
if (prevToSPTEntry.edge != nextFromSPTEntry.edge)
break;
// plateauEdges.add(prevToSPTEntry.edge);
plateauWeight += (prevToSPTEntry.getWeightOfVisitedPath() - prevToSPTEntry.parent.getWeightOfVisitedPath());
prevToSPTEntry = prevToSPTEntry.parent;
}
// if (plateauWeight <= 0 || plateauWeight / weight < minPlateauFactor)
// return true;
if (fromSPTEntry.parent == null)
throw new IllegalStateException("not implemented yet. in case of an edge based traversal the parent of fromSPTEntry could be null");
// (3b) calculate share
SPTEntry fromEE = getFirstShareEE(fromSPTEntry.parent, true);
SPTEntry toEE = getFirstShareEE(toSPTEntry.parent, false);
double shareWeight = fromEE.getWeightOfVisitedPath() + toEE.getWeightOfVisitedPath();
boolean smallShare = shareWeight / bestWeight < maxShareFactor;
if (true) {
List altNames = getAltNames(graph, fromSPTEntry);
double sortBy = calcSortBy(weightInfluence, weight, shareInfluence, shareWeight, plateauInfluence, plateauWeight);
double worstSortBy = getWorstSortBy();
// plateaus.add(new PlateauInfo(altName, plateauEdges));
if (sortBy < worstSortBy || alternatives.size() < maxPaths) {
Path path = BidirPathExtractor.extractPath(graph, weighting, fromSPTEntry, toSPTEntry, weight);
// for now do not add alternatives to set, if we do we need to remove then on alternatives.clear too (see below)
// AtomicInteger tid = addToMap(traversalIDMap, path);
// int tid = traversalMode.createTraversalId(path.calcEdges().get(0), false);
alternatives.add(new AlternativeInfo(sortBy, path, fromEE, toEE, shareWeight, altNames));
Collections.sort(alternatives, ALT_COMPARATOR);
if (alternatives.get(0) != bestAlt)
throw new IllegalStateException("best path should be always first entry");
// if (alternatives.size() > maxPaths)
// alternatives.subList(maxPaths, alternatives.size()).clear();
}
}
return true;
}
/**
* Extract path until we stumble over an existing traversal id
*/
SPTEntry getFirstShareEE(SPTEntry startEE, boolean reverse) {
while (startEE.parent != null) {
// TODO we could make use of traversal ID directly if stored in SPTEntry
int tid = traversalMode.createTraversalId(startEE.adjNode, startEE.parent.adjNode, startEE.edge, reverse);
if (isAlreadyExisting(tid))
return startEE;
startEE = startEE.parent;
}
return startEE;
}
/**
* This method returns true if the specified tid is already existent in the
* traversalIDMap
*/
boolean isAlreadyExisting(final int tid) {
final AtomicBoolean exists = new AtomicBoolean(false);
traversalIdMap.forEach(new IntObjectPredicate() {
@Override
public boolean apply(int key, IntSet set) {
if (set.contains(tid)) {
exists.set(true);
return false;
}
return true;
}
});
return exists.get();
}
/**
* Return the current worst weight for all alternatives
*/
double getWorstSortBy() {
if (alternatives.isEmpty())
throw new IllegalStateException("Empty alternative list cannot happen");
return alternatives.get(alternatives.size() - 1).sortBy;
}
// returns true if fromSPTEntry is identical to the specified best path
boolean isBestPath(SPTEntry fromSPTEntry) {
if (traversalMode.isEdgeBased()) {
if (GHUtility.getEdgeFromEdgeKey(startTID.get()) == fromSPTEntry.edge) {
if (fromSPTEntry.parent == null)
throw new IllegalStateException("best path must have no parent but was non-null: " + fromSPTEntry);
return true;
}
} else if (fromSPTEntry.parent == null) {
bestPathEntries.add(fromSPTEntry);
if (bestPathEntries.size() > 1)
throw new IllegalStateException("There is only one best path but was: " + bestPathEntries);
if (startTID.get() != fromSPTEntry.adjNode)
throw new IllegalStateException("Start traversal ID has to be identical to root edge entry "
+ "which is the plateau start of the best path but was: " + startTID + " vs. adjNode: " + fromSPTEntry.adjNode);
return true;
}
return false;
}
});
return alternatives;
}
/**
* This method adds the traversal IDs of the specified path as set to the specified map.
*/
AtomicInteger addToMap(GHIntObjectHashMap map, Path path) {
IntSet set = new GHIntHashSet();
final AtomicInteger startTID = new AtomicInteger(-1);
for (EdgeIteratorState iterState : path.calcEdges()) {
int tid = traversalMode.createTraversalId(iterState, false);
set.add(tid);
if (startTID.get() < 0) {
// for node based traversal we need to explicitely add base node as starting node and to list
if (!traversalMode.isEdgeBased()) {
tid = iterState.getBaseNode();
set.add(tid);
}
startTID.set(tid);
}
}
map.put(startTID.get(), set);
return startTID;
}
}
public static class PlateauInfo {
String name;
List edges;
public PlateauInfo(String name, List edges) {
this.name = name;
this.edges = edges;
}
@Override
public String toString() {
return name;
}
public List getEdges() {
return edges;
}
public String getName() {
return name;
}
}
}