1 package cz.cuni.amis.pathfinding.alg.astar; 2 3 import java.util.Collection; 4 import java.util.Collections; 5 import java.util.HashSet; 6 import java.util.Iterator; 7 8 import cz.cuni.amis.pathfinding.map.IPFGoal; 9 import cz.cuni.amis.pathfinding.map.IPFMap; 10 import cz.cuni.amis.pathfinding.map.IPFMapView; 11 import cz.cuni.amis.pathfinding.map.IPFMapView.DefaultView; 12 import cz.cuni.amis.utils.Iterators; 13 import cz.cuni.amis.utils.NullCheck; 14 import cz.cuni.amis.utils.astar.AStarGoal; 15 import cz.cuni.amis.utils.astar.AStarHeuristic; 16 import cz.cuni.amis.utils.astar.AStarMap; 17 import cz.cuni.amis.utils.heap.Heap; 18 import cz.cuni.amis.utils.heap.ImmutableHeap; 19 20 /** 21 * Implementation of generic A* algorithm, better refered to as A* Machine according to 22 * Dan Higgins, Generic A* Pathfind paper from AI Gaming Wisdom, 2002 23 * <p><p> 24 * What is A*<p> 25 * ----------<p> 26 * A* is space-search algorithm using a custom-built heuristic. It's an improved 27 * version of well-known Dijkstra algorithm which is used to find the shortest 28 * path in weighted graphs. Instead of picking the node with the smallest path 29 * from the start node it chooses node which seems to be on the shortest path 30 * to the goal node (and this guess is based on provided heuristic). 31 * <p><p> 32 * Note<p> 33 * ----<p> 34 * Insted of weights we speak about cost of the edges. 35 * <p><p> 36 * Limitation of A*<p> 37 * ----------------<p> 38 * 1) A* doesn't work over graphs with negative edge costs.<p> 39 * 2) heuristic has to be correct & monotonic 40 * <p> 41 * First we have to specify some interfaces for A* to work<p> 42 * ------------------------------------------------------<p> 43 * <ol> 44 * <li>{@link IPFMap} that provides the search-space notion for the A*, this implementation should be agent-agnostic, that is it should not incorporate 45 * any particular limitations/abilities into it</li> 46 * <li>{@link IPFMapView} that provides agent's customized view of the map (extra arc costs, node filtering, etc.)</li> 47 * <li>{@link IPFGoal} that provides the heuristic function + the goal definition</li> 48 * <p> 49 * Note about Nodes<p> 50 * ----------------<p> 51 * Note that we don't need to have a Node interface so you're free to have 52 * any nodes you want (POJOs). But implementation of A* requires the nodes to have 53 * {@link Object#hashCode()} and {@link Object#equals()} implemented correctly, which should be a good practice! 54 * <p> 55 * Note that also means you can't have two nodes which are equals in the map! 56 * <p> 57 * Note that if you have "unique object" for every "node", 58 * then the Java standard {@link Object#hashCode()} and {@link Object#equals()} implementations (pointer checking) are sufficient. 59 * <p><p> 60 * Ideas behind {@link IPFMap}, {@link IPFMapView} {@link IPFGoal}<p> 61 * ---------------------------------------------------------------<p> 62 * Usually you will have only one world / state space representation (that is {@link IPFMap}) but you need to 63 * change the cost of edges between nodes according to your agent (imagine a fish) for which you 64 * search the path ({@link IPFMapView}). Finally you will need to search the space for different nodes 65 * possibly using different heuristics based on the goal pursued ({@link IPFGoal}). 66 * <p><p> 67 * Imagine the situation with the lake (map) / human (one agent) / fish (another agent). 68 * Human may swim across the lake but it's faster to run around it (so you need to give the edges between 69 * water tiles an extra cost using {@link IPFMapView#getArcExtraCost(Object, Object, int)}).<p> 70 * Fish can swim really fast but can't get out of the water (so you need to forbid tiles around the lake 71 * and give the edges between the lakes' tiles using {@link IPFMapView#isNodeOpened(Object)}).<p> 72 * Finally you may have hierarchical representation of your graph that has different arc-cost for humans 73 * and fishes (taking into account the lake), thus you might need to specify different heuristic function for 74 * humans and fishes using {@link IPFGoal#getEstimatedCostToGoal(Object)}. 75 * <p> 76 * So the AStarMap will represent the world with the lake with default cost of the edges. 77 * AStarGoal may change the edges cost / forbid some nodes completely. So you will 78 * implement one goal for a human and another for a fish. 79 * <p><p> 80 * Note about the speed<p> 81 * --------------------<p> 82 * Speed of algorithm is based upon the speed of AStarOpenList and AStarCloseList. 83 */ 84 public class AStar<NODE> { 85 86 /** 87 * Holds the representation of the map. 88 */ 89 private IPFMap<NODE> map; 90 91 /** 92 * Holds the agent-specific view of the map. 93 */ 94 private IPFMapView<NODE> view; 95 96 /** 97 * AStar configured with "map" with no agent-specific view on the map, {@link DefaultView} is used. 98 * @param map 99 */ 100 public AStar(IPFMap<NODE> map) { 101 this.map = map; 102 this.view = new IPFMapView.DefaultView(); 103 NullCheck.check(this.map, "map"); 104 } 105 106 /** 107 * AStar configured with "map" and agent-specific view on the map, if "view" is null, {@link DefaultView} is going to be used. 108 * @param map 109 * @param view may be null 110 */ 111 public AStar(IPFMap<NODE> map, IPFMapView<NODE> view) { 112 this.map = map; 113 this.view = view; 114 NullCheck.check(this.map, "map"); 115 if (this.view == null) { 116 this.view = new IPFMapView.DefaultView(); 117 } 118 } 119 120 /** 121 * Map abstraction the AStar is working with. 122 * @return 123 */ 124 public IPFMap<NODE> getMap() { 125 return map; 126 } 127 128 /** 129 * Sets map abstraction into the AStar. 130 * @param map 131 */ 132 public synchronized void setMap(IPFMap<NODE> map) { 133 this.map = map; 134 } 135 136 /** 137 * Returns agent-specific map view for the map. 138 * @return 139 */ 140 public IPFMapView<NODE> getMapView() { 141 return view; 142 } 143 144 /** 145 * Sets agent-specific map view for the map. 146 * @param mapView 147 */ 148 public synchronized void setMapView(IPFMapView<NODE> mapView) { 149 this.view = mapView; 150 } 151 152 //////////////////////////////////////////////////////////// 153 // AStar runtime variables - cleared after aStar() finishes. 154 //////////////////////////////////////////////////////////// 155 156 private IPFGoal<NODE> goal = null; 157 private long iterationsMax = 0; 158 private AStarResult<NODE> result = null; 159 160 /** 161 * Method performing an AStar search over graph defined inside {@link IPFMap} starting from 'start' node driving 162 * itself towards goal that is described by {@link AStarGoal}. Note that {@link AStarGoal} also contains a heuristic {@link AStarHeuristic}. 163 * <p><p> 164 * {@link AStarMap} provides informations about node neighbours and edge costs, 165 * while {@link AStarGoal} contains the definition of goal node and extra cost / extra info 166 * about map nodes. 167 * <p><p> 168 * You may also specify maxIterations - "how long the A* should search" equals 169 * to number of evaluated nodes. If it's < 0 then A* will run until the 'goal' is found 170 * all nodes are evaluated and there is nowhere to search. If it is == 0, the A* won't even start! 171 * 172 * @param map 173 * @param start 174 * @param goal 175 * @param iterationsMax maximum of iterations to be made by algorithm during the search (zero or negative number == infinite) 176 */ 177 public synchronized AStarResult<NODE> findPath(IPFGoal<NODE> goal, long iterationsMax) { 178 179 // NOTE: values of the estimated cost is maintained in AStarResult 180 // AS HEAP: we're using Heap with AStarHeapComparator which is 181 // using data from AStarResult.estimatedCost ... 182 // that means you have to first alter AStarResult.estimatedCost 183 // before adding / decreasing key in AStarHeap 184 185 this.goal = goal; 186 this.iterationsMax = iterationsMax; 187 188 NODE start = goal.getStart(); 189 this.result = new AStarResult<NODE>(); 190 191 result.openList = new Heap<NODE>(new AStarHeapComparator<NODE>(result.estimatedCost), 64); 192 result.closeList = new HashSet<NODE>(); 193 Collection<NODE> close = result.closeList; 194 195 goal.setCloseList(Collections.unmodifiableSet(result.closeList)); 196 goal.setOpenList(new ImmutableHeap(result.openList)); 197 198 result.startNode = start; 199 200 result.putCostToNode(result.startNode, 0); 201 result.putEstimatedCostToNode(result.startNode, goal.getEstimatedCostToGoal(result.startNode)); 202 result.openList.add(result.startNode); 203 204 NODE node, nextNode; 205 Collection<NODE> neighbors; 206 Collection<NODE> extraNeighbors; 207 Iterator<NODE> nodeIter; 208 int nodePathCost, nextNodePathCost, travelCost, extraCost, nodeCost, nodeExtraCost, estimatedPathCost, newNextNodePathCost; 209 210 while ((!result.openList.empty()) && 211 ((this.iterationsMax <= 0) || (result.interations < iterationsMax)) 212 ){ 213 ++result.interations; // new iteration begin 214 215 node = result.openList.getMin(); 216 217 if (node == null){ // failure 218 result.success = false; 219 break; 220 } 221 222 result.openList.deleteMin(); 223 224 if (goal.isGoalReached(node)){ // we've reached the goal HURRAY! 225 result.goalNode = node; 226 result.success = true; 227 break; 228 } 229 230 nodePathCost = result.getCostToNode(node); 231 232 neighbors = map.getNeighbors(node); 233 extraNeighbors = view.getExtraNeighbors(node, neighbors); 234 nodeIter = new Iterators<NODE>( 235 neighbors == null ? null : neighbors.iterator(), 236 extraNeighbors == null ? null : extraNeighbors.iterator() 237 ); 238 239 while (nodeIter.hasNext()){ 240 // iterate over all of the neighbors node 241 nextNode = nodeIter.next(); 242 if (nextNode == null) continue; 243 // and evaluate them one by one 244 245 if (!view.isNodeOpened(nextNode)){ // stepping to this node is forbidden, skip it 246 continue; 247 } 248 if (!view.isArcOpened(node, nextNode)) { // travelling through this arc is forbidden, skip it 249 continue; 250 } 251 252 travelCost = map.getArcCost(node, nextNode); 253 extraCost = view.getArcExtraCost(node, nextNode, travelCost); 254 255 nodeCost = map.getNodeCost(nextNode); 256 nodeExtraCost = view.getNodeExtraCost(nextNode, nodeCost); 257 258 nextNodePathCost = result.getCostToNode(nextNode); 259 if (nextNodePathCost == -1){ 260 // we've never touched nextNode 261 nextNodePathCost = nodePathCost + travelCost + extraCost + nodeCost + nodeExtraCost; 262 if (nextNodePathCost < 0) nextNodePathCost = 0; 263 result.putCostToNode(nextNode, nextNodePathCost); 264 result.putPreviousNode(nextNode, node); 265 266 estimatedPathCost = nextNodePathCost + goal.getEstimatedCostToGoal(nextNode); 267 result.putEstimatedCostToNode(nextNode, estimatedPathCost); 268 269 result.openList.add(nextNode); 270 continue; 271 } else { 272 // we've already touched the nextNode 273 newNextNodePathCost = nodePathCost + travelCost + extraCost + nodeCost + nodeExtraCost; 274 if (newNextNodePathCost < 0) newNextNodePathCost = 0; 275 if (newNextNodePathCost < nextNodePathCost){ 276 estimatedPathCost = newNextNodePathCost + goal.getEstimatedCostToGoal(nextNode); 277 result.putCostToNode(nextNode, newNextNodePathCost); 278 result.putEstimatedCostToNode(nextNode, estimatedPathCost); 279 if (close.contains(nextNode)){ 280 close.remove(nextNode); 281 result.openList.add(nextNode); 282 } else 283 if (result.openList.contains(nextNode)){ 284 result.openList.decreaseKey(node); 285 } else { 286 result.openList.add(nextNode); 287 } 288 } 289 // if estimatedCost is higher or equal, we don't have to take any actions 290 continue; 291 } 292 } 293 close.add(node); 294 } 295 296 return result; 297 } 298 299 /** 300 * Method performing an AStar search over graph defined inside {@link IPFMap} starting from 'start' node driving 301 * itself towards goal that is described by {@link IPFGoal}. Note that {@link IPFGoal} also contains a heuristic function. 302 * <p><p> 303 * {@link IPFMap} provides informations about node neighbours and edge costs, 304 * while {@link IPFGoal} contains the definition of goal node and extra cost / extra info 305 * about map nodes. 306 * <p><p> 307 * Does not have any cap on the number of evaluated nodes. Will run until the 'goal' is found 308 * all nodes are evaluated and there is nowhere to search. 309 * 310 * @param goal defines START-NODE + GOAL-NODE 311 * @param mapView use custom {@link IPFMapView} 312 */ 313 public synchronized AStarResult<NODE> findPath(IPFGoal<NODE> goal, IPFMapView<NODE> mapView) { 314 IPFMapView<NODE> oldView = view; 315 this.view = mapView; 316 AStarResult<NODE> result = findPath(goal, 0); 317 this.view = oldView; 318 return result; 319 } 320 321 /** 322 * Method performing an AStar search over graph defined inside {@link IPFMap} starting from 'start' node driving 323 * itself towards goal that is described by {@link IPFGoal}. Note that {@link IPFGoal} also contains a heuristic function. 324 * <p><p> 325 * {@link IPFMap} provides informations about node neighbours and edge costs, 326 * while {@link IPFGoal} contains the definition of goal node and extra cost / extra info 327 * about map nodes. 328 * <p><p> 329 * Does not have any cap on the number of evaluated nodes. Will run until the 'goal' is found 330 * all nodes are evaluated and there is nowhere to search. 331 * 332 * @param goal defines START-NODE + GOAL-NODE 333 */ 334 public synchronized AStarResult<NODE> findPath(IPFGoal<NODE> goal) { 335 return findPath(goal, 0); 336 } 337 338 }