OLD | NEW |
| (Empty) |
1 // Copyright 2011 (c) The Native Client Authors. All rights reserved. | |
2 // Use of this source code is governed by a BSD-style license that can be | |
3 // found in the LICENSE file. | |
4 | |
5 /** | |
6 * @file | |
7 * The life Application object. This object instantiates a Dragger object and | |
8 * connects it to the element named @a life_module. | |
9 */ | |
10 | |
11 goog.provide('life.Application'); | |
12 | |
13 goog.require('goog.Disposable'); | |
14 goog.require('goog.array'); | |
15 goog.require('goog.events.EventType'); | |
16 goog.require('goog.style'); | |
17 | |
18 goog.require('life.controllers.ViewController'); | |
19 goog.require('stamp.StampPanel'); | |
20 | |
21 /** | |
22 * Constructor for the Application class. Use the run() method to populate | |
23 * the object with controllers and wire up the events. | |
24 * @constructor | |
25 * @extends {goog.Disposable} | |
26 */ | |
27 life.Application = function() { | |
28 goog.Disposable.call(this); | |
29 } | |
30 goog.inherits(life.Application, goog.Disposable); | |
31 | |
32 /** | |
33 * The view controller for the application. A DOM element that encapsulates | |
34 * the grayskull plugin; this is allocated at run time. Connects to the | |
35 * element with id life.Application.DomIds_.VIEW. | |
36 * @type {life.ViewController} | |
37 * @private | |
38 */ | |
39 life.Application.prototype.viewController_ = null; | |
40 | |
41 /** | |
42 * The automaton rule string. It is expressed as SSS/BB, where S is the | |
43 * neighbour count that keeps a cell alive, and B is the count that causes a | |
44 * cell to become alive. See the .LIF 1.05 section in | |
45 * http://psoup.math.wisc.edu/mcell/ca_files_formats.html for more info. | |
46 * @default 23/3 the "Normal" Conway rules. | |
47 * @type {Object.<Array>} | |
48 * @private | |
49 */ | |
50 life.Application.prototype.automatonRules_ = { | |
51 birthRule: [3], | |
52 keepAliveRule: [2, 3] | |
53 }; | |
54 | |
55 /** | |
56 * The id of the current stamp. Defaults to the DEFAULT_STAMP_ID. | |
57 * @type {string} | |
58 * @private | |
59 */ | |
60 life.Application.prototype.currentStampId_ = | |
61 life.controllers.ViewController.DEFAULT_STAMP_ID; | |
62 | |
63 /** | |
64 * The ids used for elements in the DOM. The Life Application expects these | |
65 * elements to exist. | |
66 * @enum {string} | |
67 * @private | |
68 */ | |
69 life.Application.DomIds_ = { | |
70 BIRTH_FIELD: 'birth_field', // Text field with the birth rule string. | |
71 CLEAR_BUTTON: 'clear_button', // The clear button element. | |
72 KEEP_ALIVE_FIELD: 'keep_alive_field', // Keep alive rule string. | |
73 MODULE: 'life_module', // The <embed> element representing the NaCl module. | |
74 PLAY_MODE_SELECT: 'play_mode_select', // The <select> element for play mode. | |
75 PLAY_BUTTON: 'play_button', // The play button element. | |
76 SOUND_SELECT: 'sound_select', // The <select> element for the stamp sound. | |
77 VIEW: 'life_view' // The <div> containing the NaCl element. | |
78 }; | |
79 | |
80 /** | |
81 * The Run/Stop button attribute labels. These are used to determine the state | |
82 * and label of the button. | |
83 * @enum {string} | |
84 * @private | |
85 */ | |
86 life.Application.PlayButtonAttributes_ = { | |
87 ALT_IMAGE: 'altimage', // Image to display in the "on" state. | |
88 STATE: 'state' // The button's state. | |
89 }; | |
90 | |
91 /** | |
92 * Override of disposeInternal() to dispose of retained objects. | |
93 * @override | |
94 */ | |
95 life.Application.prototype.disposeInternal = function() { | |
96 this.terminate(); | |
97 life.Application.superClass_.disposeInternal.call(this); | |
98 } | |
99 | |
100 /** | |
101 * Called by the module loading function once the module has been loaded. Wire | |
102 * up a Dragger object to @a this. | |
103 * @param {?String} opt_naclModuleId The id of an <EMBED> element which | |
104 * contains the NaCl module. If unspecified, defaults to DomIds_.MODULE. | |
105 * If the DOM element doesn't exist, the program asserts and exits. | |
106 */ | |
107 life.Application.prototype.moduleDidLoad = function(opt_naclModuleId) { | |
108 // Listen for 'unload' in order to terminate cleanly. | |
109 goog.events.listen(window, goog.events.EventType.UNLOAD, this.terminate); | |
110 | |
111 // Set up the stamp editor. | |
112 var stampEditorButton = | |
113 document.getElementById(stamp.StampPanel.DomIds.STAMP_EDITOR_BUTTON); | |
114 this.stampPanel_ = new stamp.StampPanel(stampEditorButton); | |
115 var stampEditorButtons = { | |
116 mainPanel: | |
117 document.getElementById(stamp.StampPanel.DomIds.STAMP_EDITOR_PANEL), | |
118 editorContainer: | |
119 document.getElementById(stamp.StampPanel.DomIds.STAMP_EDITOR_CONTAINER), | |
120 addColumnButton: | |
121 document.getElementById(stamp.StampPanel.DomIds.ADD_COLUMN_BUTTON), | |
122 removeColumnButton: | |
123 document.getElementById(stamp.StampPanel.DomIds.REMOVE_COLUMN_BUTTON), | |
124 addRowButton: | |
125 document.getElementById(stamp.StampPanel.DomIds.ADD_ROW_BUTTON), | |
126 removeRowButton: | |
127 document.getElementById(stamp.StampPanel.DomIds.REMOVE_ROW_BUTTON), | |
128 cancelButton: | |
129 document.getElementById(stamp.StampPanel.DomIds.CANCEL_BUTTON), | |
130 okButton: document.getElementById(stamp.StampPanel.DomIds.OK_BUTTON) | |
131 }; | |
132 this.stampPanel_.makeStampEditorPanel(stampEditorButtons); | |
133 | |
134 // When the stamp editor panel is about to open, set its stamp to the | |
135 // current stamp. | |
136 goog.events.listen(this.stampPanel_, stamp.StampPanel.Events.PANEL_WILL_OPEN, | |
137 this.handlePanelWillOpen_, false, this); | |
138 | |
139 // Listen for the "PANEL_DID_SAVE" event posted by the stamp editor. | |
140 goog.events.listen(this.stampPanel_, stamp.StampPanel.Events.PANEL_DID_SAVE, | |
141 this.handlePanelDidSave_, false, this); | |
142 | |
143 // Set up the view controller, it contains the NaCl module. | |
144 var naclModuleId = opt_naclModuleId || life.Application.DomIds_.MODULE; | |
145 this.viewController_ = new life.controllers.ViewController( | |
146 document.getElementById(naclModuleId)); | |
147 this.viewController_.setAutomatonRules(this.automatonRules_); | |
148 // Initialize the module with the default stamp. | |
149 this.currentStampId_ = this.viewController_.DEFAULT_STAMP_ID; | |
150 this.viewController_.selectStamp(this.currentStampId_); | |
151 | |
152 this.viewController_.setStampSoundUrl('sounds/boing_x.wav'); | |
153 | |
154 // Wire up the various controls. | |
155 var playModeSelect = | |
156 document.getElementById(life.Application.DomIds_.PLAY_MODE_SELECT); | |
157 if (playModeSelect) { | |
158 goog.events.listen(playModeSelect, goog.events.EventType.CHANGE, | |
159 this.selectPlayMode, false, this); | |
160 } | |
161 | |
162 var soundSelect = | |
163 document.getElementById(life.Application.DomIds_.SOUND_SELECT); | |
164 if (playModeSelect) { | |
165 goog.events.listen(soundSelect, goog.events.EventType.CHANGE, | |
166 this.selectSound, false, this); | |
167 } | |
168 | |
169 var clearButton = | |
170 document.getElementById(life.Application.DomIds_.CLEAR_BUTTON); | |
171 if (clearButton) { | |
172 goog.events.listen(clearButton, goog.events.EventType.CLICK, | |
173 this.clear, false, this); | |
174 } | |
175 | |
176 var playButton = | |
177 document.getElementById(life.Application.DomIds_.PLAY_BUTTON); | |
178 if (playButton) { | |
179 goog.events.listen(playButton, goog.events.EventType.CLICK, | |
180 this.togglePlayButton, false, this); | |
181 } | |
182 | |
183 var birthField = | |
184 document.getElementById(life.Application.DomIds_.BIRTH_FIELD); | |
185 if (birthField) { | |
186 goog.events.listen(birthField, goog.events.EventType.CHANGE, | |
187 this.updateBirthRule, false, this); | |
188 } | |
189 | |
190 var keepAliveField = | |
191 document.getElementById(life.Application.DomIds_.KEEP_ALIVE_FIELD); | |
192 if (keepAliveField) { | |
193 goog.events.listen(keepAliveField, goog.events.EventType.CHANGE, | |
194 this.updateKeepAliveRule, false, this); | |
195 } | |
196 } | |
197 | |
198 /** | |
199 * Change the play mode. | |
200 * @param {!goog.events.Event} changeEvent The CHANGE event that triggered this | |
201 * handler. | |
202 */ | |
203 life.Application.prototype.selectPlayMode = function(changeEvent) { | |
204 changeEvent.stopPropagation(); | |
205 this.viewController_.setPlayMode(changeEvent.target.value); | |
206 } | |
207 | |
208 /** | |
209 * Change the stamp sound. | |
210 * @param {!goog.events.Event} changeEvent The CHANGE event that triggered this | |
211 * handler. | |
212 */ | |
213 life.Application.prototype.selectSound = function(changeEvent) { | |
214 changeEvent.stopPropagation(); | |
215 this.viewController_.setStampSoundUrl(changeEvent.target.value); | |
216 } | |
217 | |
218 /** | |
219 * Toggle the simulation. | |
220 * @param {!goog.events.Event} clickEvent The CLICK event that triggered this | |
221 * handler. | |
222 */ | |
223 life.Application.prototype.togglePlayButton = function(clickEvent) { | |
224 clickEvent.stopPropagation(); | |
225 var button = clickEvent.target; | |
226 var buttonImage = button.style.backgroundImage; | |
227 var altImage = button.getAttribute( | |
228 life.Application.PlayButtonAttributes_.ALT_IMAGE); | |
229 var state = button.getAttribute( | |
230 life.Application.PlayButtonAttributes_.STATE); | |
231 // Switch the inner and alternate labels. | |
232 goog.style.setStyle(button, { 'backgroundImage': altImage }); | |
233 button.setAttribute(life.Application.PlayButtonAttributes_.ALT_IMAGE, | |
234 buttonImage); | |
235 if (state == 'off') { | |
236 button.setAttribute( | |
237 life.Application.PlayButtonAttributes_.STATE, 'on'); | |
238 this.viewController_.run(); | |
239 } else { | |
240 button.setAttribute( | |
241 life.Application.PlayButtonAttributes_.STATE, 'off'); | |
242 this.viewController_.stop(); | |
243 } | |
244 } | |
245 | |
246 /** | |
247 * Handle the "panel will open" event: set the stamp in the stamp editor to | |
248 * the current stamp. | |
249 * @param {!goog.events.Event} event The event that triggered this handler. | |
250 * @private | |
251 */ | |
252 life.Application.prototype.handlePanelWillOpen_ = function(event) { | |
253 event.stopPropagation(); | |
254 var currentStamp = | |
255 this.viewController_.stampWithId(this.currentStampId_); | |
256 if (currentStamp) | |
257 this.stampPanel_.setStampFromString(currentStamp); | |
258 return true; | |
259 } | |
260 | |
261 /** | |
262 * Handle the "panel did save" event: grab the stamp from the stamp editor, | |
263 * add it to the dictionary of stamps and set it as the current stamp. | |
264 * @param {!goog.events.Event} event The event that triggered this handler. | |
265 * @private | |
266 */ | |
267 life.Application.prototype.handlePanelDidSave_ = function(event) { | |
268 event.stopPropagation(); | |
269 var stampString = this.stampPanel_.getStampAsString(); | |
270 this.viewController_.addStampWithId(stampString, this.currentStampId_); | |
271 this.viewController_.selectStamp(this.currentStampId_); | |
272 } | |
273 | |
274 /** | |
275 * Clear the current simulation. | |
276 * @param {!goog.events.Event} clickEvent The CLICK event that triggered this | |
277 */ | |
278 life.Application.prototype.clear = function(clickEvent) { | |
279 clickEvent.stopPropagation(); | |
280 this.viewController_.clear(); | |
281 } | |
282 | |
283 /** | |
284 * Read the text input and change it from a comma-separated list into a string | |
285 * of the form BB, where B is a digit in [0..9] that represents the neighbour | |
286 * count that causes a cell to come to life. | |
287 * @param {!goog.events.Event} changeEvent The CHANGE event that triggered this | |
288 * handler. | |
289 */ | |
290 life.Application.prototype.updateBirthRule = function(changeEvent) { | |
291 changeEvent.stopPropagation(); | |
292 var birthRule = this.parseAutomatonRule_(changeEvent.target.value); | |
293 // Put the sanitized version of the rule string back into the text field. | |
294 changeEvent.target.value = birthRule.join(','); | |
295 // Make the new rule string and tell the NaCl module. | |
296 this.automatonRules_.birthRule = birthRule; | |
297 this.viewController_.setAutomatonRules(this.automatonRules_); | |
298 } | |
299 | |
300 /** | |
301 * Read the text input and change it from a comma-separated list into a string | |
302 * of the form SSS, where S is a digit in [0..9] that represents the neighbour | |
303 * count that allows a cell to stay alive. | |
304 * @param {!goog.events.Event} changeEvent The CHANGE event that triggered this | |
305 * handler. | |
306 */ | |
307 life.Application.prototype.updateKeepAliveRule = function(changeEvent) { | |
308 changeEvent.stopPropagation(); | |
309 var keepAliveRule = this.parseAutomatonRule_(changeEvent.target.value); | |
310 // Put the sanitized version of the rule string back into the text field. | |
311 changeEvent.target.value = keepAliveRule.join(','); | |
312 // Make the new rule string and tell the NaCl module. | |
313 this.automatonRules_.keepAliveRule = keepAliveRule; | |
314 this.viewController_.setAutomatonRules(this.automatonRules_); | |
315 } | |
316 | |
317 /** | |
318 * Parse a user-input string representing an automaton rule into an array of | |
319 * neighbour counts. |ruleString| is expected to be a comma-separated string | |
320 * of integers in range [0..9]. This routine attempts to sanitize non- | |
321 * conforming values by clipping (numbers outside [0..9] are clipped), and | |
322 * replaces non-numberic input with 0. The resulting array is sorted, and each | |
323 * value is unique. For example: '1,3,2,2' will produce [1, 2, 3]. | |
324 * @param {!string} ruleString The user-input string. | |
325 * @return {Array.<number>} An array of neighbour counts that can be used to | |
326 * create an automaton rule. | |
327 * @private | |
328 */ | |
329 life.Application.prototype.parseAutomatonRule_ = function(ruleString) { | |
330 var rule = ruleString.split(','); | |
331 | |
332 /** | |
333 * Helper function to parse a single rule element: trim off any leading or | |
334 * trailing white-space, then attempt to convert the resulting string into | |
335 * an integer. Clip the integer to range [0..8]. Replace the element in | |
336 * |array| with the resulting number. Note: non-numeric values are replaced | |
337 * with 0. | |
338 * @param {string} ruleString The string to parse. | |
339 * @param {number} index The index of the element in the original array. | |
340 * @param {Array} ruleArray The array of rules. | |
341 */ | |
342 function parseOneRule(ruleString, index, ruleArray) { | |
343 var neighbourCount = parseInt(ruleString.trim()); | |
344 if (isNaN(neighbourCount) || neighbourCount < 0) | |
345 neighbourCount = 0; | |
346 if (neighbourCount > 8) | |
347 neighbourCount = 8; | |
348 ruleArray[index] = neighbourCount; | |
349 } | |
350 | |
351 // Each rule has to be an integer in [0..8] | |
352 goog.array.forEach(rule, parseOneRule, this); | |
353 // Sort the rules numerically. | |
354 rule.sort(function(a, b) { return a - b; }); | |
355 goog.array.removeDuplicates(rule); | |
356 return rule; | |
357 } | |
358 | |
359 /** | |
360 * Asserts that cond is true; issues an alert and throws an Error otherwise. | |
361 * @param {bool} cond The condition. | |
362 * @param {String} message The error message issued if cond is false. | |
363 */ | |
364 life.Application.prototype.assert = function(cond, message) { | |
365 if (!cond) { | |
366 message = "Assertion failed: " + message; | |
367 alert(message); | |
368 throw new Error(message); | |
369 } | |
370 } | |
371 | |
372 /** | |
373 * The run() method starts and 'runs' the application. An <EMBED> tag is | |
374 * injected into the <DIV> element |opt_viewDivName| which causes the NaCl | |
375 * module to be loaded. Once loaded, the moduleDidLoad() method is called via | |
376 * a 'load' event handler that is attached to the <DIV> element. | |
377 * @param {?String} opt_viewDivName The id of a DOM element in which to | |
378 * embed the NaCl module. If unspecified, defaults to DomIds_.VIEW. The | |
379 * DOM element must exist. | |
380 */ | |
381 life.Application.prototype.run = function(opt_viewDivName) { | |
382 var viewDivName = opt_viewDivName || life.Application.DomIds_.VIEW; | |
383 var viewDiv = document.getElementById(viewDivName); | |
384 this.assert(viewDiv, "Missing DOM element '" + viewDivName + "'"); | |
385 | |
386 // A small handler for the 'load' event. It stops propagation of the 'load' | |
387 // event and then calls moduleDidLoad(). The handler is bound to |this| so | |
388 // that the calling context of the closure makes |this| point to this | |
389 // instance of the life.Applicaiton object. | |
390 var loadEventHandler = function(loadEvent) { | |
391 this.moduleDidLoad(life.Application.DomIds_.MODULE); | |
392 } | |
393 | |
394 // Note that the <EMBED> element is wrapped inside a <DIV>, which has a 'load' | |
395 // event listener attached. This method is used instead of attaching the | |
396 // 'load' event listener directly to the <EMBED> element to ensure that the | |
397 // listener is active before the NaCl module 'load' event fires. | |
398 viewDiv.addEventListener('load', goog.bind(loadEventHandler, this), true); | |
399 | |
400 var viewSize = goog.style.getSize(viewDiv); | |
401 viewDiv.innerHTML = '<embed id="' + life.Application.DomIds_.MODULE + '" ' + | |
402 ' class="autosize"' + | |
403 ' width=' + viewSize.width + | |
404 ' height=' + viewSize.height + | |
405 ' src="life.nmf"' + | |
406 ' type="application/x-nacl" />' | |
407 } | |
408 | |
409 /** | |
410 * Shut down the application instance. This unhooks all the event listeners | |
411 * and deletes the objects created in moduleDidLoad(). | |
412 */ | |
413 life.Application.prototype.terminate = function() { | |
414 goog.events.removeAll(); | |
415 this.viewController_ = null; | |
416 } | |
417 | |
418 /** | |
419 * Extend the String class to trim whitespace. | |
420 * @return {string} the original string with leading and trailing whitespace | |
421 * removed. | |
422 */ | |
423 String.prototype.trim = function () { | |
424 return this.replace(/^\s*/, '').replace(/\s*$/, ''); | |
425 } | |
OLD | NEW |