linny-r 1.4.1 → 1.4.3

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.
@@ -2789,13 +2789,15 @@ class Paper {
2789
2789
  } // END of class Paper
2790
2790
 
2791
2791
 
2792
- // CLASS ModalDialog provides basic modal dialog functionality
2792
+ // CLASS ModalDialog provides basic modal dialog functionality.
2793
2793
  class ModalDialog {
2794
2794
  constructor(id) {
2795
2795
  this.id = id;
2796
2796
  this.modal = document.getElementById(id + '-modal');
2797
2797
  this.dialog = document.getElementById(id + '-dlg');
2798
- // NOTE: dialog button properties will be `undefined` if not in the header
2798
+ // NOTE: Dialog title and button properties will be `undefined` if
2799
+ // not in the header DIV child of the dialog DIV element.
2800
+ this.title = this.dialog.getElementsByClassName('dlg-title')[0];
2799
2801
  this.ok = this.dialog.getElementsByClassName('ok-btn')[0];
2800
2802
  this.cancel = this.dialog.getElementsByClassName('cancel-btn')[0];
2801
2803
  this.info = this.dialog.getElementsByClassName('info-btn')[0];
@@ -3266,9 +3268,13 @@ class GUIController extends Controller {
3266
3268
  // The CHECK UPDATE dialog appears when a new version is detected
3267
3269
  this.check_update_modal = new ModalDialog('check-update');
3268
3270
  this.check_update_modal.ok.addEventListener('click',
3269
- () => UI.shutDownServer());
3271
+ () => UI.shutDownToUpdate());
3270
3272
  this.check_update_modal.cancel.addEventListener('click',
3271
- () => UI.check_update_modal.hide());
3273
+ () => UI.preventUpdate());
3274
+
3275
+ // The UPDATING modal appears when updating has started.
3276
+ // NOTE: This modal has no OK or CANCEL buttons.
3277
+ this.updating_modal = new ModalDialog('updating');
3272
3278
 
3273
3279
  // Add all draggable stay-on-top dialogs as controller properties
3274
3280
 
@@ -3399,7 +3405,7 @@ class GUIController extends Controller {
3399
3405
  }
3400
3406
 
3401
3407
  drawLinkArrows(cluster, link) {
3402
- // Draw all arrows in `cluster` that represent `link`
3408
+ // Draw all arrows in `cluster` that represent `link`.
3403
3409
  for(let i = 0; i < cluster.arrows.length; i++) {
3404
3410
  const a = cluster.arrows[i];
3405
3411
  if(a.links.indexOf(link) >= 0) this.paper.drawArrow(a);
@@ -3407,19 +3413,124 @@ class GUIController extends Controller {
3407
3413
  }
3408
3414
 
3409
3415
  shutDownServer() {
3410
- // Shut down -- this terminates the local host server script
3416
+ // This terminates the local host server script and display a plain
3417
+ // HTML message in the browser with a restart button.
3411
3418
  if(!SOLVER.user_id) window.open('./shutdown', '_self');
3412
3419
  }
3413
3420
 
3421
+ shutDownToUpdate() {
3422
+ // Sisgnal server that an update is required. This will close the
3423
+ // local host Linny-R server. If this server was started by the
3424
+ // standard OS batch script, this script will proceed to update
3425
+ // Linny-R via npm and then restart the server again. If not, the
3426
+ // fetch request will time out, anf the user will be warned.
3427
+ if(SOLVER.user_id) return;
3428
+ fetch('update/')
3429
+ .then((response) => {
3430
+ if(!response.ok) {
3431
+ UI.alert(`ERROR ${response.status}: ${response.statusText}`);
3432
+ }
3433
+ return response.text();
3434
+ })
3435
+ .then((data) => {
3436
+ if(UI.postResponseOK(data, true)) {
3437
+ UI.check_update_modal.hide();
3438
+ if(data.startsWith('Installing')) UI.waitToRestart();
3439
+ }
3440
+ })
3441
+ .catch((err) => {
3442
+ UI.warn(UI.WARNING.NO_CONNECTION, err);
3443
+ });
3444
+ }
3445
+
3446
+ waitToRestart() {
3447
+ // Shows the "update in progress" dialog and then fetches the current
3448
+ // version page from the server. Always wait for 5 seconds to permit
3449
+ // reading the text, and ensure that the server has been stopped.
3450
+ // Only then try to restart.
3451
+ if(SOLVER.user_id) return;
3452
+ UI.updating_modal.show();
3453
+ setTimeout(() => UI.tryToRestart(0), 5000);
3454
+ }
3455
+
3456
+ tryToRestart(trials) {
3457
+ // Fetch the current version number from the server. This may take
3458
+ // a wile, as the server was shut down and restarts only after npm
3459
+ // has updated the Linny-R software. Typically, this takes only a few
3460
+ // seconds, but the connection with the npm server may be slow.
3461
+ // Default timeout on Firefox (90 seconds) and Chrome (300 seconds)
3462
+ // should amply suffice, though, hence no provision for a second attempt.
3463
+ fetch('version/')
3464
+ .then((response) => {
3465
+ if(!response.ok) {
3466
+ UI.alert(`ERROR ${response.status}: ${response.statusText}`);
3467
+ }
3468
+ return response.text();
3469
+ })
3470
+ .then((data) => {
3471
+ if(UI.postResponseOK(data)) {
3472
+ // Change the dialog text in case the user does not confirm
3473
+ // when prompted by the browser to leave the page.
3474
+ const
3475
+ m = data.match(/(\d+\.\d+\.\d+)/),
3476
+ md = UI.updating_modal;
3477
+ md.title.innerText = 'Update terminated';
3478
+ let msg = [];
3479
+ if(m) {
3480
+ msg.push(
3481
+ `Linny-R version ${m[1]} has been installed.`,
3482
+ 'To continue, you must reload this page, and',
3483
+ 'confirm when prompted by your browser.');
3484
+ } else {
3485
+ // Inform user that install appears to have failed.
3486
+ msg.push(
3487
+ 'Installation of new version may <strong>not</strong> have',
3488
+ 'been successful. Please check the CLI for',
3489
+ 'error messages or warnings.');
3490
+ }
3491
+ md.element('msg').innerHTML = msg.join('<br>');
3492
+ // Reload `index.html`. This will start Linny-R anew.
3493
+ window.open('./', '_self');
3494
+ }
3495
+ })
3496
+ .catch((err) => {
3497
+ if(trials < 10) {
3498
+ setTimeout(() => UI.tryToRestart(trials + 1), 5000);
3499
+ } else {
3500
+ UI.warn(UI.WARNING.NO_CONNECTION, err);
3501
+ }
3502
+ });
3503
+ }
3504
+
3505
+ preventUpdate() {
3506
+ // Signal server that no update is required.
3507
+ if(SOLVER.user_id) return;
3508
+ fetch('no-update/')
3509
+ .then((response) => {
3510
+ if(!response.ok) {
3511
+ UI.alert(`ERROR ${response.status}: ${response.statusText}`);
3512
+ }
3513
+ return response.text();
3514
+ })
3515
+ .then((data) => {
3516
+ if(UI.postResponseOK(data, true)) UI.check_update_modal.hide();
3517
+ })
3518
+ .catch((err) => {
3519
+ UI.warn(UI.WARNING.NO_CONNECTION, err);
3520
+ UI.check_update_modal.hide();
3521
+ });
3522
+ }
3523
+
3414
3524
  loginPrompt() {
3415
- // Show the server logon modal
3525
+ // Show the server logon modal.
3416
3526
  this.modals.logon.element('name').value = SOLVER.user_id;
3417
3527
  this.modals.logon.element('password').value = '';
3418
3528
  this.modals.logon.show('password');
3419
3529
  }
3420
3530
 
3421
3531
  rotatingIcon(rotate=false) {
3422
- // Controls the appearance of the Linny-R icon (top-left in browser window)
3532
+ // Controls the appearance of the Linny-R icon in the top-left
3533
+ // corner of the browser window.
3423
3534
  const
3424
3535
  si = document.getElementById('static-icon'),
3425
3536
  ri = document.getElementById('rotating-icon');
@@ -3433,47 +3544,47 @@ class GUIController extends Controller {
3433
3544
  }
3434
3545
 
3435
3546
  updateTimeStep(t=MODEL.simulationTimeStep) {
3436
- // Displays `t` as the current time step
3437
- // NOTE: the Virtual Machine passes its relative time VM.t
3547
+ // Display `t` as the current time step.
3548
+ // NOTE: The Virtual Machine passes its relative time `VM.t`.
3438
3549
  document.getElementById('step').innerHTML = t;
3439
3550
  }
3440
3551
 
3441
3552
  stopSolving() {
3442
- // Reset solver-related GUI elements and notify modeler
3553
+ // Reset solver-related GUI elements and notify modeler.
3443
3554
  super.stopSolving();
3444
3555
  this.buttons.solve.classList.remove('off');
3445
3556
  this.buttons.stop.classList.remove('blink');
3446
3557
  this.buttons.stop.classList.add('off');
3447
3558
  this.rotatingIcon(false);
3448
- // Update the time step on the status bar
3559
+ // Update the time step on the status bar.
3449
3560
  this.updateTimeStep();
3450
3561
  }
3451
3562
 
3452
3563
  readyToSolve() {
3453
- // Set Stop and Reset buttons to their initial state
3564
+ // Set Stop and Reset buttons to their initial state.
3454
3565
  UI.buttons.stop.classList.remove('blink');
3455
3566
  // Hide the reset button
3456
3567
  UI.buttons.reset.classList.add('off');
3457
3568
  }
3458
3569
 
3459
3570
  startSolving() {
3460
- // Hide Start button and show Stop button
3571
+ // Hide Start button and show Stop button.
3461
3572
  UI.buttons.solve.classList.add('off');
3462
3573
  UI.buttons.stop.classList.remove('off');
3463
3574
  }
3464
3575
 
3465
3576
  waitToStop() {
3466
- // Make Stop button blink to indicate "halting -- please wait"
3577
+ // Make Stop button blink to indicate "halting -- please wait".
3467
3578
  UI.buttons.stop.classList.add('blink');
3468
3579
  }
3469
3580
 
3470
3581
  readyToReset() {
3471
- // Show the Reset button
3582
+ // Show the Reset button.
3472
3583
  UI.buttons.reset.classList.remove('off');
3473
3584
  }
3474
3585
 
3475
3586
  reset() {
3476
- // Reset properties related to cursor position on diagram
3587
+ // Reset properties related to cursor position on diagram.
3477
3588
  this.on_node = null;
3478
3589
  this.on_arrow = null;
3479
3590
  this.on_cluster = null;
@@ -3527,7 +3638,7 @@ class GUIController extends Controller {
3527
3638
  jumpToIssue() {
3528
3639
  // Set time step to the one of the warning message for the issue
3529
3640
  // index, redraw the diagram if needed, and display the message
3530
- // on the infoline
3641
+ // on the infoline.
3531
3642
  if(VM.issue_index >= 0) {
3532
3643
  const
3533
3644
  issue = VM.issue_list[VM.issue_index],
@@ -7356,7 +7467,8 @@ NOTE: Grouping groups results in a single group, e.g., (1;2);(3;4;5) evaluates a
7356
7467
  // is passed to differentiate between the DOM elements to be used
7357
7468
  const
7358
7469
  type = document.getElementById(prefix + 'variable-obj').value,
7359
- n_list = this.namesByType(VM.object_types[type]).sort(ciCompare),
7470
+ n_list = this.namesByType(VM.object_types[type]).sort(
7471
+ (a, b) => UI.compareFullNames(a, b)),
7360
7472
  vn = document.getElementById(prefix + 'variable-name'),
7361
7473
  options = [];
7362
7474
  // Add "empty" as first and initial option, but disable it.
@@ -7398,7 +7510,7 @@ NOTE: Grouping groups results in a single group, e.g., (1;2);(3;4;5) evaluates a
7398
7510
  slist.push(d.modifiers[m].selector);
7399
7511
  }
7400
7512
  // Sort to present equations in alphabetical order
7401
- slist.sort(ciCompare);
7513
+ slist.sort((a, b) => UI.compareFullNames(a, b));
7402
7514
  for(let i = 0; i < slist.length; i++) {
7403
7515
  options.push(`<option value="${slist[i]}">${slist[i]}</option>`);
7404
7516
  }
@@ -9900,13 +10012,7 @@ class GUIDatasetManager extends DatasetManager {
9900
10012
  dl = [],
9901
10013
  dnl = [],
9902
10014
  sd = this.selected_dataset,
9903
- ioclass = ['', 'import', 'export'],
9904
- ciPrefixCompare = (a, b) => {
9905
- const
9906
- pa = a.split(':_').join(' '),
9907
- pb = b.split(':_').join(' ');
9908
- return ciCompare(pa, pb);
9909
- };
10015
+ ioclass = ['', 'import', 'export'];
9910
10016
  for(let d in MODEL.datasets) if(MODEL.datasets.hasOwnProperty(d) &&
9911
10017
  // NOTE: do not list "black-boxed" entities
9912
10018
  !d.startsWith(UI.BLACK_BOX) &&
@@ -9917,7 +10023,7 @@ class GUIDatasetManager extends DatasetManager {
9917
10023
  dnl.push(d);
9918
10024
  }
9919
10025
  }
9920
- dnl.sort(ciPrefixCompare);
10026
+ dnl.sort((a, b) => UI.compareFullNames(a, b, true));
9921
10027
  // First determine indentation levels, prefixes and names
9922
10028
  const
9923
10029
  indent = [],
@@ -10678,7 +10784,7 @@ class GUIDatasetManager extends DatasetManager {
10678
10784
  const
10679
10785
  ln = document.getElementById('series-line-number'),
10680
10786
  lc = document.getElementById('series-line-count');
10681
- ln.innerHTML = this.series_data.value.substr(0,
10787
+ ln.innerHTML = this.series_data.value.substring(0,
10682
10788
  this.series_data.selectionStart).split('\n').length;
10683
10789
  lc.innerHTML = this.series_data.value.split('\n').length;
10684
10790
  }
@@ -12190,7 +12296,7 @@ class GUISensitivityAnalysis extends SensitivityAnalysis {
12190
12296
  const
12191
12297
  ds_dict = MODEL.listOfAllSelectors,
12192
12298
  html = [],
12193
- sl = Object.keys(ds_dict).sort(ciCompare);
12299
+ sl = Object.keys(ds_dict).sort((a, b) => UI.compareFullNames(a, b, true));
12194
12300
  for(let i = 0; i < sl.length; i++) {
12195
12301
  const
12196
12302
  s = sl[i],
@@ -13175,7 +13281,7 @@ class GUIExperimentManager extends ExperimentManager {
13175
13281
  for(let i = 0; i < x.variables.length; i++) {
13176
13282
  addDistinct(x.variables[i].displayName, vl);
13177
13283
  }
13178
- vl.sort(ciCompare);
13284
+ vl.sort((a, b) => UI.compareFullNames(a, b));
13179
13285
  for(let i = 0; i < vl.length; i++) {
13180
13286
  ol.push(['<option value="', vl[i], '"',
13181
13287
  (vl[i] == x.selected_variable ? ' selected="selected"' : ''),
@@ -15336,9 +15442,11 @@ class Finder {
15336
15442
  this.close_btn = document.getElementById('finder-close-btn');
15337
15443
  // Make toolbar buttons responsive
15338
15444
  this.close_btn.addEventListener('click', (e) => UI.toggleDialog(e));
15339
- this.entities = [];
15340
15445
  this.filter_input = document.getElementById('finder-filter-text');
15341
15446
  this.filter_input.addEventListener('input', () => FINDER.changeFilter());
15447
+ this.edit_btn = document.getElementById('finder-edit-btn');
15448
+ this.edit_btn.addEventListener(
15449
+ 'click', (event) => FINDER.editAttributes(event.shiftKey));
15342
15450
  this.copy_btn = document.getElementById('finder-copy-btn');
15343
15451
  this.copy_btn.addEventListener(
15344
15452
  'click', (event) => FINDER.copyAttributesToClipboard(event.shiftKey));
@@ -15346,7 +15454,7 @@ class Finder {
15346
15454
  this.item_table = document.getElementById('finder-item-table');
15347
15455
  this.expression_table = document.getElementById('finder-expression-table');
15348
15456
 
15349
- // Attribute headers are used by Finder to output entity attribute values
15457
+ // Attribute headers are used by Finder to output entity attribute values.
15350
15458
  this.attribute_headers = {
15351
15459
  A: 'ACTORS:\tWeight\tCash IN\tCash OUT\tCash FLOW',
15352
15460
  B: 'CONSTRAINTS (no attributes)',
@@ -15359,12 +15467,15 @@ class Finder {
15359
15467
  Q: 'PRODUCTS:\tLower bound\tUpper bound\tInitial level\tPrice' +
15360
15468
  '\tLevel\tCost price\tHighest cost price'
15361
15469
  };
15362
- // Set own properties
15470
+ // Set own properties.
15471
+ this.entities = [];
15472
+ this.filtered_types = [];
15363
15473
  this.reset();
15364
15474
  }
15365
15475
 
15366
15476
  reset() {
15367
15477
  this.entities.length = 0;
15478
+ this.filtered_types.length = 0;
15368
15479
  this.selected_entity = null;
15369
15480
  this.filter_input.value = '';
15370
15481
  this.filter_pattern = null;
@@ -15373,7 +15484,7 @@ class Finder {
15373
15484
  this.last_time_clicked = 0;
15374
15485
  this.clicked_object = null;
15375
15486
  // Product cluster index "remembers" for which cluster a product was
15376
- // last revealed, so it can reveal the next cluster when clicked again
15487
+ // last revealed, so it can reveal the next cluster when clicked again.
15377
15488
  this.product_cluster_index = 0;
15378
15489
  }
15379
15490
 
@@ -15383,7 +15494,7 @@ class Finder {
15383
15494
  dt = now - this.last_time_clicked;
15384
15495
  this.last_time_clicked = now;
15385
15496
  if(obj === this.clicked_object) {
15386
- // Consider click to be "double" if it occurred less than 300 ms ago
15497
+ // Consider click to be "double" if it occurred less than 300 ms ago.
15387
15498
  if(dt < 300) {
15388
15499
  this.last_time_clicked = 0;
15389
15500
  return true;
@@ -15427,6 +15538,7 @@ class Finder {
15427
15538
  fp = this.filter_pattern && this.filter_pattern.length > 0;
15428
15539
  let imgs = '';
15429
15540
  this.entities.length = 0;
15541
+ this.filtered_types.length = 0;
15430
15542
  // No list unless a pattern OR a specified SUB-set of entity types
15431
15543
  if(fp || et && et !== VM.entity_letters) {
15432
15544
  if(et.indexOf('A') >= 0) {
@@ -15435,6 +15547,7 @@ class Finder {
15435
15547
  if(!fp || patternMatch(MODEL.actors[k].name, this.filter_pattern)) {
15436
15548
  enl.push(k);
15437
15549
  this.entities.push(MODEL.actors[k]);
15550
+ addDistinct('A', this.filtered_types);
15438
15551
  }
15439
15552
  }
15440
15553
  }
@@ -15446,6 +15559,7 @@ class Finder {
15446
15559
  MODEL.processes[k].displayName, this.filter_pattern))) {
15447
15560
  enl.push(k);
15448
15561
  this.entities.push(MODEL.processes[k]);
15562
+ addDistinct('P', this.filtered_types);
15449
15563
  }
15450
15564
  }
15451
15565
  }
@@ -15456,6 +15570,7 @@ class Finder {
15456
15570
  MODEL.products[k].displayName, this.filter_pattern))) {
15457
15571
  enl.push(k);
15458
15572
  this.entities.push(MODEL.products[k]);
15573
+ addDistinct('Q', this.filtered_types);
15459
15574
  }
15460
15575
  }
15461
15576
  }
@@ -15466,6 +15581,7 @@ class Finder {
15466
15581
  MODEL.clusters[k].displayName, this.filter_pattern))) {
15467
15582
  enl.push(k);
15468
15583
  this.entities.push(MODEL.clusters[k]);
15584
+ addDistinct('C', this.filtered_types);
15469
15585
  }
15470
15586
  }
15471
15587
  }
@@ -15479,6 +15595,7 @@ class Finder {
15479
15595
  if(ds !== MODEL.equations_dataset) {
15480
15596
  enl.push(k);
15481
15597
  this.entities.push(MODEL.datasets[k]);
15598
+ addDistinct('D', this.filtered_types);
15482
15599
  }
15483
15600
  }
15484
15601
  }
@@ -15492,6 +15609,7 @@ class Finder {
15492
15609
  this.filter_pattern)) {
15493
15610
  enl.push(k);
15494
15611
  this.entities.push(MODEL.equations_dataset.modifiers[k]);
15612
+ addDistinct('E', this.filtered_types);
15495
15613
  }
15496
15614
  }
15497
15615
  }
@@ -15508,6 +15626,7 @@ class Finder {
15508
15626
  if(!bb && (!fp || patternMatch(ldn, this.filter_pattern))) {
15509
15627
  enl.push(k);
15510
15628
  this.entities.push(l);
15629
+ addDistinct('L', this.filtered_types);
15511
15630
  }
15512
15631
  }
15513
15632
  }
@@ -15520,11 +15639,12 @@ class Finder {
15520
15639
  MODEL.constraints[k].displayName, this.filter_pattern))) {
15521
15640
  enl.push(k);
15522
15641
  this.entities.push(MODEL.constraints[k]);
15642
+ addDistinct('B', this.filtered_types);
15523
15643
  }
15524
15644
  }
15525
15645
  }
15526
15646
  }
15527
- enl.sort(ciCompare);
15647
+ enl.sort((a, b) => UI.compareFullNames(a, b, true));
15528
15648
  }
15529
15649
  document.getElementById('finder-entity-imgs').innerHTML = imgs;
15530
15650
  let seid = 'etr';
@@ -15545,10 +15665,24 @@ class Finder {
15545
15665
  UI.scrollIntoView(document.getElementById(seid));
15546
15666
  document.getElementById('finder-count').innerHTML = pluralS(
15547
15667
  el.length, 'entity', 'entities');
15548
- if(el.length > 0) {
15668
+ // Only show the edit button if all filtered entities are of the
15669
+ // same type.
15670
+ let n = el.length;
15671
+ this.edit_btn.style.display = 'none';
15672
+ this.copy_btn.style.display = 'none';
15673
+ if(n > 0) {
15549
15674
  this.copy_btn.style.display = 'block';
15550
- } else {
15551
- this.copy_btn.style.display = 'none';
15675
+ const ft = this.filtered_types[0];
15676
+ if(this.filtered_types.length === 1 && 'DE'.indexOf(ft) < 0) {
15677
+ // NOTE: Attributes of "no actor" and top cluster cannot be edited.
15678
+ if((ft === 'A' && enl.indexOf('(no_actor)') >= 0) ||
15679
+ (ft === 'C' && enl.indexOf('(top_cluster)') >= 0)) n--;
15680
+ if(n > 0) {
15681
+ this.edit_btn.title = 'Edit attributes of ' +
15682
+ pluralS(n, VM.entity_names[ft]);
15683
+ this.edit_btn.style.display = 'block';
15684
+ }
15685
+ }
15552
15686
  }
15553
15687
  this.updateRightPane();
15554
15688
  }
@@ -15774,6 +15908,10 @@ class Finder {
15774
15908
  UI.showProductPropertiesDialog(obj);
15775
15909
  } else if(obj instanceof Link) {
15776
15910
  UI.showLinkPropertiesDialog(obj);
15911
+ } else if(obj instanceof Cluster && obj !== MODEL.top_cluster) {
15912
+ UI.showClusterPropertiesDialog(obj);
15913
+ } else if(obj instanceof Actor) {
15914
+ ACTOR_MANAGER.showEditActorDialog(obj.name, obj.weight.text);
15777
15915
  } else if(obj instanceof Note) {
15778
15916
  obj.showNotePropertiesDialog();
15779
15917
  } else if(obj instanceof Dataset) {
@@ -15889,12 +16027,19 @@ class Finder {
15889
16027
  }
15890
16028
  }
15891
16029
 
16030
+ editAttributes(shift) {
16031
+ // Show the Edit properties dialog for the filtered-out entities.
16032
+ // These must all be of the same type, or the edit button will not
16033
+ // show. Just in case, check anyway.
16034
+
16035
+ }
16036
+
15892
16037
  copyAttributesToClipboard(shift) {
15893
- // Copy relevant entity attributes as tab-separated text to clipboard
15894
- // NOTE: all entity types have "get" `attributes` that returns an object
15895
- // that for each defined attribute (and if model has been solved also each
15896
- // inferred attribute) has a property with its value. For dynamic
15897
- // expressions, the expression text is used
16038
+ // Copy relevant entity attributes as tab-separated text to clipboard.
16039
+ // NOTE: All entity types have "get" `attributes` that returns an
16040
+ // object that for each defined attribute (and if model has been
16041
+ // solved also each inferred attribute) has a property with its value.
16042
+ // For dynamic expressions, the expression text is used
15898
16043
  const ea_dict = {A: [], B: [], C: [], D: [], E: [], L: [], P: [], Q: []};
15899
16044
  let e = this.selected_entity;
15900
16045
  if(shift && e) {
@@ -15914,18 +16059,15 @@ class Finder {
15914
16059
  etl = seq[i],
15915
16060
  ead = ea_dict[etl];
15916
16061
  if(ead && ead.length > 0) {
15917
- // No blank line before first entity type
16062
+ // No blank line before first entity type.
15918
16063
  if(text.length > 0) text.push('');
15919
- let ah = this.attribute_letters[etl];
16064
+ const en = capitalized(VM.entity_names[etl]);
16065
+ let ah = en + '\t' + VM.entity_attribute_names[etl].join('\t');
16066
+ if(etl === 'L' || etl === 'B') ah = ah.replace(en, `${en} FROM\tTO`);
15920
16067
  if(!MODEL.infer_cost_prices) {
15921
- // No cost price calculation => trim associated attributes from header
15922
- let p = ah.indexOf('\tCost price');
15923
- if(p > 0) {
15924
- ah = ah.substr(0, p);
15925
- } else {
15926
- // SOC is exogenous, and hence comes before F in header => replace
15927
- ah = ah.replace('\tShare of cost', '');
15928
- }
16068
+ // If no cost price calculation, trim associated attributes
16069
+ // from the header.
16070
+ ah = ah.replace('\tCost price', '').replace('\tShare of cost', '');
15929
16071
  }
15930
16072
  text.push(ah);
15931
16073
  attr.length = 0;
@@ -16191,7 +16333,7 @@ class GUIReceiver {
16191
16333
  return response.text();
16192
16334
  })
16193
16335
  .then((data) => {
16194
- // For experiments, only display server response if warning or error
16336
+ // For experiments, only display server response if warning or error.
16195
16337
  UI.postResponseOK(data, !RECEIVER.experiment);
16196
16338
  // If execution completed, perform the call-back action if the
16197
16339
  // receiver is active (so not when auto-reporting a run).