linny-r 2.0.8 → 2.0.10

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (36) hide show
  1. package/README.md +3 -40
  2. package/package.json +6 -2
  3. package/server.js +19 -157
  4. package/static/images/solve-not-same-changed.png +0 -0
  5. package/static/images/solve-not-same-not-changed.png +0 -0
  6. package/static/images/solve-same-changed.png +0 -0
  7. package/static/images/solve-same-not-changed.png +0 -0
  8. package/static/index.html +137 -20
  9. package/static/linny-r.css +260 -23
  10. package/static/scripts/iro.min.js +7 -7
  11. package/static/scripts/linny-r-ctrl.js +126 -85
  12. package/static/scripts/linny-r-gui-actor-manager.js +23 -33
  13. package/static/scripts/linny-r-gui-chart-manager.js +56 -53
  14. package/static/scripts/linny-r-gui-constraint-editor.js +10 -14
  15. package/static/scripts/linny-r-gui-controller.js +644 -260
  16. package/static/scripts/linny-r-gui-dataset-manager.js +64 -66
  17. package/static/scripts/linny-r-gui-documentation-manager.js +11 -17
  18. package/static/scripts/linny-r-gui-equation-manager.js +22 -22
  19. package/static/scripts/linny-r-gui-experiment-manager.js +124 -141
  20. package/static/scripts/linny-r-gui-expression-editor.js +26 -12
  21. package/static/scripts/linny-r-gui-file-manager.js +42 -48
  22. package/static/scripts/linny-r-gui-finder.js +294 -55
  23. package/static/scripts/linny-r-gui-model-autosaver.js +2 -4
  24. package/static/scripts/linny-r-gui-monitor.js +35 -41
  25. package/static/scripts/linny-r-gui-paper.js +42 -70
  26. package/static/scripts/linny-r-gui-power-grid-manager.js +31 -34
  27. package/static/scripts/linny-r-gui-receiver.js +1 -2
  28. package/static/scripts/linny-r-gui-repository-browser.js +44 -46
  29. package/static/scripts/linny-r-gui-scale-unit-manager.js +32 -32
  30. package/static/scripts/linny-r-gui-sensitivity-analysis.js +61 -67
  31. package/static/scripts/linny-r-gui-undo-redo.js +94 -95
  32. package/static/scripts/linny-r-milp.js +20 -24
  33. package/static/scripts/linny-r-model.js +1932 -2274
  34. package/static/scripts/linny-r-utils.js +27 -27
  35. package/static/scripts/linny-r-vm.js +900 -998
  36. package/static/show-png.html +0 -113
@@ -192,58 +192,57 @@ class Controller {
192
192
  // NOTE: Add '' in case string is a number
193
193
  const lines = ('' + string).split('\n');
194
194
  let w = 0;
195
- for(let i = 0; i < lines.length; i++) {
196
- w = Math.max(w, lines[i].length * cw);
195
+ for(const l of lines) {
196
+ w = Math.max(w, l.length * cw);
197
197
  }
198
198
  return {width: w, height: lines.length * ch};
199
199
  }
200
200
 
201
201
  stringToLineArray(string, width=100, fsize=8) {
202
- // Returns an array of strings wrapped to given width at given font size
203
- // while preserving newlines -- used to format text of notes
202
+ // Return an array of strings wrapped to given width at given font size
203
+ // while preserving newlines -- used to format text of notes.
204
204
  const
205
205
  multi = [],
206
206
  lines = string.split('\n'),
207
- ll = lines.length,
208
207
  // If no paper, assume 144 px/inch, so 1 pt = 2 px
209
208
  fh = (this.paper ? this.paper.font_heights[fsize] : 2 * fsize),
210
209
  scalar = fh / 2;
211
- for(let i = 0; i < ll; i++) {
210
+ for(const l of lines) {
212
211
  // NOTE: interpret two spaces as a "non-breaking" space
213
- const words = lines[i].replace(/ /g, '\u00A0').trim().split(/ +/);
212
+ const words = l.replace(/ /g, '\u00A0').trim().split(/ +/);
214
213
  // Split words at '-' when wider than width
215
- for(let j = 0; j < words.length; j++) {
216
- if(words[j].length * scalar > width) {
217
- const sw = words[j].split('-');
218
- if(sw.length > 1) {
219
- // Replace j-th word by last fragment of split string
220
- words[j] = sw.pop();
214
+ for(let i = 0; i < words.length; i++) {
215
+ if(words[i].length * scalar > width) {
216
+ const sw = words[i].split('-');
217
+ if(sw[0] && sw.length > 1) {
218
+ // Replace i-th word by last fragment of split string
219
+ words[i] = sw.pop();
221
220
  // Insert remaining fragments before
222
- while(sw.length > 0) words.splice(j, 0, sw.pop() + '-');
221
+ while(sw.length > 0) words.splice(i, 0, sw.pop() + '-');
223
222
  }
224
223
  }
225
224
  }
226
- let line = words[0] + ' ';
227
- for(let j = 1; j < words.length; j++) {
225
+ let line = words.shift() + ' ';
226
+ for(const word of words) {
228
227
  const
229
- l = line + words[j] + ' ',
228
+ l = line + word + ' ',
230
229
  w = (l.length - 1) * scalar;
231
- if (w > width && j > 0) {
230
+ if (w > width) {
232
231
  const
233
232
  nl = line.trim(),
234
233
  nw = Math.floor(nl.length * scalar);
235
234
  multi.push(nl);
236
235
  // If width of added line exceeds the given width, adjust width
237
- // so that following lines fill out better
236
+ // so that following lines fill out better.
238
237
  width = Math.max(width, nw);
239
- line = words[j] + ' ';
238
+ line = word + ' ';
240
239
  } else {
241
240
  line = l;
242
241
  }
243
242
  }
244
243
  line = line.trim();
245
244
  // NOTE: Chrome and Safari ignore empty lines in SVG text; as a workaround,
246
- // we add a non-breaking space to lines containing only whitespace
245
+ // we add a non-breaking space to lines containing only whitespace.
247
246
  if(!line) line = '\u00A0';
248
247
  multi.push(line);
249
248
  }
@@ -280,19 +279,19 @@ class Controller {
280
279
  // Methods to ensure proper naming of entities.
281
280
 
282
281
  cleanName(name) {
283
- // Returns `name` without the object-attribute separator |, backslashes,
282
+ // Return `name` without the object-attribute separator |, backslashes,
284
283
  // and leading and trailing whitespace, and with all internal whitespace
285
284
  // reduced to a single space.
286
285
  name = name.replace(this.OA_SEPARATOR, ' ')
287
286
  .replace(/\||\\/g, ' ').trim()
288
287
  .replace(/\s\s+/g, ' ');
289
- // NOTE: this may still result in a single space, which is not a name
288
+ // NOTE: This may still result in a single space, which is not a name.
290
289
  if(name === ' ') return '';
291
290
  return name;
292
291
  }
293
292
 
294
293
  validName(name) {
295
- // Returns TRUE if `name` is a valid Linny-R entity name. These names
294
+ // Return TRUE if `name` is a valid Linny-R entity name. These names
296
295
  // must not be empty strings, may not contain brackets, backslashes or
297
296
  // vertical bars, may not end with a colon, and must start with an
298
297
  // underscore, a letter or a digit.
@@ -335,7 +334,7 @@ class Controller {
335
334
  }
336
335
 
337
336
  completePrefix(name) {
338
- // Returns the prefix part (including the final colon plus space),
337
+ // Return the prefix part (including the final colon plus space),
339
338
  // or the empty string if none.
340
339
  const p = UI.prefixesAndName(name);
341
340
  p[p.length - 1] = '';
@@ -343,6 +342,7 @@ class Controller {
343
342
  }
344
343
 
345
344
  sharedPrefix(n1, n2) {
345
+ // Return the longest series of prefixes that `n1` and `n2` have in common.
346
346
  const
347
347
  pan1 = this.prefixesAndName(n1),
348
348
  pan2 = this.prefixesAndName(n2),
@@ -350,7 +350,7 @@ class Controller {
350
350
  shared = [];
351
351
  let i = 0;
352
352
  while(i < l && ciCompare(pan1[i], pan2[i]) === 0) {
353
- // NOTE: if identical except for case, prefer "Abc" over "aBc"
353
+ // NOTE: If identical except for case, prefer "Abc" over "aBc".
354
354
  shared.push(pan1[i] < pan2[i] ? pan1[i] : pan2[i]);
355
355
  i++;
356
356
  }
@@ -358,26 +358,90 @@ class Controller {
358
358
  }
359
359
 
360
360
  colonPrefixedName(name, prefix) {
361
- // Replaces a leading colon in `name` by `prefix`.
361
+ // Replace a leading colon in `name` by `prefix`.
362
362
  // If `name` identifies a link or a constraint, this is applied to
363
363
  // both node names.
364
+ // NOTE: To give the modeler more control over what to use as prefix,
365
+ // successive colons indicate that a shorter prefix should be used.
366
+ // For example, when the prefix is XXX: YYY: ZZZ, two colons mean
367
+ // "use only XXX: YYY:", three colons "use only XXX:", etc.
364
368
  const
365
369
  arrow = (name.indexOf(this.LINK_ARROW) >= 0 ?
366
370
  this.LINK_ARROW : this.CONSTRAINT_ARROW),
367
371
  nodes = name.split(arrow);
368
372
  for(let i = 0; i < nodes.length; i++) {
369
- nodes[i] = nodes[i].replace(/^:\s*/, prefix)
373
+ // First check for the special case of just a leading colon plus
374
+ // possibly an entity attribute.
375
+ // NOTE:
376
+ const m = nodes[i].match(/^(:+)\s*(\|[^\|]*)/);
377
+ if(m) {
378
+ // Split prefix in parts.
379
+ const p = prefix.split(UI.PREFIXER);
380
+ // Remove last (empty!) substring.
381
+ p.pop();
382
+ // Shorten prefix by the number of successive colons minus 1.
383
+ for(let i = 1; i < m[1].length; i++) p.pop();
384
+ p.push('');
385
+ // New name is just the (shortened) prefix without its last ": ".
386
+ nodes[i] = p.join(UI.PREFIXER).replace(/:\s*$/, '') + m[2];
387
+ } else {
388
+ // Prefix is typically leading, so try to replace this first.
389
+ const m = nodes[i].match(/^(:+)/);
390
+ if(m) {
391
+ // Shorten prefix by the number of successive colons minus 1.
392
+ let p = prefix.split(UI.PREFIXER);
393
+ p.pop();
394
+ for(let i = 1; i < m[1].length; i++) p.pop();
395
+ p.push('');
396
+ nodes[i] = nodes[i].replace(/^:+\s*/, p.join(UI.PREFIXER));
397
+ } else {
398
+ // If no change, try to replace an embedded double prefix.
370
399
  // NOTE: An embedded double prefix, e.g., "xxx: : yyy" indicates
371
400
  // that the second colon+space should be replaced by the prefix.
372
- // This "double prefix" may occur only once in an entity name,
373
- // hence no global regexp.
374
- .replace(/(\w+):\s+:\s+(\w+)/, `$1: ${prefix}$2`);
401
+ const m = nodes[i].match(/(\w+):\s+(:+)\s+(\w+)/);
402
+ if(m) {
403
+ // Shorten prefix by the number of successive colons minus 1.
404
+ let p = prefix.split(UI.PREFIXER);
405
+ p.pop();
406
+ for(let i = 1; i < m[2].length; i++) p.pop();
407
+ p.push('');
408
+ prefix = p.join(UI.PREFIXER);
409
+ nodes[i] = `${m[1]}: ${prefix}${m[3]}`;
410
+ }
411
+ }
412
+ }
375
413
  }
376
414
  return nodes.join(arrow);
377
415
  }
416
+
417
+ entityPrefix(name) {
418
+ // Return the prefix of `name` with its trailing colon+space.
419
+ const
420
+ arrow = (name.indexOf(this.LINK_ARROW) >= 0 ?
421
+ this.LINK_ARROW : this.CONSTRAINT_ARROW),
422
+ nodes = name.split(arrow);
423
+ if(nodes.length === 1) return this.completePrefix(name);
424
+ // For names of links and constraints, it depends:
425
+ const
426
+ fn = nodes[0],
427
+ tn = nodes[1];
428
+ if(fn.indexOf(UI.PREFIXER) >= 0) {
429
+ if(tn.indexOf(UI.PREFIXER) >= 0) {
430
+ // If BOTH nodes are prefixed, use the longest prefix that these
431
+ // nodes have in common...
432
+ return UI.sharedPrefix(fn, tn) + UI.PREFIXER;
433
+ }
434
+ // .. otherwise, return the FROM node prefix.
435
+ return UI.completePrefix(fn);
436
+ }
437
+ // No FROM node prefix => return the TO node prefix (if any)
438
+ if(tn.indexOf(UI.PREFIXER) >= 0) return UI.completePrefix(tn);
439
+ // No prefixers => empty prefix.
440
+ return '';
441
+ }
378
442
 
379
443
  tailNumber(name) {
380
- // Returns the string of digits at the end of `name`. If not there,
444
+ // Return the string of digits at the end of `name`. If not there,
381
445
  // check prefixes (if any) *from right to left* for a tail number.
382
446
  // Thus, the number that is "closest" to the name part is returned.
383
447
  const pan = UI.prefixesAndName(name);
@@ -429,7 +493,6 @@ class Controller {
429
493
  return pan1.length - pan2.length;
430
494
  }
431
495
 
432
-
433
496
  nameToID(name) {
434
497
  // Return a name in lower case with link arrow replaced by three
435
498
  // underscores, constraint link arrow by four underscores, and spaces
@@ -444,10 +507,11 @@ class Controller {
444
507
  // Empty string signals failure.
445
508
  return '';
446
509
  }
447
- // NOTE: Replace single quotes by Unicode apostrophe so that they
448
- // cannot interfere with JavaScript strings delimited by single quotes.
510
+ // NOTE: Replace single quotes by Unicode apostrophe, and double quotes
511
+ // by Unicode mono-space " so that quotes cannot interfere with string
512
+ // delimiters used in JavaScript or HTML.
449
513
  return name.toLowerCase().replace(/\s/g, '_')
450
- .replace("'", '\u2019').replace('"', '\uff02');
514
+ .replaceAll("'", '\u2019').replaceAll('"', '\uff02');
451
515
  }
452
516
 
453
517
  htmlEquationName(n) {
@@ -654,9 +718,8 @@ class RepositoryBrowser {
654
718
  .then((data) => {
655
719
  if(UI.postResponseOK(data)) {
656
720
  // NOTE: Trim to prevent empty name strings.
657
- const rl = data.trim().split('\n');
658
- for(let i = 0; i < rl.length; i++) {
659
- this.addRepository(rl[i].trim());
721
+ for(const r of data.trim().split('\n')) {
722
+ this.addRepository(r.trim());
660
723
  }
661
724
  }
662
725
  // NOTE: Set index to first repository on list (typically the
@@ -669,23 +732,12 @@ class RepositoryBrowser {
669
732
 
670
733
  repositoryByName(n) {
671
734
  // Return the repository having name `n` if already known, or NULL.
672
- for(let i = 0; i < this.repositories.length; i++) {
673
- if(this.repositories[i].name === n) {
674
- return this.repositories[i];
675
- }
735
+ for(const r of this.repositories) {
736
+ if(r.name === n) return r;
676
737
  }
677
738
  return null;
678
739
  }
679
740
 
680
- asFileName(s) {
681
- // Return string `s` with whitespace converted to a single dash, and
682
- // special characters converted to underscores.
683
- return s.normalize('NFKD').trim()
684
- .replace(/[\s\-]+/g, '-')
685
- .replace(/[^A-Za-z0-9_\-]/g, '_')
686
- .replace(/^[\-\_]+|[\-\_]+$/g, '');
687
- }
688
-
689
741
  loadModuleAsModel() {
690
742
  // Load selected module as model.
691
743
  if(this.repository_index >= 0 && this.module_index >= 0) {
@@ -842,9 +894,7 @@ class ChartManager {
842
894
 
843
895
  resetChartVectors() {
844
896
  // Reset vectors of all charts.
845
- for(let i = 0; i < MODEL.charts.length; i++) {
846
- MODEL.charts[i].resetVectors();
847
- }
897
+ for(const c of MODEL.charts) c.resetVectors();
848
898
  }
849
899
 
850
900
  promptForWildcardIndices(chart, dsm) {
@@ -916,9 +966,8 @@ class SensitivityAnalysis {
916
966
  // Clear results from previous analysis.
917
967
  this.clearResults();
918
968
  this.parameters = [];
919
- for(let i = 0; i < MODEL.sensitivity_parameters.length; i++) {
969
+ for(const p of MODEL.sensitivity_parameters) {
920
970
  const
921
- p = MODEL.sensitivity_parameters[i],
922
971
  vn = p.split(UI.OA_SEPARATOR),
923
972
  obj = MODEL.objectByName(vn[0]),
924
973
  oax = (obj ? obj.attributeExpression(vn[1]) : null);
@@ -932,20 +981,16 @@ class SensitivityAnalysis {
932
981
  }
933
982
  }
934
983
  this.chart = new Chart(this.chart_title);
935
- for(let i = 0; i < MODEL.sensitivity_outcomes.length; i++) {
936
- const vn = MODEL.sensitivity_outcomes[i].split(UI.OA_SEPARATOR);
937
- this.chart.addVariable(vn[0], vn[1]);
984
+ for(const o of MODEL.sensitivity_outcomes) {
985
+ this.chart.addVariable(...o.split(UI.OA_SEPARATOR));
938
986
  }
939
987
  this.experiment = new Experiment(this.experiment_title);
940
988
  this.experiment.charts = [this.chart];
941
989
  this.experiment.inferVariables();
942
990
  // This experiment always uses the same combination: the base selectors.
943
991
  const bs = MODEL.base_case_selectors.split(' ');
944
- this.experiment.combinations = [];
945
992
  // Add this combination N+1 times for N parameters.
946
- for(let i = 0; i <= this.parameters.length; i++) {
947
- this.experiment.combinations.push(bs);
948
- }
993
+ this.experiment.combinations = Array(this.parameters.length + 1).fill(bs);
949
994
  // NOTE: Model settings will not be changed, but will be restored after
950
995
  // each run => store the original settings.
951
996
  this.experiment.original_model_settings = MODEL.settingsString;
@@ -1045,17 +1090,15 @@ class SensitivityAnalysis {
1045
1090
  this.perc = {};
1046
1091
  this.shade = {};
1047
1092
  this.data = {};
1048
- const
1049
- ol = MODEL.sensitivity_outcomes.length,
1050
- rl = MODEL.sensitivity_runs.length;
1093
+ const ol = MODEL.sensitivity_outcomes.length;
1051
1094
  if(ol === 0) return;
1052
1095
  // Always find highest relative change.
1053
1096
  let max_dif = 0;
1054
1097
  for(let i = 0; i < ol; i++) {
1055
1098
  this.data[i] = [];
1056
- for(let j = 0; j < rl; j++) {
1099
+ for(const run of MODEL.sensitivity_runs) {
1057
1100
  // Get the selected statistic for each run to get an array of numbers.
1058
- const rr = MODEL.sensitivity_runs[j].results[i];
1101
+ const rr = run.results[i];
1059
1102
  if(!rr) {
1060
1103
  this.data[i].push(VM.UNDEFINED);
1061
1104
  } else if(sas === 'N') {
@@ -1162,8 +1205,7 @@ class ExperimentManager {
1162
1205
  // Select charts having 1 or more variables, as only these are meaningful
1163
1206
  // as the dependent variables of an experiment
1164
1207
  this.suitable_charts.length = 0;
1165
- for(let i = 0; i < MODEL.charts.length; i++) {
1166
- const c = MODEL.charts[i];
1208
+ for(const c of MODEL.charts) {
1167
1209
  if(c.variables.length > 0) this.suitable_charts.push(c);
1168
1210
  }
1169
1211
  }
@@ -1278,23 +1320,22 @@ class ExperimentManager {
1278
1320
  }
1279
1321
  xr.start();
1280
1322
  this.showProgress(ci, p, n);
1281
- // NOTE: first restore original model settings (setings may be partial!)
1323
+ // NOTE: First restore original model settings (setings may be partial!).
1282
1324
  MODEL.parseSettings(x.original_model_settings);
1283
- // Parse all active settings selector strings
1284
- // NOTE: may be multiple strings; the later overwrite the earlier
1285
- for(let i = 0; i < x.settings_selectors.length; i++) {
1286
- const ssel = x.settings_selectors[i].split('|');
1325
+ // Parse all active settings selector strings.
1326
+ // NOTE: May be multiple strings; the later overwrite the earlier.
1327
+ for(const sel of x.settings_selectors) {
1328
+ const ssel = sel.split('|');
1287
1329
  if(combi.indexOf(ssel[0]) >= 0) MODEL.parseSettings(ssel[1]);
1288
1330
  }
1289
- // Also set the correct round sequence
1290
- // NOTE: if no match, default is retained
1291
- for(let i = 0; i < x.actor_selectors.length; i++) {
1292
- const asel = x.actor_selectors[i];
1331
+ // Also set the correct round sequence.
1332
+ // NOTE: If no match, default is retained.
1333
+ for(const asel of x.actor_selectors) {
1293
1334
  if(combi.indexOf(asel.selector) >= 0) {
1294
1335
  MODEL.round_sequence = asel.round_sequence;
1295
1336
  }
1296
1337
  }
1297
- // Only now compute the simulation run time (number of time steps)
1338
+ // Only now compute the simulation run time (number of time steps).
1298
1339
  xr.time_steps = MODEL.end_period - MODEL.start_period + 1;
1299
1340
  VM.callback = this.callback;
1300
1341
  // NOTE: Asynchronous call. All follow-up actions must be performed
@@ -1304,18 +1345,18 @@ class ExperimentManager {
1304
1345
  }
1305
1346
 
1306
1347
  processRun() {
1307
- // This method is called by the solveBlocks method of the Virtual Machine
1348
+ // This method is called by the solveBlocks method of the Virtual Machine.
1308
1349
  const x = MODEL.running_experiment;
1309
1350
  if(!x) return;
1310
1351
  const aci = x.active_combination_index;
1311
1352
  if(MODEL.solved) {
1312
- // NOTE: addresults will call processRestOfRun when completed
1353
+ // NOTE: addresults will call processRestOfRun when completed.
1313
1354
  x.runs[aci].addResults();
1314
1355
  } else {
1315
1356
  // Do not add results...
1316
1357
  UI.warn(`Model run #${aci} incomplete -- results will be invalid`);
1317
- // ... but do perform the usual post-processing
1318
- // NOTE: when sensitivity analysis is being performed, switch back to SA
1358
+ // ... but do perform the usual post-processing.
1359
+ // NOTE: When sensitivity analysis is being performed, switch back to SA.
1319
1360
  if(SENSITIVITY_ANALYSIS.experiment) {
1320
1361
  SENSITIVITY_ANALYSIS.processRestOfRun();
1321
1362
  } else {
@@ -72,18 +72,18 @@ class ActorManager {
72
72
  }
73
73
 
74
74
  roundLetter(n) {
75
- // Returns integer `n` as lower case letter: 1 = a, 2 = b, 26 = z
76
- // NOTE: numbers 27-52 return upper case A-Z; beyond ranges results in '?'
77
- if(n < 1 || n > this.max_rounds) return '?';
75
+ // Return integer `n` as lower case letter: 1 = a, 2 = b, 26 = z.
76
+ // NOTE: Numbers 27-31 return upper case A-E; beyond ranges results in '?'.
77
+ if(n < 1 || n > VM.max_rounds) return '?';
78
78
  return VM.round_letters[n];
79
79
  }
80
80
 
81
81
  checkRoundSequence(s) {
82
82
  // Expects a string with zero or more round letters
83
- for(let i = 0; i < s.length; i++) {
84
- const n = VM.round_letters.indexOf(s[i]);
83
+ for(const rl of s) {
84
+ const n = VM.round_letters.indexOf(rl);
85
85
  if(n < 1 || n > this.rounds) {
86
- UI.warn(`Round ${s[i]} outside range (a` +
86
+ UI.warn(`Round ${rl} outside range (a` +
87
87
  (this.rounds > 1 ? '-' + this.roundLetter(this.rounds) : '') + ')');
88
88
  return false;
89
89
  }
@@ -160,20 +160,14 @@ class ActorManager {
160
160
  // in the actors table, but both need to be passed on.
161
161
  const p = event.target.parentElement;
162
162
  // Pass name and weight of the selected actor (first and second
163
- // TD of this TR)
163
+ // TD of this TR).
164
164
  ACTOR_MANAGER.showEditActorDialog(
165
165
  p.cells[0].innerText, p.cells[1].innerText);
166
166
  };
167
- for(let i = 0; i < abs.length; i++) {
168
- abs[i].addEventListener('click', abf);
169
- }
170
- // Clicking the other cells should open the ACTOR dialog
171
- for(let i = 0; i < abns.length; i++) {
172
- abns[i].addEventListener('click', eaf);
173
- }
174
- for(let i = 0; i < abws.length; i++) {
175
- abws[i].addEventListener('click', eaf);
176
- }
167
+ for(const ab of abs) ab.addEventListener('click', abf);
168
+ // Clicking the other cells should open the ACTOR dialog.
169
+ for(const abn of abns) abn.addEventListener('click', eaf);
170
+ for(const abw of abws) abw.addEventListener('click', eaf);
177
171
  UI.modals.actors.show();
178
172
  }
179
173
 
@@ -190,7 +184,7 @@ class ActorManager {
190
184
  }
191
185
 
192
186
  addRound() {
193
- // Limit # rounds to 30 to cope with 32 bit integer used by JavaScript
187
+ // Limit # rounds to 31 to cope with 32 bit integer used by JavaScript.
194
188
  if(this.rounds < VM.max_rounds) {
195
189
  this.rounds++;
196
190
  this.round_count.innerHTML = pluralS(this.rounds, 'round');
@@ -202,12 +196,12 @@ class ActorManager {
202
196
  if(this.selected_round > 0 && this.selected_round <= this.rounds) {
203
197
  const mask = Math.pow(2, this.selected_round) - 1;
204
198
  this.updateRoundFlags();
205
- for(let i = 0; i < MODEL.actor_list.length; i++) {
206
- let rf = MODEL.actor_list[i][2];
199
+ for(const a of MODEL.actor_list) {
200
+ let rf = a[2];
207
201
  const
208
202
  low = (rf & mask),
209
203
  high = (rf & ~mask) >>> 1;
210
- MODEL.actor_list[i][2] = (low | high);
204
+ a[2] = (low | high);
211
205
  }
212
206
  this.rounds--;
213
207
  this.selected_round = 0;
@@ -291,16 +285,13 @@ class ActorManager {
291
285
  }
292
286
 
293
287
  updateActorProperties() {
294
- // This method is called when the modeler clicks OK on the actor list dialog
288
+ // This method is called when the modeler clicks OK on the actor list dialog.
295
289
  this.updateRoundFlags();
296
290
  const xp = new ExpressionParser('');
297
- let a,
298
- ali,
299
- ok = true;
300
- for(let i = 0; i < MODEL.actor_list.length; i++) {
301
- ali = MODEL.actor_list[i];
302
- a = MODEL.actors[ali[0]];
303
- // Rename actor if name has been changed
291
+ let ok = true;
292
+ for(const ali of MODEL.actor_list) {
293
+ const a = MODEL.actors[ali[0]];
294
+ // Rename actor if name has been changed.
304
295
  if(a.displayName != ali[1]) a.rename(ali[1]);
305
296
  // Set its round flags
306
297
  a.round_flags = ali[2];
@@ -316,7 +307,7 @@ class ActorManager {
316
307
  a.weight.update(xp);
317
308
  }
318
309
  }
319
- // Update import/export status
310
+ // Update import/export status.
320
311
  MODEL.ioUpdate(a, ali[4]);
321
312
  }
322
313
  const seq = this.sequence.value;
@@ -329,8 +320,8 @@ class ActorManager {
329
320
  }
330
321
 
331
322
  showActorInfo(n, shift) {
332
- // Show actor documentation when Shift is held down
333
- // NOTE: do not allow documentation of "(no actor)"
323
+ // Show actor documentation when Shift is held down.
324
+ // NOTE: do not allow documentation of "(no actor)".
334
325
  if(n > 0) {
335
326
  const a = MODEL.actorByID(MODEL.actor_list[n][0]);
336
327
  DOCUMENTATION_MANAGER.update(a, shift);
@@ -338,4 +329,3 @@ class ActorManager {
338
329
  }
339
330
 
340
331
  } // END of class ActorManager
341
-