PageRenderTime 58ms CodeModel.GetById 29ms RepoModel.GetById 0ms app.codeStats 0ms

/src/de/ghost23/common/statemachine/StateMachine.as

http://command-collection.googlecode.com/
ActionScript | 668 lines | 185 code | 57 blank | 426 comment | 46 complexity | 81ca7f49daea2adac25d6982cadaa080 MD5 | raw file
  1. package de.ghost23.common.statemachine {
  2. import de.ghost23.common.process.ACommand;
  3. import de.ghost23.common.process.ProcessEvent;
  4. import flash.events.EventDispatcher;
  5. import flash.utils.Dictionary;
  6. import flash.utils.describeType;
  7. import flash.utils.getQualifiedClassName;
  8. /**
  9. * This state machine combines the use of commands with the management of a state network
  10. * and the ability to navigate that network, if needed.
  11. *
  12. * <p>You use the state machine solely by extending StateMachine and defining the state
  13. * network in the constructor. See the example for details. The state machine generally
  14. * consists of states, commands and inputs. Currently, the state machine supports defining
  15. * input commands, which get called, when its assigned state is entered.</p>
  16. *
  17. * <p>An input into the state machine is something, that can invoke a state change
  18. * (it is like a transition with a transition condition in 'finite state machine'-terminology).
  19. * Effectively it is a method call: <code>setInput(eventType:String)</code>, where
  20. * eventType is an input type.</p>
  21. *
  22. * <p>When using the state machine, do not forget to call init(). Here is a usage example of
  23. * a state machine. For information on how to create an individual state machine, see the examples
  24. * at the bottom.</p>
  25. <listing version="3.0">package {
  26. import de.ghost23.common.statemachine.StateMachineEvent;
  27. import flash.display.Sprite;
  28. import xmlStateMachine.XMLStateMachine;
  29. public class StateMachineTest extends Sprite {
  30. public function StateMachineTest() {
  31. var mySM:XMLStateMachine = new XMLStateMachine();
  32. mySM.addEventListener(StateMachineEvent.NEW_STATE, onNewState);
  33. mySM.addEventListener(StateMachineEvent.AIMED_STATE_REACHED, onAimedStateReached);
  34. mySM.init(); // Important !
  35. // We're interested for the state XML_PARSED
  36. mySM.aimForState(XMLStates.XML_PARSED);
  37. // We set off by injecting a new file to be parsed
  38. mySM.injectNewFileUrl("sample.xml"); // See examples at the bottom for details
  39. }
  40. private function onAimedStateReached(event:StateMachineEvent):void {
  41. trace("State XMLStates.XML_PARSED is reached!");
  42. }
  43. private function onNewState(event:StateMachineEvent):void {
  44. trace("New state reached -> " + event.currentState.identifier);
  45. }
  46. }
  47. }</listing>
  48. *
  49. * @example To explain its functionality further, let's look at an example first. In this example, we
  50. * build a XML Loader/Parser by using our state machine. Please note, that in reality, this
  51. * would probably not make sense, since it means quite a lot of code for so little purpose,
  52. * but for demonstrating the state machine, it a quite a good example, because it holds quite
  53. * a few topics: asynchronous tasks, passing data from one command to another and the possibility
  54. * for a command to decide, which happens next.
  55. *
  56. * <p>In order to build a state machine, it is recommended to create a class, that extends StateMachine:</p>
  57. *
  58. <listing version="3.0">package xmlStateMachine {
  59. import de.ghost23.common.statemachine.State;
  60. import de.ghost23.common.statemachine.StateMachine;
  61. import xmlStateMachine.commands.ParseXML;
  62. import xmlStateMachine.commands.RequestFile;
  63. import xmlStateMachine.commands.Reset;
  64. import xmlStateMachine.commands.UnloadFile;
  65. public class XMLStateMachine extends StateMachine {
  66. // Possible incoming events to change state
  67. public static const FILE_LOAD_REQUESTED:String = "onFileLoadRequested";
  68. public static const FILE_LOAD_COMPLETE:String = "onFileLoadComplete";
  69. public static const FILE_LOAD_FAILED:String = "onFileLoadFailed";
  70. public static const FILE_UNLOAD_REQUESTED:String = "onFileUnloadRequested";
  71. public static const XML_PARSING_STARTED:String = "onXMLParsingStarted";
  72. public static const XML_PARSING_COMPLETE:String = "onXMLParsingComplete";
  73. public static const XML_PARSING_FAILED:String = "onXMLParsingFailed";
  74. public static const MACHINE_RESET:String = "onMachineReset";
  75. // Definition of the state machine
  76. public function XMLStateMachine() {
  77. super(XMLStates.EMPTY_IDLE);
  78. states = XMLStates;
  79. XMLStates.EMPTY_IDLE.inputCommand = Reset;
  80. XMLStates.EMPTY_IDLE.stateAfterEvent[XMLStates.FILE_REQUESTING] = FILE_LOAD_REQUESTED;
  81. XMLStates.FILE_REQUESTING.inputCommand = RequestFile;
  82. XMLStates.FILE_REQUESTING.stateAfterEvent[XMLStates.FILE_LOADED] = FILE_LOAD_COMPLETE;
  83. XMLStates.FILE_REQUESTING.stateAfterEvent[XMLStates.FILE_LOADERROR] = FILE_LOAD_FAILED;
  84. XMLStates.FILE_LOADED.transient = true;
  85. XMLStates.FILE_LOADED.stateAfterEvent[XMLStates.XML_PARSING] = XML_PARSING_STARTED;
  86. XMLStates.FILE_LOADERROR.transient = true;
  87. XMLStates.FILE_LOADERROR.stateAfterEvent[XMLStates.EMPTY_IDLE] = MACHINE_RESET;
  88. XMLStates.XML_PARSING.inputCommand = ParseXML;
  89. XMLStates.XML_PARSING.stateAfterEvent[XMLStates.XML_PARSED] = XML_PARSING_COMPLETE;
  90. XMLStates.XML_PARSING.stateAfterEvent[XMLStates.XML_REJECTED] = XML_PARSING_FAILED;
  91. XMLStates.XML_PARSED.transient = true;
  92. XMLStates.XML_PARSED.stateAfterEvent[XMLStates.FILE_UNLOADING] = FILE_UNLOAD_REQUESTED;
  93. XMLStates.XML_REJECTED.transient = true;
  94. XMLStates.XML_REJECTED.stateAfterEvent[XMLStates.FILE_UNLOADING] = FILE_UNLOAD_REQUESTED;
  95. XMLStates.FILE_UNLOADING.inputCommand = UnloadFile;
  96. XMLStates.FILE_UNLOADING.stateAfterEvent[XMLStates.EMPTY_IDLE] = MACHINE_RESET;
  97. }
  98. }
  99. }</listing>
  100. * <p>You see two things here: First of all, a bunch of so called "incoming event" strings have been
  101. * defined. These "events" (which are not real events, just string identifiers) define the possible
  102. * signals, that can be called onto the state machine, to go from one state to another. If you
  103. * visualize a state machine, you can think of these events to be the vectors from one state to another.</p>
  104. *
  105. * <p>Secondly, you see some lines of code in the constructor. They actually define the state machine,
  106. * in this case the XML Loader/Parser. I hope, that by simply reading the lines, you already get an idea,
  107. * of what happens there. Let's take the first definition, for example:</p>
  108. <listing version="3.0">XMLStates.EMPTY_IDLE.inputCommand = Reset;
  109. XMLStates.EMPTY_IDLE.stateAfterEvent[XMLStates.FILE_REQUESTING] = FILE_LOAD_REQUESTED;</listing>
  110. * <p>There seems to be a class XMLStates, which has static properties. We visit that class later. For now,
  111. * you only have to know, that this class holds a bunch of static properties, which are states, of type
  112. * de.ghost23.common.statemachine.State. So, in the example above, for the state EMPTY_IDLE, we define
  113. * an input command. We give it a class reference, Reset. An input command is an ACommand
  114. * (de.ghost23.common.process.ACommand). It will be executed, once the state is entered. This input command
  115. * is optional, you don't have to specify an input command, if nothing special should happen, once you
  116. * enter a state.</p>
  117. *
  118. * <p>Next, you see the line with the stateAfterEvent property. This is a Dictionary. Here you define, which
  119. * state should follow the EMPTY_IDLE state, when a particular event comes in, in this case FILE_LOAD_REQUESTED.
  120. * So you can read this line as: If the state machine is in the state EMPTY_IDLE, and the event FILE_LOAD_REQUESTED
  121. * comes in, move to the state FILE_REQUESTING.</p>
  122. *
  123. * <p>You will notice, that the other lines of code in the constructor mostly do similar things. Sometimes,
  124. * they define no input command, sometimes they define not only one following state, but multiple. And sometimes
  125. * a state is defined to be 'transient'. That means, that the state machine will try to move the next state,
  126. * right after this one has been entered. So a transient state is normally very short-lived and not intended
  127. * to stay for long. For example, the state XML_REJECTED is transient. Why? Because, if an xml file has been
  128. * rejected because of bad syntax or content, there is no use of staying in that state for long. Instead, it makes
  129. * sense to go back to the EMPTY_IDLE state again, so that a new xml file can be loaded. The same is true for
  130. * the case, that xml file has been parsed successfully. It makes no sense to stay in that state for long, instead
  131. * it is better to go back to idle to be ready to take another xml file, if needed.</p>
  132. *
  133. * <p>Now let's look at that XMLStates class. It is simply used as a container object for storing the possible
  134. * states for this state machine and as an enumeration object:</p>
  135. <listing version="3.0">package xmlStateMachine {
  136. import de.ghost23.common.statemachine.State;
  137. public class XMLStates {
  138. // The states
  139. public static const EMPTY_IDLE:State = new State("EMPTY_IDLE");
  140. public static const FILE_REQUESTING:State = new State("FILE_REQUESTING");
  141. public static const FILE_LOADED:State = new State("FILE_LOADED");
  142. public static const FILE_LOADERROR:State = new State("FILE_LOADERROR");
  143. public static const XML_PARSING:State = new State("XML_PARSING");
  144. public static const XML_PARSED:State = new State("XML_PARSED");
  145. public static const XML_REJECTED:State = new State("XML_REJECTED");
  146. public static const FILE_UNLOADING:State = new State("FILE_UNLOADING");
  147. }
  148. }</listing>
  149. * <p>You could also use a simple Object for doing it, but using a class and static properties
  150. * has the advantage of having code completion and compile-time checking.</p>
  151. *
  152. * <p>You might ask yourself, how do we actually tell the state machine to load a particular file?
  153. * We have to add some methods to our individual state machine to that, this isn't something,
  154. * that StateMachine dictates. So let's look at XMLStateMachine again, this time with some
  155. * usefull methods:</p>
  156. <listing version="3.0">package xmlStateMachine {
  157. import de.ghost23.common.statemachine.State;
  158. import de.ghost23.common.statemachine.StateMachine;
  159. import flash.net.URLLoader;
  160. import xmlStateMachine.commands.ParseXML;
  161. import xmlStateMachine.commands.RequestFile;
  162. import xmlStateMachine.commands.Reset;
  163. import xmlStateMachine.commands.UnloadFile;
  164. public class XMLStateMachine extends StateMachine {
  165. // Possible incoming events to change state
  166. public static const FILE_LOAD_REQUESTED:String = "onFileLoadRequested";
  167. public static const FILE_LOAD_COMPLETE:String = "onFileLoadComplete";
  168. public static const FILE_LOAD_FAILED:String = "onFileLoadFailed";
  169. public static const FILE_UNLOAD_REQUESTED:String = "onFileUnloadRequested";
  170. public static const XML_PARSING_STARTED:String = "onXMLParsingStarted";
  171. public static const XML_PARSING_COMPLETE:String = "onXMLParsingComplete";
  172. public static const XML_PARSING_FAILED:String = "onXMLParsingFailed";
  173. public static const MACHINE_RESET:String = "onMachineReset";
  174. // Possible data field names
  175. public static const PARSED_OBJECT:String = "parsedObject";
  176. public static const FILE_URL:String = "fileURL";
  177. public static const FILE_RAW_XML:String = "fileXML";
  178. // Data fields of this particular state machine
  179. public function get fileURL():String {
  180. return _data[FILE_URL];
  181. }
  182. public function get rawXMLFile():URLLoader {
  183. return _data[FILE_RAW_XML];
  184. }
  185. public function get parsedXMLObject():Object {
  186. return _data[PARSED_OBJECT];
  187. }
  188. public function setNewFile(url:String):void {
  189. _data[FILE_URL] = url;
  190. try {
  191. setInput(FILE_LOAD_REQUESTED);
  192. }catch(e:Error) {
  193. trace("invoking event FILE_LOAD_REQUESTED not " +
  194. "possible in current state: " + currentState.identifier);
  195. }
  196. }
  197. // Definition of the state machine
  198. public function XMLStateMachine() {
  199. super(XMLStates.EMPTY_IDLE);
  200. states = XMLStates;
  201. XMLStates.EMPTY_IDLE.inputCommand = Reset;
  202. XMLStates.EMPTY_IDLE.stateAfterEvent[XMLStates.FILE_REQUESTING] = FILE_LOAD_REQUESTED;
  203. XMLStates.FILE_REQUESTING.inputCommand = RequestFile;
  204. XMLStates.FILE_REQUESTING.stateAfterEvent[XMLStates.FILE_LOADED] = FILE_LOAD_COMPLETE;
  205. XMLStates.FILE_REQUESTING.stateAfterEvent[XMLStates.FILE_LOADERROR] = FILE_LOAD_FAILED;
  206. XMLStates.FILE_LOADED.transient = true;
  207. XMLStates.FILE_LOADED.stateAfterEvent[XMLStates.XML_PARSING] = XML_PARSING_STARTED;
  208. XMLStates.FILE_LOADERROR.transient = true;
  209. XMLStates.FILE_LOADERROR.stateAfterEvent[XMLStates.EMPTY_IDLE] = MACHINE_RESET;
  210. XMLStates.XML_PARSING.inputCommand = ParseXML;
  211. XMLStates.XML_PARSING.stateAfterEvent[XMLStates.XML_PARSED] = XML_PARSING_COMPLETE;
  212. XMLStates.XML_PARSING.stateAfterEvent[XMLStates.XML_REJECTED] = XML_PARSING_FAILED;
  213. XMLStates.XML_PARSED.transient = true;
  214. XMLStates.XML_PARSED.stateAfterEvent[XMLStates.FILE_UNLOADING] = FILE_UNLOAD_REQUESTED;
  215. XMLStates.XML_REJECTED.transient = true;
  216. XMLStates.XML_REJECTED.stateAfterEvent[XMLStates.FILE_UNLOADING] = FILE_UNLOAD_REQUESTED;
  217. XMLStates.FILE_UNLOADING.inputCommand = UnloadFile;
  218. XMLStates.FILE_UNLOADING.stateAfterEvent[XMLStates.EMPTY_IDLE] = MACHINE_RESET;
  219. }
  220. }
  221. }</listing>
  222. * <p>You now see some methods been added to the class, like <code>setNewFile(url:String)</code>. Most
  223. * if these methods make use of the protected property _data, which is also accessible publicly via
  224. * 'data'. _data is a Dictionary and it is passed to every ACommand to share data. In the examples,
  225. * you see lines like: <code>_data[RequestFile.FILE_URL] = url;</code>. It is good practice to use
  226. * constants from the used commands to specify, how to identify a particular data field. In the little
  227. * example here, the command RequuestFile specifies a data field name FILE_URL. So the XMLStateMachine
  228. * uses that name to set the file url.</p>
  229. *
  230. * <p>Why is it done this way? The idea is, that for the command to
  231. * be mostly independent of anything else, it defines the fields, it needs. The state machine, that
  232. * brings everything together, has to know these and populate the fields accordingly. So the dependency
  233. * graph is top-down, which is normally a good approach, because it makes the commands be reusable. Of course,
  234. * you could also decide for doing it differently, the state machine itself doesn't force a particular
  235. * approach. To have the complete picture, let's look at the RequestFile command:</p>
  236. <listing version="3.0">package xmlStateMachine.commands {
  237. import de.ghost23.common.process.ACommand;
  238. import flash.events.Event;
  239. import flash.events.IOErrorEvent;
  240. import flash.net.URLLoader;
  241. import flash.net.URLRequest;
  242. import flash.utils.Dictionary;
  243. import xmlStateMachine.XMLStateMachine;
  244. public class RequestFile extends ACommand {
  245. private var xmlLoader:URLLoader;
  246. private var data:Dictionary;
  247. public function RequestFile() {
  248. super();
  249. }
  250. override public function execute(paramContainer:Dictionary):void {
  251. trace("command RequestFile is running with file: " + paramContainer[XMLStateMachine.FILE_URL]);
  252. data = paramContainer;
  253. if(data[XMLStateMachine.FILE_URL] == null) setResult(FAILURE);
  254. else {
  255. xmlLoader = new URLLoader();
  256. xmlLoader.addEventListener(Event.COMPLETE, onLoadComplete);
  257. xmlLoader.addEventListener(IOErrorEvent.IO_ERROR, onLoadError);
  258. xmlLoader.load(new URLRequest(data[XMLStateMachine.FILE_URL]));
  259. }
  260. }
  261. private function onLoadError(event:IOErrorEvent):void {
  262. xmlLoader.removeEventListener(Event.COMPLETE, onLoadComplete);
  263. xmlLoader = null;
  264. data = null;
  265. setResult(XMLStateMachine.FILE_LOAD_FAILED);
  266. }
  267. private function onLoadComplete(event:Event):void {
  268. data[XMLStateMachine.FILE_RAW_XML] = xmlLoader;
  269. xmlLoader.removeEventListener(Event.COMPLETE, onLoadComplete);
  270. xmlLoader = null;
  271. data = null;
  272. setResult(XMLStateMachine.FILE_LOAD_COMPLETE);
  273. }
  274. }
  275. }</listing>
  276. * <p>For some detailed explanation of ACommand, see its documentation. Here, please
  277. * concentrate on the execute method, which has the parameter 'data', which is the reference
  278. * to the _data field of our state machine.</p>
  279. * @author Sven Busse
  280. * @version 1.0
  281. */
  282. public class StateMachine extends EventDispatcher {
  283. /**
  284. * An enumeration object, that holds all the states, this state machine knows of.
  285. * This can either be a simple object, or (recommended) a class with static properties.
  286. * Using the latter gives you the advantage of having code completion in your editor.
  287. * Such a class would look like this:
  288. * @example
  289. <listing version="3.0">package xmlStateMachine {
  290. import de.ghost23.common.statemachine.State;
  291. public class XMLStates {
  292. // The states
  293. public static const EMPTY_IDLE:State = new State("EMPTY_IDLE");
  294. public static const FILE_REQUESTING:State = new State("FILE_REQUESTING");
  295. public static const FILE_LOADED:State = new State("FILE_LOADED");
  296. public static const FILE_LOADERROR:State = new State("FILE_LOADERROR");
  297. public static const XML_PARSING:State = new State("XML_PARSING");
  298. public static const XML_PARSED:State = new State("XML_PARSED");
  299. public static const XML_REJECTED:State = new State("XML_REJECTED");
  300. public static const FILE_UNLOADING:State = new State("FILE_UNLOADING");
  301. }
  302. }</listing>
  303. */
  304. protected var states:Object;
  305. /**
  306. * The current input command. This variable is null, unless a command is indeed running currently.
  307. */
  308. protected var currentInputCommand:ACommand;
  309. /**
  310. * The state, that has been set by <code>aimForTarget(state:State)</code>. If no target was
  311. * set, or the target has been reached, this is null.
  312. */
  313. protected var targetState:State;
  314. private var _data:Dictionary;
  315. private var targetPath:Vector.<State>;
  316. private var _currentState:State;
  317. private var initialized:Boolean = false;
  318. /**
  319. * The current state, the state machine is in. As long the state machine has not been initialized
  320. * with <code>init()</code>, this is null.
  321. */
  322. public function get currentState():State {
  323. return _currentState;
  324. }
  325. /**
  326. * A general purpose data container, that is shared among the commands, which are part of
  327. * this state machine. This data Dictionary is passed as a reference to each executed command
  328. * via its execute method.
  329. */
  330. public function get data():Dictionary {
  331. return _data;
  332. }
  333. /**
  334. * Constructs a new state machine.
  335. * <p>You will never instantiate StateMachine directly, instead you will subclass ist
  336. * and call the super constructor there. See the examples at the bottom.</p>
  337. * @param startState The state, in which the state machine will begin after <code>init()</code> has
  338. * been called.
  339. */
  340. public function StateMachine(startState:State) {
  341. _currentState = startState;
  342. _data = new Dictionary();
  343. }
  344. /**
  345. * Initializes the state machine. Before init() is called, the state machine has no defined state.
  346. * The reason for init() is, that you are able to add yourself as a listener to the state machine,
  347. * before the state machine actually begins.
  348. */
  349. public function init():void {
  350. initialized = true;
  351. dispatchEvent(
  352. new StateMachineEvent(
  353. StateMachineEvent.NEW_STATE,
  354. null,
  355. _currentState,
  356. null
  357. )
  358. );
  359. executeCurrentInputCommand();
  360. }
  361. /**
  362. * Sets a target state, that the state machine will try to reach withing the bounds of the
  363. * defined state network.
  364. * <p>One of the features of StateMachine is, that it is navigable. With aimForState(targetState:State)
  365. * you can set a state, that you want to reach and the state machine will navigate towards that
  366. * state, if there are forks in the path, which allow for the state machine to make autonomous decisions.
  367. * For example, consider this little state machine definition:</p>
  368. <listing version="3.0">SomeStates.FIRST_STATE.transient = true;
  369. SomeStates.FIRST_STATE.stateAfterEvent[SomeStates.SECOND_STATE] = THIS_HAPPENED;
  370. SomeStates.FIRST_STATE.stateAfterEvent[SomeStates.THIRD_STATE] = THAT_HAPPENED;
  371. </listing>
  372. * FIRST_STATE is transient, means, the state machine will directly try to move to the next state, if FIRST_STATE
  373. * has been entered. The question is, which state should the state machine go to? Let's assume, you have
  374. * called <code>aimForState(SomeStates.THIRD_STATE);</code> before. Then now the state machine will look for the
  375. * shortest path from FIRST_STATE to THIRD_STATE (which in this case is easy) and will automatically invoke
  376. * input calls (setInput()), that lead to the state aimed for. It does this only for transient states, of course,
  377. * because only then it gets the power over the control flow.
  378. * @param targetState The state, you are aiming to reach.
  379. * @see de.ghost23.common.statemachine.StateMachineEvent#AIMED_STATE_REACHED
  380. */
  381. public function aimForState(targetState:State):void {
  382. if(!initialized) throw new Error("StateMachine has not yet been initialized. Use init() first!");
  383. this.targetState = targetState;
  384. targetPath = createTargetStatePath(_currentState, targetState);
  385. }
  386. /**
  387. * Sets a new input signal (which is similar to a transformation route in traditional finite state machine terminology).
  388. * @param eventType The input type. Usually you will define the possible input types as constants in your
  389. * individual state machine. See examples at the bottom.
  390. */
  391. public function setInput(eventType:String):void {
  392. if(!initialized) throw new Error("StateMachine has not yet been initialized. Use init() first!");
  393. var stateForEvent:State;
  394. for(var elmt:* in _currentState.stateAfterEvent) {
  395. if(eventType == _currentState.stateAfterEvent[elmt]) {
  396. stateForEvent = elmt;
  397. break;
  398. }
  399. }
  400. if(stateForEvent == null) {
  401. throw new Error(
  402. "Incoming Event '" + eventType + "' is not an allowed event for the current state '"
  403. + _currentState.identifier + "'");
  404. }
  405. setNewState(stateForEvent);
  406. }
  407. private function executeCurrentInputCommand():void {
  408. if(_currentState.inputCommand != null) {
  409. if(currentInputCommand != null) clearCurrentInputCommand();
  410. currentInputCommand = new _currentState.inputCommand();
  411. currentInputCommand.addEventListener(ProcessEvent.COMPLETED, onInputCommandComplete);
  412. currentInputCommand.addEventListener(ProcessEvent.FAILED, onInputCommandFailed);
  413. currentInputCommand.execute(_data);
  414. }else {
  415. onInputCommandComplete(new ProcessEvent(ProcessEvent.COMPLETED), true);
  416. }
  417. }
  418. private function onInputCommandFailed(event:ProcessEvent):void {
  419. trace("INPUT COMMAND FAILED!!! " + getQualifiedClassName(currentInputCommand));
  420. trace("STATEMACHINE IS IN AN UNDEFINED STATE!!!");
  421. clearCurrentInputCommand();
  422. }
  423. private function onInputCommandComplete(event:ProcessEvent, fake:Boolean = false):void {
  424. if(!fake) clearCurrentInputCommand();
  425. if(_currentState.transient) {
  426. if(targetPath != null && targetPath.length > 1) setInput(_currentState.stateAfterEvent[targetPath[1]]);
  427. else {
  428. var stateAfterEventList:Vector.<String> = new Vector.<String>();
  429. for(var elmt:* in _currentState.stateAfterEvent) {
  430. stateAfterEventList.push(_currentState.stateAfterEvent[elmt]);
  431. }
  432. if(stateAfterEventList.length == 1) setInput(stateAfterEventList[0]);
  433. }
  434. }else if(event.resultCode != null) {
  435. setInput(event.resultCode);
  436. }
  437. }
  438. private function clearCurrentInputCommand():void {
  439. currentInputCommand.removeEventListener(ProcessEvent.COMPLETED, onInputCommandComplete);
  440. currentInputCommand.removeEventListener(ProcessEvent.FAILED, onInputCommandFailed);
  441. currentInputCommand = null;
  442. }
  443. private function setNewState(newState:State):void {
  444. var previousState:State = _currentState;
  445. _currentState = newState;
  446. if(targetPath != null && _currentState == targetState) {
  447. targetPath = null;
  448. targetState = null;
  449. dispatchEvent(
  450. new StateMachineEvent(
  451. StateMachineEvent.AIMED_STATE_REACHED,
  452. previousState,
  453. _currentState,
  454. _currentState
  455. )
  456. );
  457. }else if(targetPath != null && targetPath.length > 1) {
  458. targetPath.shift();
  459. if(_currentState != targetPath[0]) {
  460. targetPath = createTargetStatePath(_currentState, targetState);
  461. }
  462. }
  463. dispatchEvent(
  464. new StateMachineEvent(
  465. StateMachineEvent.NEW_STATE,
  466. previousState,
  467. _currentState,
  468. targetState
  469. )
  470. );
  471. executeCurrentInputCommand();
  472. }
  473. /**
  474. * Searches a path from the current state to a specified target state,
  475. * using the depth-first algorithm.
  476. * @param startState
  477. */
  478. private function createTargetStatePath(startState:State, targetState:State):Vector.<State> {
  479. var pathToTarget:Vector.<State> = new Vector.<State>;
  480. var currentState:State = targetState;
  481. var predecessors:Dictionary = dijkstra(startState);
  482. while(predecessors[currentState] != null) { // Der Vorg???nger des Startknotens ist null
  483. pathToTarget.unshift(predecessors[currentState]);
  484. currentState = predecessors[currentState];
  485. }
  486. return pathToTarget;
  487. }
  488. private function buildEnumObjectFromClass(sourceObject:*):Object {
  489. var result:Object = new Object();
  490. // determine if o is a class instance or a plain object
  491. var classInfo:XML = describeType( sourceObject );
  492. // Loop over all of the variables and accessors in the class and
  493. // serialize them along with their values.
  494. for each (var v:XML in classInfo..*.(
  495. name() == "variable" || name() == "constant"
  496. )) {
  497. // If [Transient] metadata exists, then we should skip
  498. if (v.@name == "prototype" || (v.metadata && v.metadata.( @name == "Transient" ).length() > 0)) {
  499. continue;
  500. }
  501. result[v.@name] = sourceObject[v.@name];
  502. }
  503. return result;
  504. }
  505. // --------------- Dijkstra implementation for finding the shortest path from one state to another.
  506. private function dijkstra(startState:State):Dictionary {
  507. var distances:Dictionary = new Dictionary();
  508. var predecessors:Dictionary = new Dictionary();
  509. var stateList:Vector.<State> = new Vector.<State>();
  510. var statesEnum:Object = buildEnumObjectFromClass(states);
  511. for each(var item:Object in statesEnum) {
  512. distances[item] = Number.POSITIVE_INFINITY;
  513. predecessors[item] = null;
  514. stateList.push(item);
  515. }
  516. distances[startState] = 0;
  517. var stateWithSmallestDistance:State;
  518. var followersOfState:Dictionary;
  519. /* a negative number, if x should appear before y in the sorted sequence
  520. 0, if x equals y
  521. a positive number, if x should appear after y in the sorted sequence
  522. */
  523. var sortForSmallestDistance:Function = function(x:State, y:State):Number {
  524. return distances[x] - distances[y];
  525. };
  526. while(stateList.length > 0) { // Der eigentliche Algorithmus
  527. stateWithSmallestDistance = Vector.<State>(stateList.sort(sortForSmallestDistance))[0];
  528. // An alternative within dijkstra. Since we have found our target already, we can abort the rest.
  529. if(stateWithSmallestDistance === null) return predecessors;
  530. stateList.splice(stateList.indexOf(stateWithSmallestDistance),1);
  531. followersOfState = stateWithSmallestDistance.stateAfterEvent;
  532. for(var elmt:* in followersOfState) {
  533. if(stateList.indexOf(elmt) > -1){
  534. updateDistance(stateWithSmallestDistance, elmt, distances, predecessors);
  535. }
  536. }
  537. }
  538. return predecessors;
  539. }
  540. private function updateDistance(stateWithSmallestDistance:State, neighbour:State, distance:Dictionary, predecessor:Dictionary):void {
  541. // 1 is the weight of the line between stateWithSmallestDistance and neighbour
  542. // We do not use weights, that's why we give 1 here in general.
  543. var alternative:Number = distance[stateWithSmallestDistance] + 1;
  544. if(alternative < distance[neighbour]) {
  545. distance[neighbour] = alternative;
  546. predecessor[neighbour] = stateWithSmallestDistance;
  547. }
  548. }
  549. }
  550. }