View Javadoc

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             			result.putPreviousNode(nextNode, node);
280             			if (close.contains(nextNode)){
281             				close.remove(nextNode);            				
282             				result.openList.add(nextNode);
283             			} else 
284             				if (result.openList.contains(nextNode)){
285             					result.openList.decreaseKey(node);
286             				} else {
287             					result.openList.add(nextNode);
288             				}
289             		}
290                 	// if estimatedCost is higher or equal, we don't have to take any actions
291             		continue;
292             	}
293             }            
294             close.add(node);
295 		}
296 		
297 		return result;
298 	}
299 	
300 	/**
301 	 * Method performing an AStar search over graph defined inside {@link IPFMap} starting from 'start' node driving
302 	 * itself towards goal that is described by {@link IPFGoal}. Note that {@link IPFGoal} also contains a heuristic function.
303 	 * <p><p>
304 	 * {@link IPFMap} provides informations about node neighbours and edge costs,
305 	 * while {@link IPFGoal} contains the definition of goal node and extra cost / extra info
306 	 * about map nodes.
307 	 * <p><p>
308 	 * Does not have any cap on the number of evaluated nodes. Will run until the 'goal' is found
309 	 * all nodes are evaluated and there is nowhere to search.
310 	 * 
311 	 * @param goal defines START-NODE + GOAL-NODE
312 	 * @param mapView use custom {@link IPFMapView}
313 	 */
314 	public synchronized AStarResult<NODE> findPath(IPFGoal<NODE> goal, IPFMapView<NODE> mapView) {
315 		IPFMapView<NODE> oldView = view;
316 		this.view = mapView;
317 		AStarResult<NODE> result = findPath(goal, 0);
318 		this.view = oldView;
319 		return result;
320 	}
321 
322 	/**
323 	 * Method performing an AStar search over graph defined inside {@link IPFMap} starting from 'start' node driving
324 	 * itself towards goal that is described by {@link IPFGoal}. Note that {@link IPFGoal} also contains a heuristic function.
325 	 * <p><p>
326 	 * {@link IPFMap} provides informations about node neighbours and edge costs,
327 	 * while {@link IPFGoal} contains the definition of goal node and extra cost / extra info
328 	 * about map nodes.
329 	 * <p><p>
330 	 * Does not have any cap on the number of evaluated nodes. Will run until the 'goal' is found
331 	 * all nodes are evaluated and there is nowhere to search.
332 	 * 
333 	 * @param goal defines START-NODE + GOAL-NODE
334 	 */
335 	public synchronized AStarResult<NODE> findPath(IPFGoal<NODE> goal) {
336 		return findPath(goal, 0);
337 	}
338 	
339 }