gui_utils.js 21 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701
  1. /* This file is part of AToMPM - A Tool for Multi-Paradigm Modelling
  2. * Copyright 2011 by the AToMPM team and licensed under the LGPL
  3. * See COPYING.lesser and README.md in the root of this project for full details
  4. */
  5. GUIUtils = function(){
  6. /**
  7. * This is the old getElementById function
  8. */
  9. this.$$ = function( id ){
  10. return document.getElementById( id );
  11. };
  12. /**
  13. * Converts from page centric X coordinates to canvas centric X coordinates
  14. */
  15. this.convertToCanvasX = function(event){
  16. //experimental property
  17. return event.offsetX;
  18. //breaks when page is scrolled
  19. //return event.pageX + $('#div_container').scrollLeft() - $('#contentDiv').offset().left;
  20. };
  21. /**
  22. * Converts from page centric Y coordinates to canvas centric Y coordinates
  23. */
  24. this.convertToCanvasY = function(event){
  25. //experimental property
  26. return event.offsetY;
  27. //breaks when page is scrolled
  28. //return event.pageY + $('#div_container').scrollTop() - $('#contentDiv').offset().top;
  29. };
  30. /**
  31. * Disables the dock bar
  32. */
  33. this.disableDock = function(){
  34. $('#div_dock').attr('class', 'dock disabled_dock');
  35. };
  36. /**
  37. * Enables the dock bar
  38. */
  39. this.enableDock = function(){
  40. $('#div_dock').attr('class', 'dock');
  41. };
  42. /**
  43. * Constructs and returns a checked checkbox
  44. */
  45. this.getCheckbox = function(checked){
  46. var cb = $('<input>');
  47. cb.attr("type", 'checkbox');
  48. cb.prop("checked", checked);
  49. return cb;
  50. };
  51. // TODO: replace the bundled function with an actual object generation. The
  52. // current method is sloppy.
  53. /**
  54. * Constructs and returns an input field given an attribute's type and value,
  55. * bundled in the return value is a function to retrieve the input fields
  56. * content as well as its initial value
  57. *
  58. * For Maps and Lists, onfocus/onblur toggle a default entry to be shown.
  59. */
  60. this.getInputField = function (type,value){
  61. /* recursively expand specialTypes, if any */
  62. function explodeType(t)
  63. {
  64. var exploded = __specialTypes[t] || t;
  65. while( exploded.match(/\$/) )
  66. exploded = exploded.replace(
  67. /(\$.+?)([,\]>])/g,
  68. function(s,p1,p2){return __specialTypes[p1]+p2;});
  69. return exploded;
  70. }
  71. /* return a default value for the given exploded type string */
  72. function defaultEntry(et)
  73. {
  74. if( et == 'string' || et == 'code' )
  75. return "";
  76. else if( et == 'int' )
  77. return 0;
  78. else if( et == 'boolean' )
  79. return true;
  80. else if( et == 'double' )
  81. return 0.0;
  82. else if( et.match(/^ENUM/) )
  83. return et;
  84. else if( (matches=et.match(/^list<(.*)>$/)) )
  85. return [defaultEntry(matches[1])];
  86. else if( (matches=et.match(/^map<\[(.*?)\],\[(.*)\]>$/)) )
  87. {
  88. var m = {},
  89. keys = matches[1].split(','),
  90. types = [],
  91. depth = 0,
  92. index = 0;
  93. for( var i=0; i<matches[2].length; i++ )
  94. {
  95. if( matches[2].charAt(i) == '(' ||
  96. matches[2].charAt(i) == '<' )
  97. depth++;
  98. else if( matches[2].charAt(i) == ')' ||
  99. matches[2].charAt(i) == '>' )
  100. depth--;
  101. if( matches[2].charAt(i) == ',' && depth == 0 )
  102. {
  103. types.push( matches[2].substring(index,i) );
  104. index = i+1;
  105. }
  106. }
  107. types.push( matches[2].substring(index) );
  108. for( var i=0; i<keys.length; i++ )
  109. m[keys[i]] = defaultEntry(types[i]);
  110. return m;
  111. }
  112. else if( (matches=et.match(/^map<(.*),(.*)>$/)) )
  113. {
  114. var m = {};
  115. m[defaultEntry(matches[1])] = defaultEntry(matches[2]);
  116. return m;
  117. }
  118. }
  119. if( type == 'code' )
  120. var input = GUIUtils.getTextInput(utils.jsond(value,'\n')),
  121. getinput = function(_){return _.val();};
  122. else if( type.match(/^map/) ||
  123. type.match(/^list/) )
  124. {
  125. var input = GUIUtils.getTextInput(
  126. utils.jsond(utils.jsons(value,undefined,' '))),
  127. getinput = function(_){
  128. return utils.jsonp(utils.jsone(_.val()));
  129. };
  130. var de = defaultEntry( explodeType(type) ),
  131. isMap = type.match(/^map/),
  132. matches = undefined;
  133. input.focus(
  134. (isMap ?
  135. function()
  136. {
  137. var newVal = utils.mergeDicts([getinput(input),de]);
  138. input.val( utils.jsond(utils.jsons(newVal,undefined,' ')));
  139. } :
  140. function()
  141. {
  142. var newVal = getinput(input).concat(de);
  143. input.val( utils.jsond(utils.jsons(newVal,undefined,' ')));
  144. }));
  145. input.blur(
  146. (isMap ?
  147. function()
  148. {
  149. var val = getinput(input);
  150. utils.splitDict(val,utils.keys(de));
  151. input.val( utils.jsond(utils.jsons(val,undefined,' ')));
  152. } :
  153. function()
  154. {
  155. var val = getinput(input);
  156. if( utils.jsons(utils.tail(val)) == utils.jsons(de[0]) )
  157. val.pop();
  158. input.val( utils.jsond(utils.jsons(val,undefined,' ')));
  159. }));
  160. }
  161. else if( type.match(/^ENUM/) )
  162. {
  163. var vals = type.match(/^ENUM\((.*)\)$/)[1],
  164. input = GUIUtils.getSelector(vals.split(','),false,[value]),
  165. getinput =
  166. function(_){return HttpUtils.getSelectorSelection(_)[0];};
  167. }
  168. else if( type.match(/^boolean$/) )
  169. {
  170. var input = GUIUtils.getCheckbox(value),
  171. getinput =
  172. function(_){return _.prop("checked");};
  173. }
  174. else if( type.match(/^\$/) )
  175. return GUIUtils.getInputField(__specialTypes[type],value);
  176. else if ((matches = type.match("^file<(.*)>"))) {
  177. var input = GUIUtils.getFileInput(value,matches[1],"code_style string_input",1),
  178. getinput = function(_){return _.val();};
  179. }
  180. else
  181. var input = GUIUtils.getTextInput(value,"code_style string_input",1),
  182. getinput = (type == 'string' ?
  183. function(_){return _.val();} :
  184. function(_){return utils.jsonp(_.val());});
  185. input.title = explodeType(type);
  186. return {'input':input, 'getinput':getinput, 'oldVal':getinput(input)};
  187. };
  188. /**
  189. * Constructs and returns a <select> element with the choices list converted into
  190. * a list of <option> elements.
  191. *
  192. * @param choices - the choices for the <select> element
  193. * @param multipleChoice - if true, allows for multiple options to be selected
  194. * @param defaultSelection - sets the default selection for the list
  195. * @param numVisibleOptions - sets the number of visible options
  196. */
  197. this.getSelector = function(choices,multipleChoice,defaultSelection,numVisibleOptions){
  198. var select = $('<select>');
  199. select.attr("class", 'default_style');
  200. select.attr("size", Math.min(choices.length,numVisibleOptions || __MAX_SELECT_OPTIONS));
  201. select.attr("multiple", multipleChoice);
  202. choices.forEach(
  203. function(choice)
  204. {
  205. var option = $('<option>');
  206. option.val( choice );
  207. option.html( choice );
  208. option.attr('id', "choice_" + choice);
  209. select.append(option);
  210. if( defaultSelection != undefined &&
  211. utils.contains(defaultSelection,choice) )
  212. option.prop("selected", true);
  213. });
  214. return select;
  215. };
  216. /* constructs an INPUT given some text and a CSS class */
  217. /**
  218. * Constructs an <input> element
  219. *
  220. * @param text - the default text for the input element
  221. * @param className - the default class name for the input element
  222. * @param width - the default width of the element. If this is omitted
  223. * then the <input> defaults to 400px wide.
  224. */
  225. this.getStringInput = function(text,className,width){
  226. var input = $('<input>');
  227. input.attr("type", 'text');
  228. input.attr("class", className || 'default_style');
  229. input.val( text );
  230. input.css("width", width || '400px');
  231. return input;
  232. };
  233. this.getFileInput = function(code,pattern,className,rows){
  234. var string_input = this.getTextInput(code, className, rows);
  235. var extra_el = $('<button>');
  236. extra_el.attr("width", 16);
  237. extra_el.attr("height", 16);
  238. extra_el.html("...");
  239. extra_el.click(function(event) {
  240. var options = {'extensions':[pattern],
  241. 'multipleChoice':false,
  242. 'manualInput':false,
  243. 'title':'choose a rule model',
  244. 'startDir':'model'},
  245. callback =
  246. function(fnames)
  247. {
  248. string_input.val(fnames[0]);
  249. };
  250. WindowManagement.openDialog(_FILE_BROWSER,options,callback);
  251. event.stopPropagation();
  252. event.preventDefault();
  253. });
  254. string_input.extra_el = extra_el;
  255. return string_input;
  256. };
  257. /**
  258. * Constructs a <textarea> element. In this element, Alt + Right Arrow
  259. * is treated as a tab.
  260. *
  261. * @param code - the default code for this text area
  262. * @param className - the default class name for the <textarea>
  263. * @param rows - the default number of rows for this <textarea>
  264. */
  265. this.getTextInput = function(code,className,rows){
  266. var input = $('<textarea>');
  267. input.attr("cols", 80);
  268. rows = rows || 7;
  269. input.attr("rows", (rows || 7));
  270. input.val(code);
  271. input.attr("class", className || 'code_style');
  272. input.keydown( function(event) {
  273. if( event.keyCode == KEY_RIGHT_ARROW /* This is to simulate a tab press */ ) {
  274. if( currentKeys[ KEY_ALT ] == 1 && currentKeys[ KEY_CTRL ] != 1){
  275. var cursorPos = event.target.selectionStart,
  276. lineStart = input.val().lastIndexOf('\n',cursorPos-1)+1,
  277. tabBy = __TAB_WIDTH - (cursorPos-lineStart)%__TAB_WIDTH,
  278. tab = '';
  279. for(var i=0; i<tabBy; i++) tab += ' ';
  280. input.val(
  281. input.val().substring(0,cursorPos)+tab+
  282. input.val().substring(cursorPos));
  283. input.get(0).setSelectionRange(cursorPos+tabBy,cursorPos+tabBy);
  284. return true;
  285. }
  286. return true;
  287. }
  288. else if (event.keyCode == KEY_ENTER) {
  289. //for single row fields, don't create a new line
  290. if (rows == 1) {
  291. event.preventDefault();
  292. }
  293. }
  294. });
  295. input.keyup(function (event) {
  296. if (event.keyCode == KEY_ENTER) {
  297. //don't send the enter key for multi-line fields
  298. //this closes the window
  299. if (rows > 1) {
  300. event.stopPropagation();
  301. }
  302. }
  303. });
  304. return input;
  305. };
  306. /**
  307. * Constructs a <span> element.
  308. *
  309. * @param text - the default text to be displayed
  310. * @param className - the default class name
  311. */
  312. this.getTextSpan = function(text,className)
  313. {
  314. var span = $('<span>');
  315. span.html( text.replace(/\n/g,'<br/>') );
  316. span.attr("class", className || 'default_style');
  317. return span;
  318. };
  319. /**
  320. * Finds and removes the specified toolbar, if present
  321. *
  322. * @param tb - the toolbar to be removed
  323. */
  324. this.removeToolbar = function(tb){
  325. tb_fixed = tb.replace(/\//g, "\\/");
  326. if( $('#div_toolbar_'+tb_fixed) )
  327. {
  328. //Find the toolbar in the dock bar and remove it
  329. //from the DOM
  330. $("#div_dock").children("div").each( function() {
  331. if( this.id == "div_toolbar_" + tb ){
  332. $(this).remove();
  333. }
  334. });
  335. // Now delete it from the list of loaded toolbars
  336. delete __loadedToolbars[tb];
  337. }
  338. };
  339. /**
  340. * Throw out the current canvas, replace it with a new one.
  341. * 1. Unselect any current selection
  342. * 2. Clear canvas
  343. * 3. Clear icon and edge data
  344. * 4. reset canvas statechart
  345. */
  346. this.resetCanvas = function (){
  347. __select();
  348. __canvas.clear();
  349. __icons = {};
  350. __edges = {};
  351. __canvasBehaviourStatechart.init();
  352. };
  353. /**
  354. * Sets up a model popup window and displays it.
  355. *
  356. * @param elements - DOM elements to be displayed in order
  357. * @param getinput - function that retrieves the desired user-input
  358. * from "elements".
  359. * @param type - Can be __NO_BUTTONS, __ONE_BUTTON,
  360. * or __TWO_BUTTONS and controls the number of displayed buttons
  361. * @param title - the dialog title
  362. * @param callback - called with the result of getinput when the ok
  363. * button is clicked
  364. */
  365. this.setupAndShowDialog = function(elements,getinput,type,title,callback){
  366. // BehaviorManager.handleUserEvent(__EVENT_CANCELED_DIALOG);
  367. var dialog = $('#div_dialog'),
  368. the_id = __dialog_stack.length;
  369. dialog = dialog.clone().attr("id", 'div_dialog_'+the_id);
  370. dim_bg = $('#div_dim_bg'),
  371. div_title = $('<div>');
  372. __dialog_stack.push(dialog);
  373. dialog.appendTo(document.body);
  374. div_title.attr("class", 'dialog_title');
  375. div_title.append(GUIUtils.getTextSpan(title || ''));
  376. dialog.append(div_title);
  377. elements.forEach(
  378. function(e)
  379. {
  380. dialog.append(e);
  381. dialog.append( $('<br>') );
  382. });
  383. dialog.append( $('<br>') );
  384. if( type != __NO_BUTTONS )
  385. {
  386. var ok = $('<button class="okbutton">'); // HUSEYIN-ENTER
  387. ok.click( function(ev) {
  388. if( getinput == undefined )
  389. {
  390. BehaviorManager.handleUserEvent(__EVENT_OKAYED_DIALOG);
  391. if( callback != undefined )
  392. callback();
  393. }
  394. else
  395. {
  396. try{
  397. var input = getinput();
  398. } catch(err) {
  399. console.error('failed to retrieve dialog input :: '+err);
  400. return;
  401. }
  402. input = (utils.isArray(input) ?
  403. input.map(function(i) {return String(i);}) :
  404. input);
  405. BehaviorManager.handleUserEvent(__EVENT_OKAYED_DIALOG);
  406. callback(input);
  407. }
  408. });
  409. ok.attr("id", "dialog_btn");
  410. ok.html('ok');
  411. dialog.append(ok);
  412. }
  413. if( type == __TWO_BUTTONS )
  414. {
  415. var cancel = $('<button>');
  416. cancel.click(function(ev) {
  417. BehaviorManager.handleUserEvent(__EVENT_CANCELED_DIALOG);
  418. });
  419. cancel.html('cancel');
  420. dialog.append(cancel);
  421. }
  422. dialog.keydown(function (event) {
  423. //tab through the fields
  424. if (event.key == "Tab") {
  425. try {
  426. if (title.startsWith("edit")) {
  427. let table_row = event.target.parentElement.parentElement;
  428. let nextEle = table_row.nextElementSibling;
  429. // at end, so select first element
  430. if (nextEle == null) {
  431. nextEle = table_row.parentElement.firstElementChild;
  432. }
  433. //get the actual text field
  434. let nextField = nextEle.children[1].children[0];
  435. nextField.focus();
  436. } else if (title.startsWith("Parameters")) { //try to tab through workflow parameters
  437. let element = event.target;
  438. //get the next element
  439. //skips the <br>s and labels
  440. let nextEle = element.nextElementSibling.nextElementSibling
  441. .nextElementSibling.nextElementSibling;
  442. //cycle back around to the top
  443. if (nextEle.nodeName == "BUTTON") {
  444. nextEle = nextEle.parentElement.children[3];
  445. }
  446. nextEle.focus();
  447. }
  448. } catch (err) { //catch errors if something was unexpected
  449. console.debug("Tab event failed: " + err);
  450. }
  451. }
  452. });
  453. BehaviorManager.setActiveBehaviourStatechart(__SC_DIALOG);
  454. BehaviorManager.handleUserEvent(__EVENT_SHOW_DIALOG);
  455. };
  456. /*
  457. NOTE:: the sortedButtonNames() function sorts icon definition metamodels and
  458. button models s.t. buttons appear in an order that reflects their
  459. model... to be perfectly clean, this should be absorbed into icon
  460. definition and button model compilers, but since button models aren't
  461. compiled, it was simpler to just let this relatively simple logic live
  462. here
  463. */
  464. /**
  465. * Sets up and shows a toolbar according to the given button model or metamodel.
  466. *
  467. * This method:
  468. * 1. Removes any old instance of the toolbar if it currently exists
  469. * 2. Creates a <div> to hold the buttons
  470. * 3. Creates each button and appends it to the <div>
  471. * 4. Add the <div> to the dock
  472. * 5. Map the toolbar to its data
  473. *
  474. * @param tb - the toolbar to setup and show
  475. * @param data - the data to bind the toolbar to
  476. * @param type - the toolbar type, can be __BUTTON_TOOLBAR or __METAMODEL_TOOLBAR
  477. */
  478. this.setupAndShowToolbar = function(tb,data,type)
  479. {
  480. var imgSrc =
  481. function(name)
  482. {
  483. return (type == __BUTTON_TOOLBAR ?
  484. tb.substring(0,tb.lastIndexOf('/')+1)+name+'.icon.png' :
  485. '/Formalisms/default.icon.png');
  486. },
  487. className =
  488. function()
  489. {return (type == __BUTTON_TOOLBAR ? 'toolbar_bm' : 'toolbar_mm');},
  490. buttons =
  491. (type == __BUTTON_TOOLBAR ? data.asm.nodes : data.types),
  492. sortedButtons =
  493. function()
  494. {
  495. return (type == __BUTTON_TOOLBAR ?
  496. /* sort button names according to their position in their
  497. associated buttons model */
  498. utils.sortDict(data.csm.nodes,
  499. function(b1,b2)
  500. {
  501. var pos1 = b1['position']['value'],
  502. pos2 = b2['position']['value'];
  503. if( (pos1[1] < pos2[1]) ||
  504. (pos1[1] == pos2[1] && pos1[0] < pos2[0]) )
  505. return -1;
  506. return 1;
  507. }) :
  508. utils.sortDict(data.types,
  509. /* sort type names according to their IconIcon's position in
  510. the associated icon definition model */
  511. function(b1,b2)
  512. {
  513. var pos1 = undefined,
  514. pos2 = undefined;
  515. b1.some( function(attr)
  516. {
  517. if(attr['name'] == 'position')
  518. pos1 = attr['default'];
  519. return pos1;
  520. });
  521. b2.some( function(attr)
  522. {
  523. if(attr['name'] == 'position')
  524. pos2 = attr['default'];
  525. return pos2;
  526. });
  527. if( (pos1[1] < pos2[1]) ||
  528. (pos1[1] == pos2[1] && pos1[0] < pos2[0]) )
  529. return -1;
  530. return 1;
  531. }) );
  532. },
  533. createButton =
  534. function(name,tooltip,code)
  535. {
  536. var div = $('<div>'),
  537. img = $('<img>');
  538. div.addClass( 'toolbar_button' );
  539. div.attr("id", tb+'/'+name);
  540. div.attr("title", tooltip);
  541. div.click( function(ev){
  542. var res = HttpUtils.safeEval(code);
  543. if( res['$uerr'] )
  544. WindowManagement.openDialog(
  545. _ERROR,
  546. 'unexpected error in button code ::\n '+res['$uerr']);
  547. else if( res['$err'] )
  548. WindowManagement.openDialog(
  549. _ERROR,
  550. 'error in button code ::\n '+res['$err']);
  551. });
  552. var url = HttpUtils.url(imgSrc(name),__NO_WID);
  553. img.attr("src", url);
  554. //handle missing icon
  555. let defaultUrl = HttpUtils.url("/Formalisms/default.icon.png");
  556. let missingMsg = "Warning: The icon \"" + url + "\" is missing! The default icon has been used.";
  557. let onerrorStr = "this.onerror = ''; this.src = '" + defaultUrl + "'; console.log('" + missingMsg + "');";
  558. img.attr('onerror', onerrorStr);
  559. div.append(img);
  560. return div;
  561. };
  562. GUIUtils.removeToolbar(tb);
  563. var tb_div = $('<div>');
  564. tb_div.attr("id", 'div_toolbar_'+tb);
  565. tb_div.attr("class", className()+' toolbar unselectable' );
  566. tb_div.attr("title", tb);
  567. sortedButtons().forEach(
  568. function(b)
  569. {
  570. if( type == __METAMODEL_TOOLBAR && b.match(/(.*)Link$/) )
  571. return;
  572. var spc1 = $('<span>'),
  573. spc2 = $('<span>');
  574. // spc1.className = spc2.className = 'toolbar_space';
  575. spc1.attr("class", "toolbar_space" );
  576. spc2.attr("class", "toolbar_space" );
  577. tb_div.append(spc1);
  578. if( type == __BUTTON_TOOLBAR )
  579. tb_div.append(
  580. createButton(
  581. buttons[b]['name']['value'],
  582. buttons[b]['tooltip']['value'],
  583. buttons[b]['code']['value']) );
  584. else if( (matches = b.match(/(.*)Icon/)) )
  585. tb_div.append(
  586. createButton(
  587. b,
  588. 'create instance(s) of '+b.match(/(.*)Icon/)[1],
  589. '_setTypeToCreate("'+
  590. tb.substring(0,tb.length-'.metamodel'.length)+
  591. '/'+b+'");') );
  592. tb_div.append(spc2);
  593. } );
  594. if( tb_div.children().length == 0 )
  595. tb_div.append( GUIUtils.getTextSpan(tb,'toolbar_alt') );
  596. //get the toolbar
  597. let dock = $('#div_dock');
  598. //create an array and add the new toolbar
  599. let items = Array.from(dock[0].childNodes);
  600. items.push(tb_div[0]);
  601. //sort the dock
  602. items.sort(function(a, b) {
  603. //main menu comes first
  604. if (a.id.includes("MainMenu")){
  605. return -1;
  606. }
  607. //toolbars come first
  608. if (a.id.includes("Toolbars") && !(b.id.includes("Toolbars"))){
  609. return -1;
  610. }
  611. if (b.id.includes("Toolbars") && !(a.id.includes("Toolbars"))){
  612. return 1;
  613. }
  614. //otherwise, sort by name
  615. return a.id == b.id? 0 : (a.id > b.id ? 1 : -1);
  616. });
  617. //add the elements back into the dock
  618. for (let i = 0; i < items.length; ++i) {
  619. dock.append(items[i]);
  620. }
  621. __loadedToolbars[tb] = data;
  622. };
  623. return this;
  624. }();