// A* Demonstration applet // Copyright 2007 Amit J Patel, amitp@cs.stanford.edu // License: MIT (see LICENSE file) package { import flash.display.*; import flash.geom.*; import flash.text.*; import flash.events.*; import flash.utils.*; import flash.filters.*; import flash.net.*; [SWF(width="500", height="500", frameRate="10", backgroundColor="#ffffff")] public class main extends Sprite { // Configuration: public var configList:Array = ["base_astar", "map_u"]; // Underlying data: public var graph:Graph; public var costs:Object = new Object(); // [node] => cost public var pathfinderAlpha:Number = 0.499; public var pathfinderHeuristic:Function = manhattanHeuristic; public var pathfinderCost:Function = simpleCost; private var startNode:Object; // node private var endNode:Object; // node // Display: public var grid:Sprite; public var overlay:Sprite; public var draggableShapes:Sprite; public var statusBar:TextField; // Interaction: public var dragHandler:Function; public var dragWeight:Number; function main() { // For debugging, I create a large textfield in the // background; see the Debug class. var debug:Debug = new Debug(this); debug.y = 350; debug.height -= 350; addChild(debug); // Parameters are set in the enclosing HTML, with the // flashVars parameter. Those parameters are decoded into // loaderInfo.parameters. for (var param:String in loaderInfo.parameters) { var value:String = loaderInfo.parameters[param]; if (param == "load") { if (value.match(/^\w+$/)) { configList = configList.concat(value.split(",")); } } } // graph = new TriangleGrid(10, 21); // graph = new SquareGrid(35, 25, 14); graph = new HexagonGrid(8, 10); Utils.graph = graph; statusBar = new TextField(); statusBar.text = "A*"; statusBar.selectable = false; statusBar.width = width; statusBar.height = statusBar.textHeight + 4; addChild(statusBar); grid = new Sprite(); grid.y = statusBar.height; addChild(grid); overlay = new Sprite(); grid.addChild(overlay); draggableShapes = new Sprite(); grid.addChild(draggableShapes); draggableShapes.buttonMode = true; draggableShapes.filters = [new DropShadowFilter(1), new BevelFilter(1)]; Utils.drawGrid(grid.graphics, costs); startNode = graph.centerNode(); endNode = graph.centerNode(); grid.addEventListener(MouseEvent.MOUSE_DOWN, onMouseDown); grid.addEventListener(MouseEvent.MOUSE_UP, onMouseUp); grid.addEventListener(MouseEvent.ROLL_OUT, onMouseUp); loadNextConfiguration(); recalculate(); } // The configList is a list of configurations to load, one after // another. This function loads the first one and removes it from // the list. public function loadNextConfiguration():void { if (configList.length == 0) return; var config:String = configList.shift(); var loader:URLLoader = new URLLoader(); loader.addEventListener(Event.COMPLETE, function ():void {handleConfiguration(loader.data)}); loader.addEventListener(SecurityErrorEvent.SECURITY_ERROR, function (e:*):void {Debug.trace("SECURITY ERROR: " + e.text);}); loader.addEventListener(IOErrorEvent.IO_ERROR, function (e:*):void {Debug.trace("IO ERROR: " + e.text);}); loader.load(new URLRequest(config + ".xml")); } public function handleConfiguration(xmlData:String):void { // TODO: Parameters that we can use: start, end, grid // shape, grid size, weights at each location (requires // graph.nodeFromString), choice of algorithm, choice of // heuristic We also want nodeFromString in order to save // data to local disk (flash.net.SharedObject). // TODO: easier validation of the data, in particular, nodes. We need a // graph method to tell us whether a node is valid. var data:XML = XML(xmlData); var node:Object; for each (var child:XML in data.children()) { if (child.name() == "weight") { // For a weight, it's either a single point with at="", or // it's a line drawn with from="" and to="". var v:Number = parseFloat(child.@value); if (v != Infinity && v != 1) { Debug.trace("Invalid weight ", v, " in ", child.toXMLString()); continue; } var nodes:Array = []; if (child.@at.length() > 0) { nodes = [graph.stringToNode(child.@at)]; } else if (child.@from.length() > 0 && child.@to.length() > 0) { var start:Object = graph.stringToNode(child.@from); var end:Object = graph.stringToNode(child.@to); if (!graph.nodeValid(start)) { Debug.trace("Invalid start node in ", child.toXMLString()); } else if (!graph.nodeValid(end)) { Debug.trace("Invalid end node in ", child.toXMLString()); } else { var pathfinder:Pathfinder = new Pathfinder( graph, start, end, euclideanHeuristic, function cost(a:Object, b:Object):Number { return 1 } ); pathfinder.alpha = 0.0; pathfinder.findPath(); nodes = pathfinder.path; } } for each (node in nodes) { var s:String = graph.nodeToString(node); if (graph.nodeValid(node)) { costs[s] = v; } else { Debug.trace("Invalid node ", s, " in ", child.toXMLString()); } } } else if (child.name() == "start") { node = graph.stringToNode(child.@at); if (graph.nodeValid(node)) { startNode = node; } else { Debug.trace("Invalid start node in ", child.toXMLString()); } } else if (child.name() == "end") { node = graph.stringToNode(child.@at); if (graph.nodeValid(node)) { endNode = node; } else { Debug.trace("Invalid end node in ", child.toXMLString()); } } else if (child.name() == "alpha") { var a:Number = parseFloat(child.@value); if (0.0 <= a && a <= 1.0) { pathfinderAlpha = a; } else { Debug.trace("Invalid alpha ", a, " in ", child.toXMLString()); } } else if (child.name() == "heuristic") { if (child.@value == "manhattan") { pathfinderHeuristic = manhattanHeuristic; } else if (child.@value == "euclidean") { pathfinderHeuristic = euclideanHeuristic; } else { Debug.trace("Unknown heuristic in ", child.toXMLString()); } } } loadNextConfiguration(); Utils.drawGrid(grid.graphics, costs); recalculate(); } public function onMouseDown(evt:MouseEvent):void { var n:Object = graph.pointToNode(new Point(evt.localX, evt.localY)); if (n) { if (graph.nodesEqual(n, startNode)) { dragHandler = dragStartPoint; } else if (graph.nodesEqual(n, endNode)) { dragHandler = dragEndPoint; } else { /* TODO: we can't support multiple weights until we figure out a good way to draw them on screen */ dragWeight = simpleCost(null, n) == 1 ? Infinity : 1; dragHandler = dragObstacle; } grid.addEventListener(MouseEvent.MOUSE_MOVE, dragHandler); dragHandler(evt); } } public function onMouseUp(evt:MouseEvent):void { if (dragHandler != null) { grid.removeEventListener(MouseEvent.MOUSE_MOVE, dragHandler); dragHandler = null; } } public function dragStartPoint(evt:MouseEvent):void { var n:Object = graph.pointToNode(new Point(evt.localX, evt.localY)); if (n && isFinite(simpleCost(null, n)) && !graph.nodesEqual(n, startNode)) { startNode = n; recalculate(); } } public function dragEndPoint(evt:MouseEvent):void { var n:Object = graph.pointToNode(new Point(evt.localX, evt.localY)); if (n && isFinite(simpleCost(null, n)) && !graph.nodesEqual(n, endNode)) { endNode = n; recalculate(); } } public function dragObstacle(evt:MouseEvent):void { var n:Object = graph.pointToNode(new Point(evt.localX, evt.localY)); if (!n || graph.nodesEqual(n, startNode) || graph.nodesEqual(n, endNode)) { // We can't draw here, either because it's invalid, or because // we don't want to mess up the draggable markers. return; } var ns:String = graph.nodeToString(n); if (costs[ns] != dragWeight) { costs[ns] = dragWeight; Utils.drawGrid(grid.graphics, costs); recalculate(); } } public function recalculate():void { overlay.graphics.clear(); draggableShapes.graphics.clear(); var t1:uint = flash.utils.getTimer(); var pathfinder:Pathfinder = new Pathfinder(graph, startNode, endNode, pathfinderHeuristic, pathfinderCost); pathfinder.alpha = pathfinderAlpha; if (simpleCost(null, startNode) != Infinity && simpleCost(null, endNode) != Infinity) { pathfinder.findPath(); } var t2:uint = flash.utils.getTimer(); var visitedLength:int = 0; for (var kw:String in pathfinder.visited) { visitedLength++; } Utils.drawExploredNodes(pathfinder, overlay.graphics); /* Draw start and end nodes */ draggableShapes.graphics.beginFill(0x8080ff, 1.0); Utils.drawNode(draggableShapes.graphics, endNode, true); draggableShapes.graphics.endFill(); draggableShapes.graphics.beginFill(0xff8080, 1.0); Utils.drawNode(draggableShapes.graphics, startNode, true); draggableShapes.graphics.endFill(); Utils.drawParentPointers(pathfinder, overlay.graphics); Utils.drawPath(pathfinder.path, overlay.graphics); var t3:uint = flash.utils.getTimer(); if (pathfinder.path.length > 1) { statusBar.text = "A*: Path length: " + pathfinder.path.length + "; OPEN set has " + pathfinder.open.length + " nodes" + "; visited " + visitedLength + " nodes" + "; " + (t2-t1) + "ms to compute."; } else { statusBar.text = "A*: drag pink start position and blue goal position."; } } public function manhattanHeuristic(a:Object, b:Object):Number { /* HACK: we add cost and subtract one to improve the situation in which the goal has a non-1 cost */ return graph.distance(a, b) + simpleCost(a, b) - 1; } public function euclideanHeuristic(a:Object, b:Object):Number { return graph.nodeCenter(a).subtract(graph.nodeCenter(b)).length; } public function simpleCost(a:Object, b:Object):Number { /* For now, we restrict costs to 1 or infinity, because we don't have a * good way to display all the relevant data visually (f, g, h, * cost, open, closed, parent pointer). */ var c:Number = costs[graph.nodeToString(b)]; if (isNaN(c)) { return 1; } else { return c; } } } }