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 }