linny-r 2.0.7 → 2.0.9

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 (31) hide show
  1. package/README.md +3 -40
  2. package/package.json +1 -1
  3. package/server.js +19 -157
  4. package/static/index.html +74 -21
  5. package/static/linny-r.css +22 -16
  6. package/static/scripts/iro.min.js +7 -7
  7. package/static/scripts/linny-r-ctrl.js +51 -72
  8. package/static/scripts/linny-r-gui-actor-manager.js +23 -33
  9. package/static/scripts/linny-r-gui-chart-manager.js +50 -45
  10. package/static/scripts/linny-r-gui-constraint-editor.js +6 -10
  11. package/static/scripts/linny-r-gui-controller.js +254 -230
  12. package/static/scripts/linny-r-gui-dataset-manager.js +143 -32
  13. package/static/scripts/linny-r-gui-documentation-manager.js +11 -17
  14. package/static/scripts/linny-r-gui-equation-manager.js +22 -22
  15. package/static/scripts/linny-r-gui-experiment-manager.js +102 -129
  16. package/static/scripts/linny-r-gui-file-manager.js +53 -46
  17. package/static/scripts/linny-r-gui-finder.js +105 -51
  18. package/static/scripts/linny-r-gui-model-autosaver.js +2 -4
  19. package/static/scripts/linny-r-gui-monitor.js +35 -41
  20. package/static/scripts/linny-r-gui-paper.js +42 -70
  21. package/static/scripts/linny-r-gui-power-grid-manager.js +31 -34
  22. package/static/scripts/linny-r-gui-receiver.js +1 -2
  23. package/static/scripts/linny-r-gui-repository-browser.js +44 -46
  24. package/static/scripts/linny-r-gui-scale-unit-manager.js +32 -32
  25. package/static/scripts/linny-r-gui-sensitivity-analysis.js +61 -67
  26. package/static/scripts/linny-r-gui-undo-redo.js +94 -95
  27. package/static/scripts/linny-r-milp.js +20 -24
  28. package/static/scripts/linny-r-model.js +1832 -2248
  29. package/static/scripts/linny-r-utils.js +35 -27
  30. package/static/scripts/linny-r-vm.js +807 -905
  31. package/static/show-png.html +0 -113
@@ -47,13 +47,11 @@ class GUIFileManager {
47
47
  // buttons on the top menu.
48
48
 
49
49
  getRemoteData(dataset, url) {
50
- // Gets data from a URL, or from a file on the local host
50
+ // Gets data from a URL, or from a file on the local host.
51
51
  if(url === '') return;
52
52
  if(url.indexOf('%') >= 0) {
53
53
  // Expand %i, %j and %k if used in the URL.
54
- const letters = ['i', 'j', 'k'];
55
- for(let i = 0; i < letters.length; i++) {
56
- const l = letters[i];
54
+ for(const l of ['i', 'j', 'k']) {
57
55
  url = url.replaceAll('%' + l, valueOfIndexVariable(l));
58
56
  }
59
57
  }
@@ -361,9 +359,8 @@ class GUIFileManager {
361
359
  }
362
360
  fetch('autosave/', postData({
363
361
  action: 'store',
364
- file: REPOSITORY_BROWSER.asFileName(
365
- (MODEL.name || 'no-name') + '_by_' +
366
- (MODEL.author || 'no-author')),
362
+ file: asFileName((MODEL.name || 'no-name') +
363
+ '_by_' + (MODEL.author || 'no-author')),
367
364
  xml: MODEL.asXML,
368
365
  wsd: workspace
369
366
  }))
@@ -391,48 +388,22 @@ class GUIFileManager {
391
388
  });
392
389
  }
393
390
 
394
- renderDiagramAsPNG(tight) {
395
- // When `tight` is TRUE, add no whitespace around the diagram.
396
- window.localStorage.removeItem('png-url');
397
- if(tight) {
398
- // First align to grid and then fit to size.
399
- MODEL.alignToGrid();
400
- UI.paper.fitToSize(1);
401
- } else {
402
- UI.paper.fitToSize();
403
- MODEL.alignToGrid();
391
+ loadCSVFile() {
392
+ document.getElementById('load-csv-modal').style.display = 'none';
393
+ try {
394
+ const file = document.getElementById('load-csv-file').files[0];
395
+ if(!file) return;
396
+ const reader = new FileReader();
397
+ reader.onload = (event) => DATASET_MANAGER.readCSVData(event.target.result);
398
+ reader.readAsText(file);
399
+ } catch(err) {
400
+ UI.alert('Error while reading file: ' + err);
404
401
  }
405
- this.renderSVGAsPNG(UI.paper.opaqueSVG);
406
402
  }
407
403
 
408
- renderSVGAsPNG(svg) {
409
- // Sends SVG to the server, which will convert it to PNG using Inkscape;
410
- // if successful, the server will return the URL to the PNG file location;
411
- // this URL is passed via the browser's local storage to the newly opened
412
- // browser tab that awaits this URL and then loads it
413
- const form = {
414
- action: 'png',
415
- user: VM.solver_user,
416
- token: VM.solver_token,
417
- data: btoa(encodeURI(svg))
418
- };
419
- fetch('solver/', postData(form))
420
- .then((response) => {
421
- if(!response.ok) {
422
- UI.alert(`ERROR ${response.status}: ${response.statusText}`);
423
- }
424
- return response.text();
425
- })
426
- .then((data) => {
427
- // Pass URL of image to the newly opened browser window
428
- window.localStorage.setItem('png-url', data);
429
- })
430
- .catch((err) => UI.warn(UI.WARNING.NO_CONNECTION, err));
431
- }
432
-
433
- saveDiagramAsSVG(tight) {
404
+ saveDiagramAsSVG(event) {
434
405
  // Output SVG as string with nodes and arrows 100% opaque.
435
- if(tight) {
406
+ if(event.altKey) {
436
407
  // First align to grid and then fit to size.
437
408
  MODEL.alignToGrid();
438
409
  UI.paper.fitToSize(1);
@@ -440,10 +411,15 @@ class GUIFileManager {
440
411
  UI.paper.fitToSize();
441
412
  MODEL.alignToGrid();
442
413
  }
443
- this.pushOutSVG(UI.paper.opaqueSVG);
414
+ if(event.shiftKey) {
415
+ this.pushOutSVG(UI.paper.opaqueSVG);
416
+ } else {
417
+ this.pushOutPNG(UI.paper.opaqueSVG);
418
+ }
444
419
  }
445
420
 
446
421
  pushOutSVG(svg) {
422
+ // Output SVG to browser as SVG image file download.
447
423
  const blob = new Blob([svg], {'type': 'image/svg+xml'});
448
424
  const e = document.getElementById('svg-saver');
449
425
  e.download = 'model.svg';
@@ -451,5 +427,36 @@ class GUIFileManager {
451
427
  e.href = (window.URL || webkitURL).createObjectURL(blob);
452
428
  e.click();
453
429
  }
430
+
431
+ pushOutPNG(svg) {
432
+ // Output SVG to browser as PNG image file download.
433
+ const
434
+ bytes = new TextEncoder().encode(svg),
435
+ binstr = Array.from(bytes, (b) => String.fromCodePoint(b)).join(''),
436
+ uri = 'data:image/svg+xml;base64,' + window.btoa(binstr),
437
+ img = new Image();
438
+ img.onload = () => {
439
+ const
440
+ cvs = document.createElement('canvas'),
441
+ ctx = cvs.getContext('2d');
442
+ cvs.width = img.width * 4;
443
+ cvs.height = img.height * 4;
444
+ ctx.scale(4, 4);
445
+ ctx.drawImage(img, 0, 0);
446
+ cvs.toBlob(blob => {
447
+ const
448
+ e = document.getElementById('svg-saver'),
449
+ url = (window.URL || webkitURL).createObjectURL(blob),
450
+ name = asFileName(MODEL.focal_cluster.parent ?
451
+ MODEL.focal_cluster.displayName : MODEL.name) ||
452
+ 'Linny-R-model';
453
+ e.download = name + '.png';
454
+ e.type = 'image/png';
455
+ e.href = url;
456
+ e.click();
457
+ });
458
+ };
459
+ img.src = uri;
460
+ }
454
461
 
455
462
  } // END of class GUIFileManager
@@ -46,7 +46,10 @@ class Finder {
46
46
  this.filter_input.addEventListener('input', () => FINDER.changeFilter());
47
47
  this.edit_btn = document.getElementById('finder-edit-btn');
48
48
  this.edit_btn.addEventListener(
49
- 'click', (event) => FINDER.editAttributes());
49
+ 'click', () => FINDER.editAttributes());
50
+ this.chart_btn = document.getElementById('finder-chart-btn');
51
+ this.chart_btn.addEventListener(
52
+ 'click', () => FINDER.confirmAddChartVariables());
50
53
  this.copy_btn = document.getElementById('finder-copy-btn');
51
54
  this.copy_btn.addEventListener(
52
55
  'click', (event) => FINDER.copyAttributesToClipboard(event.shiftKey));
@@ -54,6 +57,13 @@ class Finder {
54
57
  this.item_table = document.getElementById('finder-item-table');
55
58
  this.expression_table = document.getElementById('finder-expression-table');
56
59
 
60
+ // The Confirm add chart variables modal.
61
+ this.add_chart_variables_modal = new ModalDialog('confirm-add-chart-variables');
62
+ this.add_chart_variables_modal.ok.addEventListener(
63
+ 'click', () => FINDER.addVariablesToChart());
64
+ this.add_chart_variables_modal.cancel.addEventListener(
65
+ 'click', () => FINDER.add_chart_variables_modal.hide());
66
+
57
67
  // Attribute headers are used by Finder to output entity attribute values.
58
68
  this.attribute_headers = {
59
69
  A: 'ACTORS:\tWeight\tCash IN\tCash OUT\tCash FLOW',
@@ -301,41 +311,100 @@ class Finder {
301
311
  this.edit_btn.style.display = 'none';
302
312
  this.copy_btn.style.display = 'none';
303
313
  if(n > 0) {
304
- this.copy_btn.style.display = 'block';
314
+ this.copy_btn.style.display = 'inline-block';
315
+ if(CHART_MANAGER.visible && CHART_MANAGER.chart_index >= 0) {
316
+ const ca = this.commonAttributes;
317
+ if(ca.length) {
318
+ this.chart_btn.title = 'Add ' + pluralS(n, 'variable') +
319
+ ' to selected chart';
320
+ this.chart_btn.style.display = 'inline-block';
321
+ }
322
+ }
305
323
  n = this.entityGroup.length;
306
324
  if(n > 0) {
307
325
  this.edit_btn.title = 'Edit attributes of ' +
308
326
  pluralS(n, this.entities[0].type.toLowerCase());
309
- this.edit_btn.style.display = 'block';
327
+ this.edit_btn.style.display = 'inline-block';
310
328
  }
311
329
  }
312
330
  this.updateRightPane();
313
331
  }
314
332
 
333
+ get commonAttributes() {
334
+ // Returns list of attributes that all filtered entities have in common.
335
+ let ca = Object.keys(VM.attribute_names);
336
+ for(const et of this.filtered_types) {
337
+ ca = intersection(ca, VM.attribute_codes[et]);
338
+ }
339
+ return ca;
340
+ }
341
+
315
342
  get entityGroup() {
316
343
  // Returns the list of filtered entities if all are of the same type,
317
344
  // while excluding (no actor), (top cluster), datasets and equations.
318
345
  const
319
346
  eg = [],
320
- n = this.entities.length;
321
- if(n > 0) {
322
- const ft = this.filtered_types[0];
323
- if(this.filtered_types.length === 1 && 'DE'.indexOf(ft) < 0) {
324
- for(let i = 0; i < n; i++) {
325
- const e = this.entities[i];
326
- // Exclude "no actor" and top cluster.
327
- if(e.name && e.name !== '(no_actor)' && e.name !== '(top_cluster)' &&
328
- // Also exclude actor cash flow data products because
329
- // many of their properties should not be changed.
330
- !e.name.startsWith('$')) {
331
- eg.push(e);
332
- }
347
+ ft = this.filtered_types[0];
348
+ if(this.filtered_types.length === 1 && 'DE'.indexOf(ft) < 0) {
349
+ for(const e of this.entities) {
350
+ // Exclude "no actor" and top cluster.
351
+ if(e.name && e.name !== '(no_actor)' && e.name !== '(top_cluster)' &&
352
+ // Also exclude actor cash flow data products because
353
+ // many of their properties should not be changed.
354
+ !e.name.startsWith('$')) {
355
+ eg.push(e);
333
356
  }
334
357
  }
335
358
  }
336
359
  return eg;
337
360
  }
338
361
 
362
+ confirmAddChartVariables() {
363
+ // Show confirmation dialog to add variables to chart.
364
+ const
365
+ md = this.add_chart_variables_modal,
366
+ n = this.entities.length,
367
+ ca = this.commonAttributes;
368
+ let html,
369
+ et = '1 entity';
370
+ if(this.filtered_types.length === 1) {
371
+ et = pluralS(n, this.entities[0].type.toLowerCase());
372
+ } else if(n !== 1) {
373
+ et = `${n} entities`;
374
+ }
375
+ for(const a of ca) {
376
+ html += `<option value="${a}">${VM.attribute_names[a]}</option>`;
377
+ }
378
+ md.element('attribute').innerHTML = html;
379
+ md.element('count').innerText = et;
380
+ md.show();
381
+ }
382
+
383
+ addVariablesToChart() {
384
+ // Add selected attribute for each filtered entity as chart variable
385
+ // to the selected chart.
386
+ const
387
+ md = this.add_chart_variables_modal,
388
+ ci = CHART_MANAGER.chart_index;
389
+ // Double-check whether chart exists.
390
+ if(ci < 0 || ci >= MODEL.charts.length) {
391
+ console.log('ANOMALY: No chart for index', ci);
392
+ }
393
+ const
394
+ c = MODEL.charts[ci],
395
+ a = md.element('attribute').value,
396
+ s = UI.boxChecked('confirm-add-chart-variables-stacked'),
397
+ enl = [];
398
+ for(const e of this.entities) enl.push(e.name);
399
+ enl.sort((a, b) => UI.compareFullNames(a, b, true));
400
+ for(const en of enl) {
401
+ const vi = c.addVariable(en, a);
402
+ if(vi !== null) c.variables[vi].stacked = s;
403
+ }
404
+ CHART_MANAGER.updateDialog();
405
+ md.hide();
406
+ }
407
+
339
408
  updateRightPane() {
340
409
  const
341
410
  se = this.selected_entity,
@@ -352,10 +421,7 @@ class Finder {
352
421
  if(se.cluster) occ.push(se.cluster.identifier);
353
422
  } else if(se instanceof Product) {
354
423
  // Products "occur" in clusters where they have a position.
355
- const cl = se.productPositionClusters;
356
- for(let i = 0; i < cl.length; i++) {
357
- occ.push(cl[i].identifier);
358
- }
424
+ for(const c of se.productPositionClusters) occ.push(c.identifier);
359
425
  } else if(se instanceof Actor) {
360
426
  // Actors "occur" in clusters where they "own" processes or clusters.
361
427
  for(let k in MODEL.processes) if(MODEL.processes.hasOwnProperty(k)) {
@@ -374,13 +440,12 @@ class Finder {
374
440
  // NOTE: No "occurrence" of datasets or equations.
375
441
  // @@TO DO: identify MODULES (?)
376
442
  // All entities can also occur as chart variables.
377
- for(let j = 0; j < MODEL.charts.length; j++) {
378
- const c = MODEL.charts[j];
379
- for(let k = 0; k < c.variables.length; k++) {
380
- const v = c.variables[k];
443
+ for(let ci = 0; ci < MODEL.charts.length; ci++) {
444
+ const c = MODEL.charts[ci];
445
+ for(const v of c.variables) {
381
446
  if(v.object === se || (se instanceof DatasetModifier &&
382
447
  se.identifier === UI.nameToID(v.attribute))) {
383
- occ.push(MODEL.chart_id_prefix + j);
448
+ occ.push(MODEL.chart_id_prefix + ci);
384
449
  break;
385
450
  }
386
451
  }
@@ -437,9 +502,8 @@ class Finder {
437
502
  // Check all notes in clusters for their color expressions and field.
438
503
  for(let k in MODEL.clusters) if(MODEL.clusters.hasOwnProperty(k)) {
439
504
  const c = MODEL.clusters[k];
440
- for(let i = 0; i < c.notes.length; i++) {
441
- const n = c.notes[i];
442
- // Look for entity in both note contents and note color expression
505
+ for(const n of c.notes) {
506
+ // Look for entity in both note contents and note color expression.
443
507
  if(re.test(n.color.text) || re.test(n.contents)) {
444
508
  xal.push('NOTE');
445
509
  xol.push(n.identifier);
@@ -463,11 +527,9 @@ class Finder {
463
527
  const c = MODEL.constraints[k];
464
528
  for(let i = 0; i < c.bound_lines.length; i++) {
465
529
  const bl = c.bound_lines[i];
466
- for(let j = 0; j < bl.selectors.length; j++) {
467
- if(re.test(bl.selectors[j].expression.text)) {
468
- xal.push('I' + (i + 1));
469
- xol.push(c.identifier);
470
- }
530
+ for(const sel of bl.selectors) if(re.test(sel.expression.text)) {
531
+ xal.push('I' + (i + 1));
532
+ xol.push(c.identifier);
471
533
  }
472
534
  }
473
535
  }
@@ -720,28 +782,25 @@ class Finder {
720
782
 
721
783
  copyAttributesToClipboard(shift) {
722
784
  // Copy relevant entity attributes as tab-separated text to clipboard.
723
- // NOTE: All entity types have "get" `attributes` that returns an
785
+ // NOTE: All entity types have "get" method `attributes` that returns an
724
786
  // object that for each defined attribute (and if model has been
725
787
  // solved also each inferred attribute) has a property with its value.
726
- // For dynamic expressions, the expression text is used
788
+ // For dynamic expressions, the expression text is used.
727
789
  const ea_dict = {A: [], B: [], C: [], D: [], E: [], L: [], P: [], Q: []};
728
- let e = this.selected_entity;
790
+ const e = this.selected_entity;
729
791
  if(shift && e) {
730
792
  ea_dict[e.typeLetter].push(e.attributes);
731
793
  } else {
732
- for(let i = 0; i < this.entities.length; i++) {
733
- e = this.entities[i];
734
- ea_dict[e.typeLetter].push(e.attributes);
735
- }
794
+ for(const e of this.entities) ea_dict[e.typeLetter].push(e.attributes);
736
795
  }
737
796
  const
738
797
  seq = ['A', 'B', 'C', 'D', 'E', 'P', 'Q', 'L'],
739
798
  text = [],
740
799
  attr = [];
741
- for(let i = 0; i < seq.length; i++) {
800
+ for(const etl of seq) {
742
801
  const
743
- etl = seq[i],
744
- ead = ea_dict[etl];
802
+ ead = ea_dict[etl],
803
+ atcodes = VM.attribute_codes[etl];
745
804
  if(ead && ead.length > 0) {
746
805
  // No blank line before first entity type.
747
806
  if(text.length > 0) text.push('');
@@ -755,14 +814,9 @@ class Finder {
755
814
  }
756
815
  text.push(ah);
757
816
  attr.length = 0;
758
- for(let i = 0; i < ead.length; i++) {
759
- const
760
- ea = ead[i],
761
- ac = VM.attribute_codes[etl],
762
- al = [ea.name];
763
- for(let j = 0; j < ac.length; j++) {
764
- if(ea.hasOwnProperty(ac[j])) al.push(ea[ac[j]]);
765
- }
817
+ for(const ea of ead) {
818
+ const al = [ea.name];
819
+ for(const ac of atcodes) if(ea.hasOwnProperty(ac)) al.push(ea[ac]);
766
820
  attr.push(al.join('\t'));
767
821
  }
768
822
  attr.sort();
@@ -158,10 +158,8 @@ class ModelAutoSaver {
158
158
  document.getElementById('load-modal').style.display = 'none';
159
159
  // Contruct the table to select from.
160
160
  let html = '';
161
- for(let i = 0; i < this.model_list.length; i++) {
162
- const
163
- m = this.model_list[i],
164
- bytes = UI.sizeInBytes(m.size).split(' ');
161
+ for(const m of this.model_list) {
162
+ const bytes = UI.sizeInBytes(m.size).split(' ');
165
163
  html += ['<tr class="dataset" style="color: gray" ',
166
164
  'onclick="FILE_MANAGER.loadAutoSavedModel(\'',
167
165
  m.name,'\');"><td class="restore-name">', m.name, '</td><td>',
@@ -122,9 +122,7 @@ class GUIMonitor {
122
122
  addProgressBlock(b, err, time) {
123
123
  // Adds a block to the progress bar, and updates the relative block lengths
124
124
  let total_time = 0;
125
- for(let i = 0; i < b; i++) {
126
- total_time += VM.solver_times[i];
127
- }
125
+ for(let i = 0; i < b; i++) total_time += VM.solver_times[i];
128
126
  const
129
127
  n = document.createElement('div'),
130
128
  ssecs = VM.solver_secs[b - 1];
@@ -158,15 +156,13 @@ class GUIMonitor {
158
156
  showBlock(b) {
159
157
  this.shown_block = b;
160
158
  const cn = this.progress_bar.childNodes;
161
- for(let i = 0; i < cn.length; i++) {
162
- cn[i].classList.remove('sel-pb');
163
- }
159
+ for(const n of cn) n.classList.remove('sel-pb');
164
160
  cn[b - 1].classList.add('sel-pb');
165
161
  this.updateContent(this.tab);
166
162
  }
167
163
 
168
164
  updateDialog() {
169
- // Implements default behavior for a draggable/resizable dialog
165
+ // Implements default behavior for a draggable/resizable dialog.
170
166
  this.updateContent(this.tab);
171
167
  }
172
168
 
@@ -199,33 +195,31 @@ class GUIMonitor {
199
195
  }
200
196
 
201
197
  showCallStack(t) {
202
- // Show the error message in the dialog header
203
- // NOTE: prevent showing again when VM detects multiple errors
198
+ // Show the error message in the dialog header.
199
+ // NOTE: Prevent showing again when VM detects multiple errors.
204
200
  if(this.call_stack_shown) return;
205
201
  const
206
202
  csl = VM.call_stack.length,
207
203
  top = VM.call_stack[csl - 1],
208
204
  err = top.vector[t],
209
- // Make separate lists of variable names and their expressions
205
+ // Make separate lists of variable names and their expressions.
210
206
  vlist = [],
211
207
  xlist = [];
212
208
  document.getElementById('call-stack-error').innerHTML =
213
209
  `ERROR at t=${t}: ` + VM.errorMessage(err);
214
- for(let i = 0; i < csl; i++) {
215
- const
216
- x = VM.call_stack[i],
217
- // For equations, only show the attribute
218
- ons = (x.object === MODEL.equations_dataset ? '' :
219
- x.object.displayName + '|');
210
+ for(const x of VM.call_stack) {
211
+ // For equations, only show the attribute.
212
+ const ons = (x.object === MODEL.equations_dataset ? '' :
213
+ x.object.displayName + '|');
220
214
  vlist.push(ons + x.attribute);
221
- // Trim spaces around all object-attribute separators in the expression
215
+ // Trim spaces around all object-attribute separators in the expression.
222
216
  xlist.push(x.text.replace(/\s*\|\s*/g, '|'));
223
217
  }
224
- // Highlight variables where they are used in the expressions
218
+ // Highlight variables where they are used in the expressions.
225
219
  const vcc = UI.chart_colors.length;
226
220
  for(let i = 0; i < xlist.length; i++) {
227
221
  for(let j = 0; j < vlist.length; j++) {
228
- // Ignore selectors, as these may be different per experiment
222
+ // Ignore selectors, as these may be different per experiment.
229
223
  const
230
224
  vnl = vlist[j].split('|'),
231
225
  sel = (vnl.length > 1 ? vnl.pop() : ''),
@@ -236,26 +230,26 @@ class GUIMonitor {
236
230
  xlist[i] = xlist[i].split(vn).join(vnc);
237
231
  }
238
232
  }
239
- // Then also color the variables
233
+ // Then also color the variables.
240
234
  for(let i = 0; i < vlist.length; i++) {
241
235
  vlist[i] = '<span style="font-weight: 600; color: ' +
242
236
  `${UI.chart_colors[i % vcc]}">${vlist[i]}</span>`;
243
237
  }
244
- // Start without indentation
238
+ // Start without indentation.
245
239
  let pad = 0;
246
- // First show the variable being computed
240
+ // First show the variable being computed.
247
241
  const tbl = ['<div>', vlist[0], '</div>'];
248
- // Then iterate upwards over the call stack
242
+ // Then iterate upwards over the call stack.
249
243
  for(let i = 0; i < vlist.length - 1; i++) {
250
- // Show the expression, followed by the next computed variable
244
+ // Show the expression, followed by the next computed variable.
251
245
  tbl.push(['<div class="call-stack-row" style="padding-left: ',
252
246
  pad, 'px"><div class="call-stack-expr">', xlist[i],
253
- '</div><div class="call-stack-vbl">&nbsp;\u2937', vlist[i+1],
247
+ '</div><div class="call-stack-vbl">&nbsp;\u2937', vlist[i + 1],
254
248
  '</div></div>'].join(''));
255
249
  // Increase indentation
256
250
  pad += 8;
257
251
  }
258
- // Show the last expression, highlighting the array-out-of-bounds (if any)
252
+ // Show the last expression, highlighting the array-out-of-bounds (if any).
259
253
  let last_x = xlist[xlist.length - 1],
260
254
  anc = '';
261
255
  if(VM.out_of_bounds_array) {
@@ -265,14 +259,14 @@ class GUIMonitor {
265
259
  }
266
260
  tbl.push('<div class="call-stack-expr" style="padding-left: ' +
267
261
  `${pad}px">${last_x}</div>`);
268
- // Add index-out-of-bounds message if appropriate
262
+ // Add index-out-of-bounds message if appropriate.
269
263
  if(anc) {
270
264
  tbl.push('<div style="color: gray; margin-top: 8px; font-size: 10px">',
271
265
  VM.out_of_bounds_msg.replace(VM.out_of_bounds_array, anc), '</div>');
272
266
  }
273
- // Dump the code for the last expression to the console
267
+ // Dump the code for the last expression to the console.
274
268
  console.log('Code for', top.text, top.code);
275
- // Show the call stack dialog
269
+ // Show the call stack dialog.
276
270
  document.getElementById('call-stack-table').innerHTML = tbl.join('');
277
271
  document.getElementById('call-stack-modal').style.display = 'block';
278
272
  this.call_stack_shown = true;
@@ -284,19 +278,19 @@ class GUIMonitor {
284
278
  }
285
279
 
286
280
  logMessage(block, msg) {
287
- // Appends a solver message to the monitor's messages textarea
281
+ // Append a solver message to the monitor's messages textarea
288
282
  if(this.messages_text.value === VM.no_messages) {
289
- // Erase the "(no messages)" if still showing
283
+ // Erase the "(no messages)" if still showing.
290
284
  this.messages_text.value = '';
291
285
  }
292
286
  if(this.shown_block === 0 && block !== this.last_message_block) {
293
- // Clear text area when starting with new block while no block selected
287
+ // Clear text area when starting with new block while no block selected.
294
288
  this.last_message_block = block;
295
289
  this.messages_text.value = '';
296
290
  }
297
291
  // NOTE: `msg` is appended only if no block has been selected by
298
292
  // clicking on the progress bar, or if the message belongs to the
299
- // selected block
293
+ // selected block.
300
294
  if(this.shown_block === 0 || this.shown_block === block) {
301
295
  this.messages_text.value += msg + '\n';
302
296
  }
@@ -347,8 +341,8 @@ class GUIMonitor {
347
341
  }
348
342
 
349
343
  connectToServer() {
350
- // Prompts for credentials if not connected yet.
351
- // NOTE: No authentication prompt if SOLVER.user_id in `linny-r-config.js`
344
+ // Prompt for credentials if not connected yet.
345
+ // NOTE: No authentication prompt if SOLVER.user_id in `linny-r-config.js`.
352
346
  // is left blank.
353
347
  if(!VM.solver_user) {
354
348
  VM.connected = false;
@@ -414,7 +408,7 @@ UI.logHeapSize(`BEFORE creating post data`);
414
408
  mipgap: MODEL.MIP_gap
415
409
  });
416
410
  UI.logHeapSize(`AFTER creating post data`);
417
- // Immediately free the memory taken up by VM.lines
411
+ // Immediately free the memory taken up by VM.lines.
418
412
  VM.lines = '';
419
413
  UI.logHeapSize(`BEFORE submitting block #${bwr} to solver`);
420
414
  fetch('solver/', pd)
@@ -430,15 +424,15 @@ UI.logHeapSize(`AFTER creating post data`);
430
424
  try {
431
425
  VM.processServerResponse(JSON.parse(data));
432
426
  UI.logHeapSize('After processing results for block #' + this.block_count);
433
- // If no errors, solve next block (if any)
434
- // NOTE: use setTimeout so that this calling function returns,
435
- // and browser can update its DOM to display progress
427
+ // If no errors, solve next block (if any).
428
+ // NOTE: Use setTimeout so that this calling function returns,
429
+ // and browser can update its DOM to display progress.
436
430
  setTimeout(() => VM.solveBlocks(), 1);
437
431
  } catch(err) {
438
- // Log details on the console
432
+ // Log details on the console.
439
433
  console.log('ERROR while parsing JSON:', err);
440
434
  console.log(data);
441
- // Pass summary on to the browser
435
+ // Pass summary on to the browser.
442
436
  const msg = 'ERROR: Unexpected data from server: ' +
443
437
  ellipsedText(data);
444
438
  this.logMessage(this.block_count, msg);