/* The MIT License (MIT)
*
* Copyright (c) 2015 Reinventing Geospatial, Inc.
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
package com.rgi.geopackage.extensions.routing.router.astar;
import com.rgi.common.Memoize2;
import com.rgi.geopackage.extensions.network.AttributeDescription;
import com.rgi.geopackage.extensions.network.AttributedEdge;
import com.rgi.geopackage.extensions.network.AttributedNode;
import com.rgi.geopackage.extensions.network.NodeExitGetter;
import com.rgi.geopackage.extensions.routing.GeoPackageRoutingExtension;
import com.rgi.geopackage.extensions.routing.Route;
import com.rgi.geopackage.extensions.routing.RoutingNetworkDescription;
import com.rgi.geopackage.extensions.routing.router.Router;
import java.sql.SQLException;
import java.util.Collection;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.PriorityQueue;
import java.util.function.BiFunction;
import java.util.function.Function;
/**
* @author Luke Lambert
*/
public class AStar extends Router implements AutoCloseable
{
/**
* Constructor
*
* @param routingExtension
* Handle to a GeoPackage's routing extension
* @param routingNetwork
* Network on which to route between a start and end node
* @param nodeAttributeDescriptions
* Attributes of each network node to query for. These
* attributes will be passed to the edge cost evaluator via
* {@link AttributedNode#getAttributes()} as an array of {@link Object}s
* in the <i>in the order in which the {@link
* AttributeDescription}s are specified</i>.
* @param edgeAttributeDescriptions
* Attributes of each network edge to query for. These
* attributes will be passed to the edge cost evaluator via
* {@link AttributedEdge#getEdgeAttributes()} as an
* array of {@link Object}s in the <i>in the order in which the
* {@link AttributeDescription}s are specified</i>.
* @param edgeCostEvaluator
* Cost function for each edge in the path
* @param heuristic
* Cost heuristic function to be applied between a intermediate
* and end node to determine the search order of A*
* @throws SQLException
* if there is a database error
*/
public AStar(final GeoPackageRoutingExtension routingExtension,
final RoutingNetworkDescription routingNetwork,
final Collection<AttributeDescription> nodeAttributeDescriptions,
final Collection<AttributeDescription> edgeAttributeDescriptions,
final Function<AttributedEdge, Double> edgeCostEvaluator,
final BiFunction<AttributedNode, AttributedNode, Double> heuristic) throws SQLException
{
this(routingExtension,
routingNetwork,
nodeAttributeDescriptions,
edgeAttributeDescriptions,
edgeCostEvaluator,
heuristic,
null, // Creates empty list
null); // Creates empty list
}
/**
* Constructor
*
* @param routingExtension
* Handle to a GeoPackage's routing extension
* @param routingNetwork
* Network on which to route between a start and end node
* @param nodeAttributeDescriptions
* Attributes of each network node to query for. These
* attributes will be passed to the edge cost evaluator via
* {@link AttributedNode#getAttributes()} as an array of {@link Object}s
* in the <i>in the order in which the {@link
* AttributeDescription}s are specified</i>.
* @param edgeAttributeDescriptions
* Attributes of each network edge to query for. These
* attributes will be passed to the edge cost evaluator via
* {@link AttributedEdge#getEdgeAttributes()} as an
* array of {@link Object}s in the <i>in the order in which the
* {@link AttributeDescription}s are specified</i>.
* @param edgeCostEvaluator
* Cost function for each edge in the path
* @param heuristic
* Cost heuristic function to be applied between a intermediate
* and end node to determine the search order of A*
* @param restrictedNodeIdentifiers
* Collection of nodes to not consider in routing
* @param restrictedEdgeIdentifiers
* Collection of edges to not consider in routing
* @throws SQLException
* if there is a database error
*/
public AStar(final GeoPackageRoutingExtension routingExtension,
final RoutingNetworkDescription routingNetwork,
final Collection<AttributeDescription> nodeAttributeDescriptions,
final Collection<AttributeDescription> edgeAttributeDescriptions,
final Function<AttributedEdge, Double> edgeCostEvaluator,
final BiFunction<AttributedNode, AttributedNode, Double> heuristic,
final Collection<Integer> restrictedNodeIdentifiers,
final Collection<Integer> restrictedEdgeIdentifiers) throws SQLException
{
super(routingExtension,
routingNetwork,
nodeAttributeDescriptions,
edgeAttributeDescriptions,
edgeCostEvaluator,
restrictedNodeIdentifiers,
restrictedEdgeIdentifiers);
if(heuristic == null)
{
throw new IllegalArgumentException("Heuristic function may not be null");
}
this.cachedHeuristic = new Memoize2<>(heuristic);
this.edgeGetter = this.networkExtension.getNodeExitGetter(routingNetwork.getNetwork(),
this.nodeAttributeDescriptions,
this.edgeAttributeDescriptions);
}
@Override
public void close() throws SQLException
{
this.edgeGetter.close();
}
/**
* This algorithm will find the route from the start node to the end node
*
* @param startNodeIdentifier
* Starting node
* @param endNodeIdentifier
* Ending node
* @return Optimal path from the start node to the end node
* @throws SQLException
* if there is a database error
*/
@Override
public Route route(final int startNodeIdentifier,
final int endNodeIdentifier) throws SQLException
{
final PriorityQueue<Vertex> openList = new PriorityQueue<>(10, AStar.vertexComparator);
final Collection<Integer> closedList = new HashSet<>((this.restrictedNodeIdentifiers == null) ? Collections.<Integer>emptySet() : this.restrictedNodeIdentifiers);
final Map<Integer, Vertex> nodeMap = new HashMap<>();
final AttributedNode startNode = this.networkExtension.getAttributedNode(startNodeIdentifier, this.nodeAttributeDescriptions);
final AttributedNode endNode = this.networkExtension.getAttributedNode(endNodeIdentifier, this.nodeAttributeDescriptions);
// Starting Vertex
final Vertex startVertex = new Vertex(startNode,
0.0,
this.cachedHeuristic.get(startNode, endNode));
nodeMap.put(startNodeIdentifier, startVertex);
for(Vertex currentVertex = startVertex; currentVertex != null; currentVertex = openList.poll())
{
// If current vertex is the target then we are done
if(currentVertex.getNode().getIdentifier() == endNodeIdentifier)
{
return getAStarPath(endNodeIdentifier, nodeMap);
}
closedList.add(currentVertex.getNode().getIdentifier()); // Put it in "done" pile
for(final AttributedEdge exit : this.edgeGetter.getExits(currentVertex.getNode().getIdentifier())) // For each node adjacent to the current node
{
// Ignore restricted edges
if(!this.restrictedEdgeIdentifiers.contains(exit.getEdgeIdentifier()))
{
Vertex reachableVertex = nodeMap.get(exit.getToNode().getIdentifier());
if(reachableVertex == null)
{
reachableVertex = new Vertex(exit.getToNode());
nodeMap.put(exit.getToNode().getIdentifier(), reachableVertex);
}
// If the closed list already searched this vertex, skip it
if(!closedList.contains(exit.getToNode().getIdentifier()))
{
final double edgeCost = this.edgeCostEvaluator.apply(exit);
if(edgeCost <= 0.0) // Are positive values that are extremely close to 0 going to be a problem?
{
throw new RuntimeException("The A* algorithm is only valid for edge costs greater than 0");
}
final double costFromStart = currentVertex.getCostFromStart() + edgeCost;
final boolean isShorterPath = costFromStart < reachableVertex.getCostFromStart();
if(!openList.contains(reachableVertex) || isShorterPath)
{
final double estimatedCostFromEnd = exit.getToNode().getIdentifier() == endNode.getIdentifier() ? 0.0
: this.cachedHeuristic.get(reachableVertex.getNode(), endNode);
reachableVertex.update(costFromStart,
estimatedCostFromEnd,
currentVertex,
exit.getEdgeIdentifier(),
exit.getEdgeAttributes(),
edgeCost);
if(isShorterPath)
{
openList.remove(reachableVertex); // Re-add to trigger the reprioritization of this vertex
}
openList.add(reachableVertex);
}
}
}
}
}
return null; // No path between the start and end nodes
}
private static Route getAStarPath(final Integer end, final Map<Integer, Vertex> nodeMap)
{
final LinkedList<List<Object>> nodesAttributes = new LinkedList<List<Object>>();
final LinkedList<List<Object>> edgesAttributes = new LinkedList<List<Object>>();
final LinkedList<Integer> edgeIdentifiers = new LinkedList<Integer>();
final LinkedList<Double> edgeCosts = new LinkedList<Double>();
for(Vertex vertex = nodeMap.get(end); vertex != null; vertex = vertex.getPrevious())
{
nodesAttributes.addFirst(vertex.getNode().getAttributes());
if(vertex.getPrevious() != null)
{
edgesAttributes.addFirst(vertex.getEdgeAttributes());
edgeIdentifiers.addFirst(vertex.getEdgeIdentifier());
edgeCosts .addFirst(vertex.getEdgeCost());
}
}
return new Route(nodesAttributes,
edgesAttributes,
edgeIdentifiers,
edgeCosts);
}
private final Memoize2<AttributedNode, AttributedNode, Double> cachedHeuristic;
private final NodeExitGetter edgeGetter;
private static final Comparator<Vertex> vertexComparator = (vertex1, vertex2) -> Double.compare((vertex1.getEstimatedCostToEnd() + vertex1.getCostFromStart()),
(vertex2.getEstimatedCostToEnd() + vertex2.getCostFromStart()));
}