linny-r 1.9.3 → 2.0.2

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 (39) hide show
  1. package/LICENSE +1 -1
  2. package/README.md +172 -126
  3. package/console.js +2 -1
  4. package/package.json +1 -1
  5. package/post-install.js +93 -37
  6. package/server.js +73 -29
  7. package/static/images/eq-negated.png +0 -0
  8. package/static/images/power.png +0 -0
  9. package/static/images/tex.png +0 -0
  10. package/static/index.html +226 -11
  11. package/static/linny-r.css +458 -8
  12. package/static/scripts/linny-r-ctrl.js +6 -4
  13. package/static/scripts/linny-r-gui-actor-manager.js +1 -1
  14. package/static/scripts/linny-r-gui-chart-manager.js +20 -13
  15. package/static/scripts/linny-r-gui-constraint-editor.js +410 -50
  16. package/static/scripts/linny-r-gui-controller.js +138 -21
  17. package/static/scripts/linny-r-gui-dataset-manager.js +28 -20
  18. package/static/scripts/linny-r-gui-documentation-manager.js +11 -3
  19. package/static/scripts/linny-r-gui-equation-manager.js +1 -1
  20. package/static/scripts/linny-r-gui-experiment-manager.js +1 -1
  21. package/static/scripts/linny-r-gui-expression-editor.js +7 -1
  22. package/static/scripts/linny-r-gui-file-manager.js +63 -19
  23. package/static/scripts/linny-r-gui-finder.js +1 -1
  24. package/static/scripts/linny-r-gui-model-autosaver.js +1 -1
  25. package/static/scripts/linny-r-gui-monitor.js +1 -1
  26. package/static/scripts/linny-r-gui-paper.js +108 -25
  27. package/static/scripts/linny-r-gui-power-grid-manager.js +529 -0
  28. package/static/scripts/linny-r-gui-receiver.js +1 -1
  29. package/static/scripts/linny-r-gui-repository-browser.js +1 -1
  30. package/static/scripts/linny-r-gui-scale-unit-manager.js +1 -1
  31. package/static/scripts/linny-r-gui-sensitivity-analysis.js +1 -1
  32. package/static/scripts/linny-r-gui-tex-manager.js +110 -0
  33. package/static/scripts/linny-r-gui-undo-redo.js +1 -1
  34. package/static/scripts/linny-r-milp.js +1 -1
  35. package/static/scripts/linny-r-model.js +982 -123
  36. package/static/scripts/linny-r-utils.js +3 -3
  37. package/static/scripts/linny-r-vm.js +731 -252
  38. package/static/show-diff.html +1 -1
  39. package/static/show-png.html +1 -1
@@ -13,7 +13,7 @@ handler functions.
13
13
  */
14
14
 
15
15
  /*
16
- Copyright (c) 2017-2023 Delft University of Technology
16
+ Copyright (c) 2017-2024 Delft University of Technology
17
17
 
18
18
  Permission is hereby granted, free of charge, to any person obtaining a copy
19
19
  of this software and associated documentation files (the "Software"), to deal
@@ -421,7 +421,7 @@ class GUIController extends Controller {
421
421
  this.edit_btns = ['clone', 'paste', 'delete', 'undo', 'redo'];
422
422
  this.model_btns = ['settings', 'save', 'repository', 'actors',
423
423
  'dataset', 'equation', 'chart', 'sensitivity', 'experiment',
424
- 'diagram', 'savediagram', 'finder', 'monitor', 'solve'];
424
+ 'diagram', 'savediagram', 'finder', 'monitor', 'tex', 'solve'];
425
425
  this.other_btns = ['new', 'load', 'receiver', 'documentation',
426
426
  'parent', 'lift', 'solve', 'stop', 'reset', 'zoomin', 'zoomout',
427
427
  'stepback', 'stepforward', 'autosave', 'recall'];
@@ -549,7 +549,7 @@ class GUIController extends Controller {
549
549
  this.buttons.settings.addEventListener('click',
550
550
  () => UI.showSettingsDialog(MODEL));
551
551
  this.buttons.save.addEventListener('click',
552
- () => FILE_MANAGER.saveModel());
552
+ () => FILE_MANAGER.saveModel(event.shiftKey));
553
553
  this.buttons.actors.addEventListener('click',
554
554
  () => ACTOR_MANAGER.showDialog());
555
555
  this.buttons.diagram.addEventListener('click',
@@ -569,6 +569,7 @@ class GUIController extends Controller {
569
569
  this.buttons.finder.addEventListener('click', tdf);
570
570
  this.buttons.monitor.addEventListener('click', tdf);
571
571
  this.buttons.documentation.addEventListener('click', tdf);
572
+ this.buttons.tex.addEventListener('click', tdf);
572
573
  // Cluster navigation elements:
573
574
  this.focal_name.addEventListener('click',
574
575
  () => UI.showClusterPropertiesDialog(MODEL.focal_cluster));
@@ -717,6 +718,10 @@ class GUIController extends Controller {
717
718
  () => SCALE_UNIT_MANAGER.show());
718
719
  this.modals.settings.element('solver-prefs-btn').addEventListener('click',
719
720
  () => UI.showSolverPreferencesDialog());
721
+ this.modals.settings.element('power').addEventListener('click',
722
+ () => UI.togglePowerGridButton());
723
+ this.modals.settings.element('power-btn').addEventListener('click',
724
+ () => POWER_GRID_MANAGER.show());
720
725
  // Make solver modal elements responsive.
721
726
  this.modals.solver.ok.addEventListener('click',
722
727
  () => UI.updateSolverPreferences());
@@ -773,7 +778,12 @@ class GUIController extends Controller {
773
778
  this.modals.process.element('UB-x').addEventListener('click', eoxedit);
774
779
  this.modals.process.element('IL-x').addEventListener('click', eoxedit);
775
780
  this.modals.process.element('LCF-x').addEventListener('click', eoxedit);
776
-
781
+ // Processes can represent power grid elements.
782
+ this.modals.process.element('grid-plate').addEventListener(
783
+ 'mouseenter', () => UI.showGridPlateMenu('process'));
784
+ // Make the grid plate menu responsive.
785
+ this.modals.process.element('grid-plate-menu').addEventListener(
786
+ 'mouseleave', () => UI.hideGridPlateMenu('process'));
777
787
  this.modals.product.ok.addEventListener('click',
778
788
  () => UI.updateProductProperties());
779
789
  this.modals.product.cancel.addEventListener('click',
@@ -1089,10 +1099,10 @@ class GUIController extends Controller {
1089
1099
  // Only then try to restart.
1090
1100
  if(SOLVER.user_id) return;
1091
1101
  UI.updating_modal.show();
1092
- setTimeout(() => UI.tryToRestart(0), 5000);
1102
+ setTimeout(() => UI.tryToRestart(), 5000);
1093
1103
  }
1094
1104
 
1095
- tryToRestart(trials) {
1105
+ tryToRestart() {
1096
1106
  // Fetch the current version number from the server. This may take
1097
1107
  // a wile, as the server was shut down and restarts only after npm
1098
1108
  // has updated the Linny-R software. Typically, this takes only a few
@@ -1122,6 +1132,7 @@ class GUIController extends Controller {
1122
1132
  'confirm when prompted by your browser.');
1123
1133
  // Hide "update" button in server dialog.
1124
1134
  UI.modals.server.element('update').style.display = 'none';
1135
+ return;
1125
1136
  } else {
1126
1137
  // Inform user that install appears to have failed.
1127
1138
  msg.push(
@@ -1135,11 +1146,7 @@ class GUIController extends Controller {
1135
1146
  }
1136
1147
  })
1137
1148
  .catch((err) => {
1138
- if(trials < 10) {
1139
- setTimeout(() => UI.tryToRestart(trials + 1), 5000);
1140
- } else {
1141
- UI.warn(UI.WARNING.NO_CONNECTION, err);
1142
- }
1149
+ UI.warn(UI.WARNING.NO_CONNECTION, err);
1143
1150
  });
1144
1151
  }
1145
1152
 
@@ -2242,8 +2249,9 @@ class GUIController extends Controller {
2242
2249
  while(i < inp.length && inp[i].disabled) i++;
2243
2250
  if(i < inp.length) {
2244
2251
  inp[i].focus();
2245
- } else if('constraint-modal xp-clusters-modal'.indexOf(topmod.id) >= 0) {
2246
- // NOTE: Constraint modal and "ignore clusters" modal must NOT close
2252
+ } else if(['constraint-modal', 'boundline-data-modal',
2253
+ 'xp-clusters-modal'].indexOf(topmod.id) >= 0) {
2254
+ // NOTE: Constraint modal, boundline data modal and "ignore clusters" modal must NOT close
2247
2255
  // when Enter is pressed, but only de-focus the input field.
2248
2256
  e.target.blur();
2249
2257
  } else {
@@ -2299,9 +2307,14 @@ class GUIController extends Controller {
2299
2307
  } else if(code === 'ArrowRight') {
2300
2308
  e.preventDefault();
2301
2309
  this.stepForward(e);
2310
+ } else if(e.ctrlKey && code === 'KeyS') {
2311
+ // Ctrl-S means: save model. Treat separately because Shift-key
2312
+ // alters the way in which the model file is saved.
2313
+ e.preventDefault();
2314
+ FILE_MANAGER.saveModel(e.shiftKey);
2302
2315
  } else if(alt && code === 'KeyR') {
2303
2316
  // Alt-R means: run to diagnose infeasible/unbounded problem.
2304
- VM.solveModel(true);
2317
+ VM.solveModel(true);
2305
2318
  } else if(alt && ['KeyC', 'KeyM'].indexOf(code) >= 0) {
2306
2319
  // Special shortcut keys for "clone selection" and "model settings".
2307
2320
  const be = new Event('click');
@@ -2763,8 +2776,8 @@ class GUIController extends Controller {
2763
2776
  }
2764
2777
 
2765
2778
  hideStayOnTopDialogs() {
2766
- // Hide and reset all stay-on-top dialogs (even when not showing)
2767
- // NOTE: this routine is called when a new model is loaded
2779
+ // Hide and reset all stay-on-top dialogs (even when not showing).
2780
+ // NOTE: This routine is called when a new model is loaded.
2768
2781
  DATASET_MANAGER.dialog.style.display = 'none';
2769
2782
  this.buttons.dataset.classList.remove('stay-activ');
2770
2783
  DATASET_MANAGER.reset();
@@ -2786,6 +2799,9 @@ class GUIController extends Controller {
2786
2799
  DOCUMENTATION_MANAGER.dialog.style.display = 'none';
2787
2800
  this.buttons.documentation.classList.remove('stay-activ');
2788
2801
  DOCUMENTATION_MANAGER.reset();
2802
+ TEX_MANAGER.dialog.style.display = 'none';
2803
+ this.buttons.tex.classList.remove('stay-activ');
2804
+ TEX_MANAGER.reset();
2789
2805
  FINDER.dialog.style.display = 'none';
2790
2806
  this.buttons.finder.classList.remove('stay-activ');
2791
2807
  FINDER.reset();
@@ -3558,10 +3574,13 @@ console.log('HERE name conflicts', name_conflicts, mapping);
3558
3574
  this.setBox('settings-decimal-comma', model.decimal_comma);
3559
3575
  this.setBox('settings-align-to-grid', model.align_to_grid);
3560
3576
  this.setBox('settings-block-arrows', model.show_block_arrows);
3561
- this.setBox('settings-diagnose', MODEL.always_diagnose);
3577
+ this.setBox('settings-diagnose', model.always_diagnose);
3578
+ this.setBox('settings-power', model.with_power_flow);
3562
3579
  this.setBox('settings-cost-prices', model.infer_cost_prices);
3563
3580
  this.setBox('settings-report-results', model.report_results);
3564
3581
  this.setBox('settings-encrypt', model.encrypt);
3582
+ const pg_btn = md.element('power-btn');
3583
+ pg_btn.style.display = (model.with_power_flow ? 'inline-block' : 'none');
3565
3584
  md.show('name');
3566
3585
  }
3567
3586
 
@@ -3616,9 +3635,9 @@ console.log('HERE name conflicts', name_conflicts, mapping);
3616
3635
  model.report_results = UI.boxChecked('settings-report-results');
3617
3636
  model.encrypt = UI.boxChecked('settings-encrypt');
3618
3637
  model.decimal_comma = UI.boxChecked('settings-decimal-comma');
3619
- MODEL.always_diagnose = this.boxChecked('settings-diagnose');
3638
+ model.always_diagnose = this.boxChecked('settings-diagnose');
3620
3639
  // Notify modeler that diagnosis changes the value of +INF.
3621
- if(MODEL.always_diagnose) {
3640
+ if(model.always_diagnose) {
3622
3641
  UI.notify('To diagnose unbounded problems, values beyond 1e+10 ' +
3623
3642
  'are considered as infinite (\u221E)');
3624
3643
  }
@@ -3627,6 +3646,9 @@ console.log('HERE name conflicts', name_conflicts, mapping);
3627
3646
  redraw = !model.align_to_grid && cb;
3628
3647
  model.align_to_grid = cb;
3629
3648
  model.grid_pixels = Math.floor(px);
3649
+ cb = UI.boxChecked('settings-power');
3650
+ redraw = redraw || cb !== model.with_power_flow;
3651
+ model.with_power_flow = cb;
3630
3652
  cb = UI.boxChecked('settings-cost-prices');
3631
3653
  redraw = redraw || cb !== model.infer_cost_prices;
3632
3654
  model.infer_cost_prices = cb;
@@ -3674,6 +3696,20 @@ console.log('HERE name conflicts', name_conflicts, mapping);
3674
3696
  if(redraw) this.drawDiagram(model);
3675
3697
  }
3676
3698
 
3699
+ togglePowerGridButton() {
3700
+ // Responds to clicking the "power grid options" checkbox by toggling
3701
+ // the "View/edit power grids" button.
3702
+ const
3703
+ cb = this.modals.settings.element('power'),
3704
+ pb = this.modals.settings.element('power-btn');
3705
+ // NOTE: When clicked, state has not been updated yet.
3706
+ if(cb.classList.contains('clear')) {
3707
+ pb.style.display = 'inline-block';
3708
+ } else {
3709
+ pb.style.display = 'none';
3710
+ }
3711
+ }
3712
+
3677
3713
  // Solver preferences modal
3678
3714
 
3679
3715
  showSolverPreferencesDialog() {
@@ -3749,6 +3785,17 @@ console.log('HERE name conflicts', name_conflicts, mapping);
3749
3785
  md.group = group;
3750
3786
  md.element('name').value = p.name;
3751
3787
  md.element('actor').value = (p.hasActor ? p.actor.name : '');
3788
+ md.element('length').value = p.length_in_km;
3789
+ md.grid_id = (p.grid ? p.grid.id : '');
3790
+ this.hideGridPlateMenu('process');
3791
+ this.updateGridFields();
3792
+ const tex = md.element('tex-id');
3793
+ tex.value = p.TEX_id;
3794
+ if(TEX_MANAGER.visible) {
3795
+ tex.style.display = 'block';
3796
+ } else {
3797
+ tex.style.display = 'none';
3798
+ }
3752
3799
  // Focus on lower bound when showing the dialog for a group.
3753
3800
  if(group.length > 0) {
3754
3801
  attr = 'LB';
@@ -3766,6 +3813,61 @@ console.log('HERE name conflicts', name_conflicts, mapping);
3766
3813
  md.element(attr + '-x').dispatchEvent(new Event('click'));
3767
3814
  }
3768
3815
  }
3816
+
3817
+ showGridPlateMenu(modal) {
3818
+ const md = this.modals[modal];
3819
+ POWER_GRID_MANAGER.updateGridMenu(modal);
3820
+ md.element('grid-plate-menu').style.display = 'block';
3821
+ }
3822
+
3823
+ hideGridPlateMenu(modal) {
3824
+ const md = this.modals[modal];
3825
+ md.element('grid-plate-menu').style.display = 'none';
3826
+ }
3827
+
3828
+ setGridPlate(div) {
3829
+ const
3830
+ parts = div.id.split('-'),
3831
+ modal = parts[0],
3832
+ md = this.modals[modal],
3833
+ id = parts.pop(),
3834
+ grid = MODEL.powerGridByID(id);
3835
+ // NOTE: Store power grid identifier as property of the modal.
3836
+ md.grid_id = (grid ? id : '');
3837
+ this.updateGridFields();
3838
+ }
3839
+
3840
+ updateGridFields() {
3841
+ // Adjust the powergrid-related elements of the dialog according to
3842
+ // the value of the `grid_id` property of the modal.
3843
+ const
3844
+ md = this.modals.process,
3845
+ plate = md.element('grid-plate'),
3846
+ overlay = md.element('grid-overlay'),
3847
+ notab = ['LB', 'IL', 'LCF'],
3848
+ pg = MODEL.powerGridByID(md.grid_id);
3849
+ if(pg) {
3850
+ plate.className = 'grid-kV-plate';
3851
+ plate.style.backgroundColor = pg.color;
3852
+ plate.innerHTML = pg.voltage;
3853
+ overlay.style.display = 'block';
3854
+ // Disable tab stop for the properties that are now not shown.
3855
+ for(let i = 0; i < notab.length; i++) {
3856
+ md.element(notab[i]).tabIndex = -1;
3857
+ }
3858
+ } else {
3859
+ plate.innerHTML = '(&#x21AF;)';
3860
+ plate.className = 'no-grid-plate';
3861
+ overlay.style.display = 'none';
3862
+ // Enable tab stop for the properties that are now not shown.
3863
+ for(let i = 0; i < notab.length; i++) {
3864
+ md.element(notab[i]).tabIndex = 0;
3865
+ }
3866
+ }
3867
+ this.hideGridPlateMenu('process');
3868
+ // Show plate "button" only when power grids option is set for model.
3869
+ plate.style.display = (MODEL.with_power_flow ? 'block' : 'none');
3870
+ }
3769
3871
 
3770
3872
  updateProcessProperties() {
3771
3873
  // Validates process properties, and only updates the edited process
@@ -3826,6 +3928,10 @@ console.log('HERE name conflicts', name_conflicts, mapping);
3826
3928
  p.integer_level = this.boxChecked('process-integer');
3827
3929
  p.level_to_zero = this.boxChecked('process-shut-down');
3828
3930
  p.collapsed = this.boxChecked('process-collapsed');
3931
+ p.power_grid = MODEL.powerGridByID(md.grid_id);
3932
+ p.length_in_km = safeStrToFloat(md.element('length').value, 0);
3933
+ p.TEX_id = md.element('tex-id').value;
3934
+ if(TEX_MANAGER.visible) TEX_MANAGER.update(p);
3829
3935
  if(md.group.length > 1) {
3830
3936
  // Redraw the entire diagram, as multiple processes may have changed.
3831
3937
  md.updateModifiedProperties(p);
@@ -3852,6 +3958,13 @@ console.log('HERE name conflicts', name_conflicts, mapping);
3852
3958
  md.element('P-unit').innerHTML =
3853
3959
  (p.scale_unit === '1' ? '' : '/' + p.scale_unit);
3854
3960
  md.element('currency').innerHTML = MODEL.currency_unit;
3961
+ const tex = md.element('tex-id');
3962
+ tex.value = p.TEX_id;
3963
+ if(TEX_MANAGER.visible) {
3964
+ tex.style.display = 'block';
3965
+ } else {
3966
+ tex.style.display = 'none';
3967
+ }
3855
3968
  // NOTE: IO parameter status is not "group-edited"!
3856
3969
  this.setImportExportBox('product', MODEL.ioType(p));
3857
3970
  // Focus on lower bound when showing the dialog for a group.
@@ -3924,9 +4037,11 @@ console.log('HERE name conflicts', name_conflicts, mapping);
3924
4037
  p.upper_bound)) return false;
3925
4038
  if(p.name.startsWith('$')) {
3926
4039
  // NOTE: For actor cash flow data products, price and initial
3927
- // level must remain blank.
4040
+ // level must remain blank...
3928
4041
  md.element('P').value = '';
3929
4042
  md.element('IL').value = '';
4043
+ // ... and the unit must be the model's currency unit.
4044
+ md.element('unit').value = MODEL.currency_unit;
3930
4045
  }
3931
4046
  if(!this.updateExpressionInput('product-IL', 'initial level',
3932
4047
  p.initial_level)) return false;
@@ -3943,10 +4058,10 @@ console.log('HERE name conflicts', name_conflicts, mapping);
3943
4058
  }
3944
4059
  // At this point, all input has been validated, so entity properties
3945
4060
  // can be modified.
4061
+ p.changeScaleUnit(md.element('unit').value);
3946
4062
  if(!p.name.startsWith('$')) {
3947
4063
  // NOTE: For actor cash flow data products, these properties must
3948
4064
  // also retain their initial value.
3949
- p.changeScaleUnit(md.element('unit').value);
3950
4065
  p.is_source = this.boxChecked('product-source');
3951
4066
  p.is_sink = this.boxChecked('product-sink');
3952
4067
  // NOTE: Do not unset `is_data` if product has ingoing data arrows.
@@ -3964,6 +4079,8 @@ console.log('HERE name conflicts', name_conflicts, mapping);
3964
4079
  p.no_links = this.boxChecked('product-no-links');
3965
4080
  let must_redraw = (pnl !== p.no_links);
3966
4081
  MODEL.ioUpdate(p, this.getImportExportBox('product'));
4082
+ p.TEX_id = md.element('tex-id').value;
4083
+ if(TEX_MANAGER.visible) TEX_MANAGER.update(p);
3967
4084
  // If a group was edited, update all entities in this group.
3968
4085
  if(md.group.length > 0) md.updateModifiedProperties(p);
3969
4086
  if(must_redraw || md.group.length > 1) {
@@ -11,7 +11,7 @@ for the Linny-R Dataset Manager dialog.
11
11
  */
12
12
 
13
13
  /*
14
- Copyright (c) 2017-2023 Delft University of Technology
14
+ Copyright (c) 2017-2024 Delft University of Technology
15
15
 
16
16
  Permission is hereby granted, free of charge, to any person obtaining a copy
17
17
  of this software and associated documentation files (the "Software"), to deal
@@ -577,8 +577,8 @@ class GUIDatasetManager extends DatasetManager {
577
577
  }
578
578
 
579
579
  selectModifier(event, id, x=true) {
580
- // Select modifier, or when double-clicked, edit its expression or the
581
- // name of the modifier
580
+ // Select modifier, or when double-clicked, edit its expression when
581
+ // x = TRUE, or the name of the modifier when x = FALSE.
582
582
  this.focal_table = this.modifier_table;
583
583
  if(this.selected_dataset) {
584
584
  const m = this.selected_dataset.modifiers[UI.nameToID(id)],
@@ -781,13 +781,17 @@ class GUIDatasetManager extends DatasetManager {
781
781
  if(this.selected_modifier) ms = this.selected_modifier.selector;
782
782
  md = this.rename_selector_modal;
783
783
  }
784
+ md.element('type').innerText = 'dataset modifier';
784
785
  md.element('name').value = ms;
785
786
  md.show('name');
786
787
  }
787
788
 
788
789
  newModifier() {
790
+ const md = this.new_selector_modal;
791
+ // NOTE: Selector modal is also used by constraint editor.
792
+ if(md.element('type').innerText !== 'dataset modifier') return;
789
793
  const
790
- sel = this.new_selector_modal.element('name').value,
794
+ sel = md.element('name').value,
791
795
  m = this.selected_dataset.addModifier(sel);
792
796
  if(m) {
793
797
  this.selected_modifier = m;
@@ -795,72 +799,76 @@ class GUIDatasetManager extends DatasetManager {
795
799
  // (ignoring those with wildcards)
796
800
  const sl = this.selected_dataset.plainSelectors;
797
801
  if(sl.length > 1) MODEL.expandDimension(sl);
798
- this.new_selector_modal.hide();
802
+ md.hide();
799
803
  this.updateModifiers();
800
804
  }
801
805
  }
802
806
 
803
807
  renameModifier() {
804
808
  if(!this.selected_modifier) return;
809
+ const md = this.rename_selector_modal;
810
+ // NOTE: Selector modal is also used by constraint editor.
811
+ if(md.element('type').innerText !== 'dataset modifier') return;
805
812
  const
806
813
  wild = this.selected_modifier.hasWildcards,
807
- sel = this.rename_selector_modal.element('name').value,
808
- // NOTE: normal dataset selector, so remove all invalid characters
814
+ sel = md.element('name').value,
815
+ // NOTE: Normal dataset selector, so remove all invalid characters.
809
816
  clean_sel = sel.replace(/[^a-zA-z0-9\%\+\-\?\*]/g, ''),
810
817
  // Keep track of old name
811
818
  oldm = this.selected_modifier,
812
- // NOTE: addModifier returns existing one if selector not changed
819
+ // NOTE: addModifier returns existing one if selector not changed.
813
820
  m = this.selected_dataset.addModifier(clean_sel);
814
821
  // NULL can result when new name is invalid
815
822
  if(!m) return;
816
- // If selected modifier was the dataset default selector, update it
823
+ // If selected modifier was the dataset default selector, update it.
817
824
  if(oldm.selector === this.selected_dataset.default_selector) {
818
825
  this.selected_dataset.default_selector = m.selector;
819
826
  }
820
827
  MODEL.renameSelectorInExperiments(oldm.selector, clean_sel);
821
- // If only case has changed, just update the selector
828
+ // If only case has changed, just update the selector.
822
829
  if(m === oldm) {
823
830
  m.selector = clean_sel;
824
831
  this.updateDialog();
825
- this.rename_selector_modal.hide();
832
+ md.hide();
826
833
  return;
827
834
  }
828
- // Rest is needed only when a new modifier has been added
835
+ // Rest is needed only when a new modifier has been added.
829
836
  m.expression = oldm.expression;
830
837
  if(wild) {
831
- // Wildcard selector means: recompile the modifier expression
838
+ // Wildcard selector means: recompile the modifier expression.
832
839
  m.expression.attribute = m.selector;
833
840
  m.expression.compile();
834
841
  }
835
842
  this.deleteModifier();
836
843
  this.selected_modifier = m;
837
- // Update all chartvariables referencing this dataset + old selector
844
+ // Update all chartvariables referencing this dataset + old selector.
838
845
  const vl = MODEL.datasetVariables;
839
846
  let cv_cnt = 0;
840
847
  for(let i = 0; i < vl.length; i++) {
848
+ const v = vl[i];
841
849
  if(v.object === this.selected_dataset && v.attribute === oldm.selector) {
842
850
  v.attribute = m.selector;
843
851
  cv_cnt++;
844
852
  }
845
853
  }
846
- // Also replace old selector in all expressions (count these as well)
854
+ // Also replace old selector in all expressions (count these as well).
847
855
  const xr_cnt = MODEL.replaceAttributeInExpressions(
848
856
  oldm.dataset.name + '|' + oldm.selector, m.selector);
849
- // Notify modeler of changes (if any)
857
+ // Notify modeler of changes (if any).
850
858
  const msg = [];
851
859
  if(cv_cnt) msg.push(pluralS(cv_cnt, ' chart variable'));
852
860
  if(xr_cnt) msg.push(pluralS(xr_cnt, ' expression variable'));
853
861
  if(msg.length) {
854
862
  UI.notify('Updated ' + msg.join(' and '));
855
863
  // Also update these stay-on-top dialogs, as they may display a
856
- // variable name for this dataset + modifier
864
+ // variable name for this dataset + modifier.
857
865
  UI.updateControllerDialogs('CDEFJX');
858
866
  }
859
- // NOTE: update dimensions only if dataset now has 2 or more modifiers
860
- // (ignoring those with wildcards)
867
+ // NOTE: Update dimensions only if dataset now has 2 or more modifiers
868
+ // (ignoring those with wildcards).
861
869
  const sl = this.selected_dataset.plainSelectors;
862
870
  if(sl.length > 1) MODEL.expandDimension(sl);
863
- this.rename_selector_modal.hide();
871
+ md.hide();
864
872
  this.updateModifiers();
865
873
  }
866
874
 
@@ -12,7 +12,7 @@ viewing and editing documentation text for model entities.
12
12
  */
13
13
 
14
14
  /*
15
- Copyright (c) 2017-2023 Delft University of Technology
15
+ Copyright (c) 2017-2024 Delft University of Technology
16
16
 
17
17
  Permission is hereby granted, free of charge, to any person obtaining a copy
18
18
  of this software and associated documentation files (the "Software"), to deal
@@ -336,8 +336,14 @@ class DocumentationManager {
336
336
  this.markup = (e.comments ? e.comments : '');
337
337
  this.editor.value = this.markup;
338
338
  this.viewer.innerHTML = this.markdown;
339
- this.edit_btn.classList.remove('disab');
340
- this.edit_btn.classList.add('enab');
339
+ if(e.grid && MODEL.solved && MODEL.with_power_flow) {
340
+ // Show cycle flows instead of comments.
341
+ const cf = POWER_GRID_MANAGER.allCycleFlows(e);
342
+ if(cf) this.viewer.innerHTML = cf;
343
+ } else {
344
+ this.edit_btn.classList.remove('disab');
345
+ this.edit_btn.classList.add('enab');
346
+ }
341
347
  // NOTE: Permit documentation of the model by raising the dialog.
342
348
  if(this.entity === MODEL) this.dialog.style.zIndex = 101;
343
349
  } else if(e instanceof DatasetModifier) {
@@ -354,6 +360,8 @@ class DocumentationManager {
354
360
  }
355
361
  }
356
362
  }
363
+ // When TEX renderer is visible, also update it.
364
+ if(TEX_MANAGER.visible) TEX_MANAGER.update(e, shift);
357
365
  }
358
366
 
359
367
  rewrite(str) {
@@ -11,7 +11,7 @@ for the Linny-R Equation Manager dialog.
11
11
  */
12
12
 
13
13
  /*
14
- Copyright (c) 2017-2023 Delft University of Technology
14
+ Copyright (c) 2017-2024 Delft University of Technology
15
15
 
16
16
  Permission is hereby granted, free of charge, to any person obtaining a copy
17
17
  of this software and associated documentation files (the "Software"), to deal
@@ -11,7 +11,7 @@ for the Linny-R Experiment Manager dialog.
11
11
  */
12
12
 
13
13
  /*
14
- Copyright (c) 2017-2023 Delft University of Technology
14
+ Copyright (c) 2017-2024 Delft University of Technology
15
15
 
16
16
  Permission is hereby granted, free of charge, to any person obtaining a copy
17
17
  of this software and associated documentation files (the "Software"), to deal
@@ -11,7 +11,7 @@ functionality for the Linny-R Expression Editor dialog.
11
11
  */
12
12
 
13
13
  /*
14
- Copyright (c) 2017-2023 Delft University of Technology
14
+ Copyright (c) 2017-2024 Delft University of Technology
15
15
 
16
16
  Permission is hereby granted, free of charge, to any person obtaining a copy
17
17
  of this software and associated documentation files (the "Software"), to deal
@@ -263,6 +263,9 @@ NOTE: Grouping groups results in a single group, e.g., (1;2);(3;4;5) evaluates a
263
263
  } else if(!this.edited_input_id && EQUATION_MANAGER.edited_expression) {
264
264
  own = MODEL.equations_dataset;
265
265
  sel = EQUATION_MANAGER.selected_modifier.selector;
266
+ } else if(!this.edited_input_id && CONSTRAINT_EDITOR.edited_expression) {
267
+ own = CONSTRAINT_EDITOR.selected;
268
+ sel = CONSTRAINT_EDITOR.selected_selector;
266
269
  } else {
267
270
  own = UI.edited_object;
268
271
  sel = this.edited_input_id.split('-').pop();
@@ -292,6 +295,9 @@ NOTE: Grouping groups results in a single group, e.g., (1;2);(3;4;5) evaluates a
292
295
  DATASET_MANAGER.modifyExpression(xp.expr);
293
296
  } else if(EQUATION_MANAGER.edited_expression) {
294
297
  EQUATION_MANAGER.modifyEquation(xp.expr);
298
+ } else if(CONSTRAINT_EDITOR.edited_expression) {
299
+ // NOTE: Boundline selector expressions may result in a grouping.
300
+ CONSTRAINT_EDITOR.modifyExpression(xp.expr, xp.concatenating);
295
301
  }
296
302
  UI.modals.expression.hide();
297
303
  return true;