/src/de/ghost23/common/statemachine/StateMachine.as
ActionScript | 668 lines | 185 code | 57 blank | 426 comment | 46 complexity | 81ca7f49daea2adac25d6982cadaa080 MD5 | raw file
- package de.ghost23.common.statemachine {
-
- import de.ghost23.common.process.ACommand;
- import de.ghost23.common.process.ProcessEvent;
- import flash.events.EventDispatcher;
- import flash.utils.Dictionary;
- import flash.utils.describeType;
- import flash.utils.getQualifiedClassName;
-
- /**
- * This state machine combines the use of commands with the management of a state network
- * and the ability to navigate that network, if needed.
- *
- * <p>You use the state machine solely by extending StateMachine and defining the state
- * network in the constructor. See the example for details. The state machine generally
- * consists of states, commands and inputs. Currently, the state machine supports defining
- * input commands, which get called, when its assigned state is entered.</p>
- *
- * <p>An input into the state machine is something, that can invoke a state change
- * (it is like a transition with a transition condition in 'finite state machine'-terminology).
- * Effectively it is a method call: <code>setInput(eventType:String)</code>, where
- * eventType is an input type.</p>
- *
- * <p>When using the state machine, do not forget to call init(). Here is a usage example of
- * a state machine. For information on how to create an individual state machine, see the examples
- * at the bottom.</p>
- <listing version="3.0">package {
-
- import de.ghost23.common.statemachine.StateMachineEvent;
-
- import flash.display.Sprite;
- import xmlStateMachine.XMLStateMachine;
-
- public class StateMachineTest extends Sprite {
-
- public function StateMachineTest() {
-
- var mySM:XMLStateMachine = new XMLStateMachine();
- mySM.addEventListener(StateMachineEvent.NEW_STATE, onNewState);
- mySM.addEventListener(StateMachineEvent.AIMED_STATE_REACHED, onAimedStateReached);
- mySM.init(); // Important !
-
- // We're interested for the state XML_PARSED
- mySM.aimForState(XMLStates.XML_PARSED);
-
- // We set off by injecting a new file to be parsed
- mySM.injectNewFileUrl("sample.xml"); // See examples at the bottom for details
- }
-
- private function onAimedStateReached(event:StateMachineEvent):void {
- trace("State XMLStates.XML_PARSED is reached!");
- }
-
- private function onNewState(event:StateMachineEvent):void {
- trace("New state reached -> " + event.currentState.identifier);
- }
- }
- }</listing>
- *
- * @example To explain its functionality further, let's look at an example first. In this example, we
- * build a XML Loader/Parser by using our state machine. Please note, that in reality, this
- * would probably not make sense, since it means quite a lot of code for so little purpose,
- * but for demonstrating the state machine, it a quite a good example, because it holds quite
- * a few topics: asynchronous tasks, passing data from one command to another and the possibility
- * for a command to decide, which happens next.
- *
- * <p>In order to build a state machine, it is recommended to create a class, that extends StateMachine:</p>
- *
- <listing version="3.0">package xmlStateMachine {
- import de.ghost23.common.statemachine.State;
- import de.ghost23.common.statemachine.StateMachine;
-
- import xmlStateMachine.commands.ParseXML;
- import xmlStateMachine.commands.RequestFile;
- import xmlStateMachine.commands.Reset;
- import xmlStateMachine.commands.UnloadFile;
-
- public class XMLStateMachine extends StateMachine {
-
- // Possible incoming events to change state
- public static const FILE_LOAD_REQUESTED:String = "onFileLoadRequested";
- public static const FILE_LOAD_COMPLETE:String = "onFileLoadComplete";
- public static const FILE_LOAD_FAILED:String = "onFileLoadFailed";
- public static const FILE_UNLOAD_REQUESTED:String = "onFileUnloadRequested";
- public static const XML_PARSING_STARTED:String = "onXMLParsingStarted";
- public static const XML_PARSING_COMPLETE:String = "onXMLParsingComplete";
- public static const XML_PARSING_FAILED:String = "onXMLParsingFailed";
- public static const MACHINE_RESET:String = "onMachineReset";
-
- // Definition of the state machine
- public function XMLStateMachine() {
-
- super(XMLStates.EMPTY_IDLE);
-
- states = XMLStates;
-
- XMLStates.EMPTY_IDLE.inputCommand = Reset;
- XMLStates.EMPTY_IDLE.stateAfterEvent[XMLStates.FILE_REQUESTING] = FILE_LOAD_REQUESTED;
-
- XMLStates.FILE_REQUESTING.inputCommand = RequestFile;
- XMLStates.FILE_REQUESTING.stateAfterEvent[XMLStates.FILE_LOADED] = FILE_LOAD_COMPLETE;
- XMLStates.FILE_REQUESTING.stateAfterEvent[XMLStates.FILE_LOADERROR] = FILE_LOAD_FAILED;
-
- XMLStates.FILE_LOADED.transient = true;
- XMLStates.FILE_LOADED.stateAfterEvent[XMLStates.XML_PARSING] = XML_PARSING_STARTED;
-
- XMLStates.FILE_LOADERROR.transient = true;
- XMLStates.FILE_LOADERROR.stateAfterEvent[XMLStates.EMPTY_IDLE] = MACHINE_RESET;
-
- XMLStates.XML_PARSING.inputCommand = ParseXML;
- XMLStates.XML_PARSING.stateAfterEvent[XMLStates.XML_PARSED] = XML_PARSING_COMPLETE;
- XMLStates.XML_PARSING.stateAfterEvent[XMLStates.XML_REJECTED] = XML_PARSING_FAILED;
-
- XMLStates.XML_PARSED.transient = true;
- XMLStates.XML_PARSED.stateAfterEvent[XMLStates.FILE_UNLOADING] = FILE_UNLOAD_REQUESTED;
-
- XMLStates.XML_REJECTED.transient = true;
- XMLStates.XML_REJECTED.stateAfterEvent[XMLStates.FILE_UNLOADING] = FILE_UNLOAD_REQUESTED;
-
- XMLStates.FILE_UNLOADING.inputCommand = UnloadFile;
- XMLStates.FILE_UNLOADING.stateAfterEvent[XMLStates.EMPTY_IDLE] = MACHINE_RESET;
- }
- }
- }</listing>
- * <p>You see two things here: First of all, a bunch of so called "incoming event" strings have been
- * defined. These "events" (which are not real events, just string identifiers) define the possible
- * signals, that can be called onto the state machine, to go from one state to another. If you
- * visualize a state machine, you can think of these events to be the vectors from one state to another.</p>
- *
- * <p>Secondly, you see some lines of code in the constructor. They actually define the state machine,
- * in this case the XML Loader/Parser. I hope, that by simply reading the lines, you already get an idea,
- * of what happens there. Let's take the first definition, for example:</p>
- <listing version="3.0">XMLStates.EMPTY_IDLE.inputCommand = Reset;
- XMLStates.EMPTY_IDLE.stateAfterEvent[XMLStates.FILE_REQUESTING] = FILE_LOAD_REQUESTED;</listing>
- * <p>There seems to be a class XMLStates, which has static properties. We visit that class later. For now,
- * you only have to know, that this class holds a bunch of static properties, which are states, of type
- * de.ghost23.common.statemachine.State. So, in the example above, for the state EMPTY_IDLE, we define
- * an input command. We give it a class reference, Reset. An input command is an ACommand
- * (de.ghost23.common.process.ACommand). It will be executed, once the state is entered. This input command
- * is optional, you don't have to specify an input command, if nothing special should happen, once you
- * enter a state.</p>
- *
- * <p>Next, you see the line with the stateAfterEvent property. This is a Dictionary. Here you define, which
- * state should follow the EMPTY_IDLE state, when a particular event comes in, in this case FILE_LOAD_REQUESTED.
- * So you can read this line as: If the state machine is in the state EMPTY_IDLE, and the event FILE_LOAD_REQUESTED
- * comes in, move to the state FILE_REQUESTING.</p>
- *
- * <p>You will notice, that the other lines of code in the constructor mostly do similar things. Sometimes,
- * they define no input command, sometimes they define not only one following state, but multiple. And sometimes
- * a state is defined to be 'transient'. That means, that the state machine will try to move the next state,
- * right after this one has been entered. So a transient state is normally very short-lived and not intended
- * to stay for long. For example, the state XML_REJECTED is transient. Why? Because, if an xml file has been
- * rejected because of bad syntax or content, there is no use of staying in that state for long. Instead, it makes
- * sense to go back to the EMPTY_IDLE state again, so that a new xml file can be loaded. The same is true for
- * the case, that xml file has been parsed successfully. It makes no sense to stay in that state for long, instead
- * it is better to go back to idle to be ready to take another xml file, if needed.</p>
- *
- * <p>Now let's look at that XMLStates class. It is simply used as a container object for storing the possible
- * states for this state machine and as an enumeration object:</p>
- <listing version="3.0">package xmlStateMachine {
- import de.ghost23.common.statemachine.State;
-
- public class XMLStates {
-
- // The states
- public static const EMPTY_IDLE:State = new State("EMPTY_IDLE");
- public static const FILE_REQUESTING:State = new State("FILE_REQUESTING");
- public static const FILE_LOADED:State = new State("FILE_LOADED");
- public static const FILE_LOADERROR:State = new State("FILE_LOADERROR");
- public static const XML_PARSING:State = new State("XML_PARSING");
- public static const XML_PARSED:State = new State("XML_PARSED");
- public static const XML_REJECTED:State = new State("XML_REJECTED");
- public static const FILE_UNLOADING:State = new State("FILE_UNLOADING");
- }
- }</listing>
- * <p>You could also use a simple Object for doing it, but using a class and static properties
- * has the advantage of having code completion and compile-time checking.</p>
- *
- * <p>You might ask yourself, how do we actually tell the state machine to load a particular file?
- * We have to add some methods to our individual state machine to that, this isn't something,
- * that StateMachine dictates. So let's look at XMLStateMachine again, this time with some
- * usefull methods:</p>
- <listing version="3.0">package xmlStateMachine {
- import de.ghost23.common.statemachine.State;
- import de.ghost23.common.statemachine.StateMachine;
-
- import flash.net.URLLoader;
-
- import xmlStateMachine.commands.ParseXML;
- import xmlStateMachine.commands.RequestFile;
- import xmlStateMachine.commands.Reset;
- import xmlStateMachine.commands.UnloadFile;
-
- public class XMLStateMachine extends StateMachine {
-
- // Possible incoming events to change state
- public static const FILE_LOAD_REQUESTED:String = "onFileLoadRequested";
- public static const FILE_LOAD_COMPLETE:String = "onFileLoadComplete";
- public static const FILE_LOAD_FAILED:String = "onFileLoadFailed";
- public static const FILE_UNLOAD_REQUESTED:String = "onFileUnloadRequested";
- public static const XML_PARSING_STARTED:String = "onXMLParsingStarted";
- public static const XML_PARSING_COMPLETE:String = "onXMLParsingComplete";
- public static const XML_PARSING_FAILED:String = "onXMLParsingFailed";
- public static const MACHINE_RESET:String = "onMachineReset";
-
- // Possible data field names
- public static const PARSED_OBJECT:String = "parsedObject";
- public static const FILE_URL:String = "fileURL";
- public static const FILE_RAW_XML:String = "fileXML";
-
- // Data fields of this particular state machine
-
- public function get fileURL():String {
- return _data[FILE_URL];
- }
-
- public function get rawXMLFile():URLLoader {
- return _data[FILE_RAW_XML];
- }
-
- public function get parsedXMLObject():Object {
- return _data[PARSED_OBJECT];
- }
-
- public function setNewFile(url:String):void {
- _data[FILE_URL] = url;
- try {
- setInput(FILE_LOAD_REQUESTED);
- }catch(e:Error) {
- trace("invoking event FILE_LOAD_REQUESTED not " +
- "possible in current state: " + currentState.identifier);
- }
- }
-
- // Definition of the state machine
- public function XMLStateMachine() {
-
- super(XMLStates.EMPTY_IDLE);
-
- states = XMLStates;
-
- XMLStates.EMPTY_IDLE.inputCommand = Reset;
- XMLStates.EMPTY_IDLE.stateAfterEvent[XMLStates.FILE_REQUESTING] = FILE_LOAD_REQUESTED;
-
- XMLStates.FILE_REQUESTING.inputCommand = RequestFile;
- XMLStates.FILE_REQUESTING.stateAfterEvent[XMLStates.FILE_LOADED] = FILE_LOAD_COMPLETE;
- XMLStates.FILE_REQUESTING.stateAfterEvent[XMLStates.FILE_LOADERROR] = FILE_LOAD_FAILED;
-
- XMLStates.FILE_LOADED.transient = true;
- XMLStates.FILE_LOADED.stateAfterEvent[XMLStates.XML_PARSING] = XML_PARSING_STARTED;
-
- XMLStates.FILE_LOADERROR.transient = true;
- XMLStates.FILE_LOADERROR.stateAfterEvent[XMLStates.EMPTY_IDLE] = MACHINE_RESET;
-
- XMLStates.XML_PARSING.inputCommand = ParseXML;
- XMLStates.XML_PARSING.stateAfterEvent[XMLStates.XML_PARSED] = XML_PARSING_COMPLETE;
- XMLStates.XML_PARSING.stateAfterEvent[XMLStates.XML_REJECTED] = XML_PARSING_FAILED;
-
- XMLStates.XML_PARSED.transient = true;
- XMLStates.XML_PARSED.stateAfterEvent[XMLStates.FILE_UNLOADING] = FILE_UNLOAD_REQUESTED;
-
- XMLStates.XML_REJECTED.transient = true;
- XMLStates.XML_REJECTED.stateAfterEvent[XMLStates.FILE_UNLOADING] = FILE_UNLOAD_REQUESTED;
-
- XMLStates.FILE_UNLOADING.inputCommand = UnloadFile;
- XMLStates.FILE_UNLOADING.stateAfterEvent[XMLStates.EMPTY_IDLE] = MACHINE_RESET;
- }
- }
- }</listing>
- * <p>You now see some methods been added to the class, like <code>setNewFile(url:String)</code>. Most
- * if these methods make use of the protected property _data, which is also accessible publicly via
- * 'data'. _data is a Dictionary and it is passed to every ACommand to share data. In the examples,
- * you see lines like: <code>_data[RequestFile.FILE_URL] = url;</code>. It is good practice to use
- * constants from the used commands to specify, how to identify a particular data field. In the little
- * example here, the command RequuestFile specifies a data field name FILE_URL. So the XMLStateMachine
- * uses that name to set the file url.</p>
- *
- * <p>Why is it done this way? The idea is, that for the command to
- * be mostly independent of anything else, it defines the fields, it needs. The state machine, that
- * brings everything together, has to know these and populate the fields accordingly. So the dependency
- * graph is top-down, which is normally a good approach, because it makes the commands be reusable. Of course,
- * you could also decide for doing it differently, the state machine itself doesn't force a particular
- * approach. To have the complete picture, let's look at the RequestFile command:</p>
- <listing version="3.0">package xmlStateMachine.commands {
-
- import de.ghost23.common.process.ACommand;
-
- import flash.events.Event;
- import flash.events.IOErrorEvent;
- import flash.net.URLLoader;
- import flash.net.URLRequest;
- import flash.utils.Dictionary;
- import xmlStateMachine.XMLStateMachine;
-
- public class RequestFile extends ACommand {
-
- private var xmlLoader:URLLoader;
- private var data:Dictionary;
-
- public function RequestFile() {
- super();
- }
-
- override public function execute(paramContainer:Dictionary):void {
-
- trace("command RequestFile is running with file: " + paramContainer[XMLStateMachine.FILE_URL]);
- data = paramContainer;
-
- if(data[XMLStateMachine.FILE_URL] == null) setResult(FAILURE);
- else {
-
- xmlLoader = new URLLoader();
- xmlLoader.addEventListener(Event.COMPLETE, onLoadComplete);
- xmlLoader.addEventListener(IOErrorEvent.IO_ERROR, onLoadError);
- xmlLoader.load(new URLRequest(data[XMLStateMachine.FILE_URL]));
- }
- }
-
- private function onLoadError(event:IOErrorEvent):void {
-
- xmlLoader.removeEventListener(Event.COMPLETE, onLoadComplete);
- xmlLoader = null;
- data = null;
- setResult(XMLStateMachine.FILE_LOAD_FAILED);
- }
-
- private function onLoadComplete(event:Event):void {
-
- data[XMLStateMachine.FILE_RAW_XML] = xmlLoader;
- xmlLoader.removeEventListener(Event.COMPLETE, onLoadComplete);
- xmlLoader = null;
- data = null;
- setResult(XMLStateMachine.FILE_LOAD_COMPLETE);
- }
- }
- }</listing>
- * <p>For some detailed explanation of ACommand, see its documentation. Here, please
- * concentrate on the execute method, which has the parameter 'data', which is the reference
- * to the _data field of our state machine.</p>
- * @author Sven Busse
- * @version 1.0
- */
- public class StateMachine extends EventDispatcher {
-
- /**
- * An enumeration object, that holds all the states, this state machine knows of.
- * This can either be a simple object, or (recommended) a class with static properties.
- * Using the latter gives you the advantage of having code completion in your editor.
- * Such a class would look like this:
- * @example
- <listing version="3.0">package xmlStateMachine {
- import de.ghost23.common.statemachine.State;
-
- public class XMLStates {
-
- // The states
- public static const EMPTY_IDLE:State = new State("EMPTY_IDLE");
- public static const FILE_REQUESTING:State = new State("FILE_REQUESTING");
- public static const FILE_LOADED:State = new State("FILE_LOADED");
- public static const FILE_LOADERROR:State = new State("FILE_LOADERROR");
- public static const XML_PARSING:State = new State("XML_PARSING");
- public static const XML_PARSED:State = new State("XML_PARSED");
- public static const XML_REJECTED:State = new State("XML_REJECTED");
- public static const FILE_UNLOADING:State = new State("FILE_UNLOADING");
- }
- }</listing>
- */
- protected var states:Object;
-
- /**
- * The current input command. This variable is null, unless a command is indeed running currently.
- */
- protected var currentInputCommand:ACommand;
-
- /**
- * The state, that has been set by <code>aimForTarget(state:State)</code>. If no target was
- * set, or the target has been reached, this is null.
- */
- protected var targetState:State;
-
- private var _data:Dictionary;
- private var targetPath:Vector.<State>;
- private var _currentState:State;
- private var initialized:Boolean = false;
-
- /**
- * The current state, the state machine is in. As long the state machine has not been initialized
- * with <code>init()</code>, this is null.
- */
- public function get currentState():State {
- return _currentState;
- }
-
- /**
- * A general purpose data container, that is shared among the commands, which are part of
- * this state machine. This data Dictionary is passed as a reference to each executed command
- * via its execute method.
- */
- public function get data():Dictionary {
- return _data;
- }
-
- /**
- * Constructs a new state machine.
- * <p>You will never instantiate StateMachine directly, instead you will subclass ist
- * and call the super constructor there. See the examples at the bottom.</p>
- * @param startState The state, in which the state machine will begin after <code>init()</code> has
- * been called.
- */
- public function StateMachine(startState:State) {
-
- _currentState = startState;
- _data = new Dictionary();
- }
-
- /**
- * Initializes the state machine. Before init() is called, the state machine has no defined state.
- * The reason for init() is, that you are able to add yourself as a listener to the state machine,
- * before the state machine actually begins.
- */
- public function init():void {
-
- initialized = true;
- dispatchEvent(
- new StateMachineEvent(
- StateMachineEvent.NEW_STATE,
- null,
- _currentState,
- null
- )
- );
- executeCurrentInputCommand();
- }
-
- /**
- * Sets a target state, that the state machine will try to reach withing the bounds of the
- * defined state network.
- * <p>One of the features of StateMachine is, that it is navigable. With aimForState(targetState:State)
- * you can set a state, that you want to reach and the state machine will navigate towards that
- * state, if there are forks in the path, which allow for the state machine to make autonomous decisions.
- * For example, consider this little state machine definition:</p>
- <listing version="3.0">SomeStates.FIRST_STATE.transient = true;
- SomeStates.FIRST_STATE.stateAfterEvent[SomeStates.SECOND_STATE] = THIS_HAPPENED;
- SomeStates.FIRST_STATE.stateAfterEvent[SomeStates.THIRD_STATE] = THAT_HAPPENED;
- </listing>
- * FIRST_STATE is transient, means, the state machine will directly try to move to the next state, if FIRST_STATE
- * has been entered. The question is, which state should the state machine go to? Let's assume, you have
- * called <code>aimForState(SomeStates.THIRD_STATE);</code> before. Then now the state machine will look for the
- * shortest path from FIRST_STATE to THIRD_STATE (which in this case is easy) and will automatically invoke
- * input calls (setInput()), that lead to the state aimed for. It does this only for transient states, of course,
- * because only then it gets the power over the control flow.
- * @param targetState The state, you are aiming to reach.
- * @see de.ghost23.common.statemachine.StateMachineEvent#AIMED_STATE_REACHED
- */
- public function aimForState(targetState:State):void {
-
- if(!initialized) throw new Error("StateMachine has not yet been initialized. Use init() first!");
- this.targetState = targetState;
- targetPath = createTargetStatePath(_currentState, targetState);
- }
-
- /**
- * Sets a new input signal (which is similar to a transformation route in traditional finite state machine terminology).
- * @param eventType The input type. Usually you will define the possible input types as constants in your
- * individual state machine. See examples at the bottom.
- */
- public function setInput(eventType:String):void {
-
- if(!initialized) throw new Error("StateMachine has not yet been initialized. Use init() first!");
- var stateForEvent:State;
- for(var elmt:* in _currentState.stateAfterEvent) {
- if(eventType == _currentState.stateAfterEvent[elmt]) {
- stateForEvent = elmt;
- break;
- }
- }
- if(stateForEvent == null) {
- throw new Error(
- "Incoming Event '" + eventType + "' is not an allowed event for the current state '"
- + _currentState.identifier + "'");
- }
-
- setNewState(stateForEvent);
- }
-
- private function executeCurrentInputCommand():void {
-
- if(_currentState.inputCommand != null) {
- if(currentInputCommand != null) clearCurrentInputCommand();
- currentInputCommand = new _currentState.inputCommand();
- currentInputCommand.addEventListener(ProcessEvent.COMPLETED, onInputCommandComplete);
- currentInputCommand.addEventListener(ProcessEvent.FAILED, onInputCommandFailed);
- currentInputCommand.execute(_data);
- }else {
- onInputCommandComplete(new ProcessEvent(ProcessEvent.COMPLETED), true);
- }
- }
-
- private function onInputCommandFailed(event:ProcessEvent):void {
-
- trace("INPUT COMMAND FAILED!!! " + getQualifiedClassName(currentInputCommand));
- trace("STATEMACHINE IS IN AN UNDEFINED STATE!!!");
- clearCurrentInputCommand();
- }
-
- private function onInputCommandComplete(event:ProcessEvent, fake:Boolean = false):void {
-
- if(!fake) clearCurrentInputCommand();
-
- if(_currentState.transient) {
- if(targetPath != null && targetPath.length > 1) setInput(_currentState.stateAfterEvent[targetPath[1]]);
- else {
- var stateAfterEventList:Vector.<String> = new Vector.<String>();
- for(var elmt:* in _currentState.stateAfterEvent) {
- stateAfterEventList.push(_currentState.stateAfterEvent[elmt]);
- }
- if(stateAfterEventList.length == 1) setInput(stateAfterEventList[0]);
- }
- }else if(event.resultCode != null) {
- setInput(event.resultCode);
- }
- }
-
- private function clearCurrentInputCommand():void {
-
- currentInputCommand.removeEventListener(ProcessEvent.COMPLETED, onInputCommandComplete);
- currentInputCommand.removeEventListener(ProcessEvent.FAILED, onInputCommandFailed);
- currentInputCommand = null;
- }
-
- private function setNewState(newState:State):void {
-
- var previousState:State = _currentState;
- _currentState = newState;
-
- if(targetPath != null && _currentState == targetState) {
-
- targetPath = null;
- targetState = null;
- dispatchEvent(
- new StateMachineEvent(
- StateMachineEvent.AIMED_STATE_REACHED,
- previousState,
- _currentState,
- _currentState
- )
- );
- }else if(targetPath != null && targetPath.length > 1) {
-
- targetPath.shift();
- if(_currentState != targetPath[0]) {
- targetPath = createTargetStatePath(_currentState, targetState);
- }
- }
-
- dispatchEvent(
- new StateMachineEvent(
- StateMachineEvent.NEW_STATE,
- previousState,
- _currentState,
- targetState
- )
- );
-
- executeCurrentInputCommand();
- }
-
- /**
- * Searches a path from the current state to a specified target state,
- * using the depth-first algorithm.
- * @param startState
- */
- private function createTargetStatePath(startState:State, targetState:State):Vector.<State> {
-
- var pathToTarget:Vector.<State> = new Vector.<State>;
-
- var currentState:State = targetState;
- var predecessors:Dictionary = dijkstra(startState);
- while(predecessors[currentState] != null) { // Der Vorg???nger des Startknotens ist null
- pathToTarget.unshift(predecessors[currentState]);
- currentState = predecessors[currentState];
- }
- return pathToTarget;
- }
-
- private function buildEnumObjectFromClass(sourceObject:*):Object {
-
- var result:Object = new Object();
- // determine if o is a class instance or a plain object
- var classInfo:XML = describeType( sourceObject );
-
- // Loop over all of the variables and accessors in the class and
- // serialize them along with their values.
- for each (var v:XML in classInfo..*.(
- name() == "variable" || name() == "constant"
- )) {
-
- // If [Transient] metadata exists, then we should skip
- if (v.@name == "prototype" || (v.metadata && v.metadata.( @name == "Transient" ).length() > 0)) {
- continue;
- }
-
- result[v.@name] = sourceObject[v.@name];
- }
-
- return result;
- }
-
- // --------------- Dijkstra implementation for finding the shortest path from one state to another.
-
- private function dijkstra(startState:State):Dictionary {
-
- var distances:Dictionary = new Dictionary();
- var predecessors:Dictionary = new Dictionary();
- var stateList:Vector.<State> = new Vector.<State>();
- var statesEnum:Object = buildEnumObjectFromClass(states);
-
- for each(var item:Object in statesEnum) {
- distances[item] = Number.POSITIVE_INFINITY;
- predecessors[item] = null;
- stateList.push(item);
- }
- distances[startState] = 0;
-
- var stateWithSmallestDistance:State;
- var followersOfState:Dictionary;
-
- /* a negative number, if x should appear before y in the sorted sequence
- 0, if x equals y
- a positive number, if x should appear after y in the sorted sequence
- */
- var sortForSmallestDistance:Function = function(x:State, y:State):Number {
- return distances[x] - distances[y];
- };
-
- while(stateList.length > 0) { // Der eigentliche Algorithmus
-
- stateWithSmallestDistance = Vector.<State>(stateList.sort(sortForSmallestDistance))[0];
-
- // An alternative within dijkstra. Since we have found our target already, we can abort the rest.
- if(stateWithSmallestDistance === null) return predecessors;
-
- stateList.splice(stateList.indexOf(stateWithSmallestDistance),1);
-
- followersOfState = stateWithSmallestDistance.stateAfterEvent;
-
- for(var elmt:* in followersOfState) {
- if(stateList.indexOf(elmt) > -1){
- updateDistance(stateWithSmallestDistance, elmt, distances, predecessors);
- }
- }
- }
- return predecessors;
- }
-
- private function updateDistance(stateWithSmallestDistance:State, neighbour:State, distance:Dictionary, predecessor:Dictionary):void {
-
- // 1 is the weight of the line between stateWithSmallestDistance and neighbour
- // We do not use weights, that's why we give 1 here in general.
- var alternative:Number = distance[stateWithSmallestDistance] + 1;
-
- if(alternative < distance[neighbour]) {
- distance[neighbour] = alternative;
- predecessor[neighbour] = stateWithSmallestDistance;
- }
- }
- }
- }