linny-r 1.4.5 → 1.5.0

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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "linny-r",
3
- "version": "1.4.5",
3
+ "version": "1.5.0",
4
4
  "description": "Executable graphical language with WYSIWYG editor for MILP models",
5
5
  "main": "server.js",
6
6
  "scripts": {
package/static/index.html CHANGED
@@ -1754,6 +1754,8 @@ NOTE: * and ? will be interpreted as wildcards"
1754
1754
  title="New equation">
1755
1755
  <img id="eq-rename-btn" class="btn disab" src="images/rename.png"
1756
1756
  title="Rename selected equation">
1757
+ <img id="eq-clone-btn" class="btn disab" src="images/clone.png"
1758
+ title="Clone selected equation">
1757
1759
  <img id="eq-edit-btn" class="btn disab" src="images/edit.png"
1758
1760
  title="Edit selected equation">
1759
1761
  <img id="eq-delete-btn" class="btn disab" src="images/delete.png"
@@ -1790,6 +1792,18 @@ NOTE: * and ? will be interpreted as wildcards"
1790
1792
  </div>
1791
1793
  </div>
1792
1794
 
1795
+ <!-- the CLONE EQUATION dialog prompts for the new name of an equation -->
1796
+ <div id="clone-equation-modal" class="modal">
1797
+ <div id="clone-equation-dlg" class="inp-dlg">
1798
+ <div class="dlg-title">
1799
+ Clone equation
1800
+ <img class="cancel-btn" src="images/cancel.png">
1801
+ <img class="ok-btn" src="images/ok.png">
1802
+ </div>
1803
+ <input id="clone-equation-name" type="text" autocomplete="off">
1804
+ </div>
1805
+ </div>
1806
+
1793
1807
 
1794
1808
  <!-- the CHART dialog allows definition of charts and displays them -->
1795
1809
  <div id="chart-dlg" class="inp-dlg">
@@ -1840,16 +1854,19 @@ NOTE: * and ? will be interpreted as wildcards"
1840
1854
  src="images/add.png"
1841
1855
  title="Add variable (Shift-click to add a new equation)">
1842
1856
  <img id="chart-variable-up-btn" class="btn disab"
1843
- src="images/up.png" style="margin-left: 4px"
1857
+ src="images/up.png"
1844
1858
  title="Move selected variable up in list">
1845
1859
  <img id="chart-variable-down-btn" class="btn disab"
1846
1860
  src="images/down.png"
1847
1861
  title="Move selected variable down in list">
1848
1862
  <img id="chart-edit-variable-btn" class="btn disab"
1849
- src="images/edit.png" style="margin-left: 4px"
1863
+ src="images/edit.png"
1864
+ title="Edit selected variable">
1865
+ <img id="chart-sort-variable-btn" class="btn disab"
1866
+ src="images/sort-not.png"
1850
1867
  title="Edit selected variable">
1851
1868
  <img id="chart-delete-variable-btn" class="btn disab"
1852
- src="images/remove.png" style="margin-left: 4px"
1869
+ src="images/remove.png"
1853
1870
  title="Delete selected variable">
1854
1871
  </div>
1855
1872
  </div>
@@ -1857,6 +1874,23 @@ NOTE: * and ? will be interpreted as wildcards"
1857
1874
  <table id="chart-variables-table">
1858
1875
  </table>
1859
1876
  </div>
1877
+ <div id="chart-sorting-menu">
1878
+ <img id="chart-sort-not-btn" class="btn enab"
1879
+ src="images/sort-not.png"
1880
+ title="Do not sort series data for selected variable">
1881
+ <img id="chart-sort-asc-btn" class="btn enab"
1882
+ src="images/sort-asc.png"
1883
+ title="Sort series data for selected variable in ascending order">
1884
+ <img id="chart-sort-asc-lead-btn" class="btn enab"
1885
+ src="images/sort-asc-lead.png"
1886
+ title="Sort chart data in ascending order for selected variable">
1887
+ <img id="chart-sort-desc-btn" class="btn enab"
1888
+ src="images/sort-desc.png"
1889
+ title="Sort series data for selected variable in descending order">
1890
+ <img id="chart-sort-desc-lead-btn" class="btn enab"
1891
+ src="images/sort-desc-lead.png"
1892
+ title="Sort chart data in descending order for selected variable">
1893
+ </div>
1860
1894
  </div>
1861
1895
  <div id="chart-display-panel">
1862
1896
  <!-- NOTE: the scroller scrolls when the container is "stretched" -->
@@ -2439,7 +2439,8 @@ td.equation-expression {
2439
2439
  #new-dataset-dlg,
2440
2440
  #rename-dataset-dlg,
2441
2441
  #new-equation-dlg,
2442
- #rename-equation-dlg {
2442
+ #rename-equation-dlg,
2443
+ #clone-equation-dlg {
2443
2444
  width: 240px;
2444
2445
  height: 45px;
2445
2446
  }
@@ -2447,7 +2448,8 @@ td.equation-expression {
2447
2448
  #new-dataset-name,
2448
2449
  #rename-dataset-name,
2449
2450
  #new-equation-name,
2450
- #rename-equation-name {
2451
+ #rename-equation-name,
2452
+ #clone-equation-name {
2451
2453
  position: absolute;
2452
2454
  bottom: 2px;
2453
2455
  left: 2px;
@@ -2748,15 +2750,59 @@ td.equation-expression {
2748
2750
  width: 146px;
2749
2751
  display: inline-block;
2750
2752
  position: absolute;
2751
- left: 60px;
2753
+ left: 55px;
2752
2754
  top: -2px;
2753
2755
  }
2754
2756
 
2755
2757
  #chart-variable-buttons > img.btn {
2756
2758
  height: 18px;
2757
2759
  width: 18px;
2760
+ margin-left: 2px;
2761
+ }
2762
+
2763
+ #chart-sorting-menu {
2764
+ position: absolute;
2765
+ top: 89.5px;
2766
+ left: 153px;
2767
+ width: 19px;
2768
+ display: none;
2769
+ background-color: #f4f0f2;
2770
+ border: solid silver 1px;
2771
+ border-radius: 3px;
2772
+ padding: 1px;
2773
+ }
2774
+
2775
+ #chart-sorting-menu > img.btn {
2776
+ height: 18px;
2777
+ width: 18px;
2778
+ margin: 0px;
2779
+ }
2780
+
2781
+ td.vbl-asc,
2782
+ td.vbl-desc,
2783
+ td.vbl-asc-lead,
2784
+ td.vbl-desc-lead {
2785
+ color: #8000b0;
2758
2786
  }
2759
2787
 
2788
+ td.vbl-asc-lead,
2789
+ td.vbl-desc-lead {
2790
+ font-weight: bold;
2791
+ }
2792
+
2793
+ td.vbl-asc::after,
2794
+ td.vbl-asc-lead::after {
2795
+ content: ' \2B67';
2796
+ color: #b00080;
2797
+ }
2798
+
2799
+ td.vbl-desc::after,
2800
+ td.vbl-desc-lead::after {
2801
+ content: ' \2B68';
2802
+ color: #b00080;
2803
+ }
2804
+
2805
+
2760
2806
  #chart-variables {
2761
2807
  position: absolute;
2762
2808
  top: 113px;
@@ -298,6 +298,14 @@ class Controller {
298
298
  // NOTE: normalize to also accept letters with accents
299
299
  if(name === this.TOP_CLUSTER_NAME) return true;
300
300
  name = name.normalize('NFKD').trim();
301
+ if(name.startsWith('$')) {
302
+ const
303
+ parts = name.substring(1).split(' '),
304
+ flow = parts.shift(),
305
+ aid = this.nameToID(parts.join(' ')),
306
+ a = MODEL.actorByID(aid);
307
+ return a && ['IN', 'OUT', 'FLOW'].indexOf(flow) >= 0;
308
+ }
301
309
  return name && !name.match(/\[\\\|\]/) && !name.endsWith(':') &&
302
310
  (name.startsWith(this.BLACK_BOX) || name[0].match(/[\w]/));
303
311
  }
@@ -714,34 +722,44 @@ class DatasetManager {
714
722
  } // END of class DatasetManager
715
723
 
716
724
 
717
- // CLASS ChartManager controls the collection of charts of a model
725
+ // CLASS ChartManager controls the collection of charts of a model.
718
726
  class ChartManager {
719
727
  constructor() {
720
728
  this.new_chart_title = '(new chart)';
721
- // NOTE: The SVG height is fixed at 500 units, as this gives good results
722
- // for the SVG units for line width = 1; fill patterns definitions are
723
- // defined to work for images of this height (see further down)
729
+ // NOTE: The SVG height is fixed at 500 units, as this gives good
730
+ // results for the SVG units for line width = 1.
731
+ // Fill patterns definitions are defined to work for images of this
732
+ // height (see further down).
724
733
  this.svg_height = 500;
725
734
  this.container_height = this.svg_height;
726
- // Default aspect ratio W:H is 1.75 -- stretch factor will make it more oblong
735
+ // Default aspect ratio W:H is 1.75. The stretch factor of the chart
736
+ // manager will make the chart more oblong.
727
737
  this.container_width = this.svg_height * 1.75;
728
738
  this.legend_options = ['None', 'Top', 'Right', 'Bottom'];
729
- // Basic properties -- also needed for console application
739
+ // Basic properties -- also needed for console application.
730
740
  this.visible = false;
731
741
  this.chart_index = -1;
732
742
  this.variable_index = -1;
733
743
  this.stretch_factor = 1;
734
744
  this.drawing_graph = false;
735
745
  this.runs_chart = false;
736
- // Fill styles used to differentiate between experiments in histograms
746
+ // Arrows indicating sort direction.
747
+ this.sort_arrows = {
748
+ 'not' : '',
749
+ 'asc': ' \u2B67',
750
+ 'desc': ' \u2B68',
751
+ 'asc-lead': ' \u21D7',
752
+ 'desc-lead': ' \u21D8'
753
+ };
754
+ // Fill styles used to differentiate between experiments in histograms.
737
755
  this.fill_styles = [
738
756
  'diagonal-cross-hatch', 'dots',
739
757
  'diagonal-hatch', 'checkers', 'horizontal-hatch',
740
758
  'cross-hatch', 'circles', 'vertical-hatch'
741
759
  ];
742
760
 
743
- // SVG for chart fill patterns
744
- // NOTE: mask width and height are based on SVG height = 500
761
+ // SVG for chart fill patterns.
762
+ // NOTE: Mask width and height are based on SVG height = 500.
745
763
  this.fill_patterns = `
746
764
  <pattern id="vertical-hatch" width="4" height="4"
747
765
  patternUnits="userSpaceOnUse">
@@ -1261,7 +1279,7 @@ class ExperimentManager {
1261
1279
  }
1262
1280
 
1263
1281
  processRestOfRun() {
1264
- // Performs post-processing after run results have been added
1282
+ // Performs post-processing after run results have been added.
1265
1283
  const x = MODEL.running_experiment;
1266
1284
  if(!x) return;
1267
1285
  const aci = x.active_combination_index;
@@ -79,8 +79,25 @@ class GUIChartManager extends ChartManager {
79
79
  'click', () => CHART_MANAGER.moveVariable(1));
80
80
  document.getElementById('chart-edit-variable-btn').addEventListener(
81
81
  'click', () => CHART_MANAGER.editVariable());
82
+ document.getElementById('chart-sort-variable-btn').addEventListener(
83
+ 'mouseenter', () => CHART_MANAGER.showSortingMenu());
82
84
  document.getElementById('chart-delete-variable-btn').addEventListener(
83
85
  'click', () => CHART_MANAGER.deleteVariable());
86
+ // Make the sorting menu responsive.
87
+ this.sorting_menu = document.getElementById('chart-sorting-menu');
88
+ this.sorting_menu.addEventListener(
89
+ 'mouseleave', () => CHART_MANAGER.hideSortingMenu());
90
+ document.getElementById('chart-sort-not-btn').addEventListener(
91
+ 'click', (e) => CHART_MANAGER.setSortType(e.target));
92
+ document.getElementById('chart-sort-asc-btn').addEventListener(
93
+ 'click', (e) => CHART_MANAGER.setSortType(e.target));
94
+ document.getElementById('chart-sort-asc-lead-btn').addEventListener(
95
+ 'click', (e) => CHART_MANAGER.setSortType(e.target));
96
+ document.getElementById('chart-sort-desc-btn').addEventListener(
97
+ 'click', (e) => CHART_MANAGER.setSortType(e.target));
98
+ document.getElementById('chart-sort-desc-lead-btn').addEventListener(
99
+ 'click', (e) => CHART_MANAGER.setSortType(e.target));
100
+ // Add properties for access to other chart manager dialog elements.
84
101
  this.variables_table = document.getElementById('chart-variables-table');
85
102
  this.display_panel = document.getElementById('chart-display-panel');
86
103
  this.toggle_chevron = document.getElementById('chart-toggle-chevron');
@@ -188,6 +205,7 @@ class GUIChartManager extends ChartManager {
188
205
  this.setRunsChart(false);
189
206
  this.last_time_selected = 0;
190
207
  this.paste_color = '';
208
+ this.hideSortingMenu();
191
209
  }
192
210
 
193
211
  enterKey() {
@@ -336,17 +354,27 @@ class GUIChartManager extends ChartManager {
336
354
  '<td class="v-box"><div id="v-box-', i, '" class="vbox',
337
355
  (cv.visible ? ' checked' : ' clear'),
338
356
  '" onclick="CHART_MANAGER.toggleVariable(', i,
339
- ');"></div></td><td class="v-name">', cv.displayName,
357
+ ');"></div></td><td class="v-name vbl-', cv.sorted,
358
+ '">', cv.displayName,
340
359
  '</td></tr>'].join(''));
341
360
  }
342
361
  this.variables_table.innerHTML = ol.join('');
343
362
  } else {
344
363
  this.variable_index = -1;
345
364
  }
365
+ // Set the image of the sort type button.
366
+ if(this.variable_index >= 0) {
367
+ const
368
+ cv = c.variables[this.variable_index],
369
+ sb = document.getElementById('chart-sort-variable-btn'),
370
+ mb = document.getElementById(`chart-sort-${cv.sorted}-btn`);
371
+ sb.src = `images/sort-${cv.sorted}.png`;
372
+ sb.title = mb.title;
373
+ }
346
374
  const
347
375
  u_btn = 'chart-variable-up ',
348
376
  d_btn = 'chart-variable-down ',
349
- ed_btns = 'chart-edit-variable chart-delete-variable ';
377
+ ed_btns = 'chart-edit-variable chart-sort-variable chart-delete-variable ';
350
378
  // Just in case variable index has not been adjusted after some
351
379
  // variables have been deleted
352
380
  if(this.variable_index >= c.variables.length) {
@@ -378,8 +406,35 @@ class GUIChartManager extends ChartManager {
378
406
  this.stretchChart(0);
379
407
  }
380
408
 
409
+ showSortingMenu() {
410
+ // Show the pane with sort type buttons only if variable is selected.
411
+ this.sorting_menu.style.display =
412
+ (this.variable_index >= 0 ? 'block' : 'none');
413
+ }
414
+
415
+ hideSortingMenu() {
416
+ // Hide the pane with sort type buttons.
417
+ this.sorting_menu.style.display = 'none';
418
+ }
419
+
420
+ setSortType(btn) {
421
+ // Set the sort type for the selected chart variable.
422
+ if(this.chart_index < 0 || this.variable_index < 0) return;
423
+ const
424
+ c = MODEL.charts[this.chart_index],
425
+ cv = c.variables[this.variable_index],
426
+ parts = btn.id.split('-');
427
+ parts.shift();
428
+ parts.shift();
429
+ parts.pop();
430
+ cv.sorted = parts.join('-');
431
+ this.hideSortingMenu();
432
+ this.updateDialog();
433
+ }
434
+
381
435
  updateExperimentInfo() {
382
- // Display selected experiment title in dialog header if run data are used
436
+ // Display selected experiment title in dialog header if run data
437
+ // are used.
383
438
  const
384
439
  selx = EXPERIMENT_MANAGER.selected_experiment,
385
440
  el = document.getElementById('chart-experiment-info');
@@ -498,7 +553,7 @@ class GUIChartManager extends ChartManager {
498
553
  cv = c.variables[i],
499
554
  nv = new ChartVariable(nc);
500
555
  nv.setProperties(cv.object, cv.attribute, cv.stacked,
501
- cv.color, cv.scale_factor, cv.line_width);
556
+ cv.color, cv.scale_factor, cv.line_width, cv.sorted);
502
557
  nc.variables.push(nv);
503
558
  }
504
559
  this.chart_index = MODEL.indexOfChart(nc.title);
@@ -649,7 +704,7 @@ class GUIChartManager extends ChartManager {
649
704
  this.variable_modal.element('color').style.backgroundColor = cv.color;
650
705
  this.setColorPicker(cv.color);
651
706
  // Show change equation buttons only for equation variables
652
- if(cv.object === MODEL.equations_dataset) {
707
+ if(cv.object === MODEL.equations_dataset || cv.object instanceof DatasetModifier) {
653
708
  this.change_equation_btns.style.display = 'block';
654
709
  } else {
655
710
  this.change_equation_btns.style.display = 'none';
@@ -741,7 +796,7 @@ class GUIChartManager extends ChartManager {
741
796
  // Renames the selected variable (if it is an equation)
742
797
  if(this.chart_index >= 0 && this.variable_index >= 0) {
743
798
  const v = MODEL.charts[this.chart_index].variables[this.variable_index];
744
- if(v.object === MODEL.equations_dataset) {
799
+ if(v.object === MODEL.equations_dataset || v.object instanceof DatasetModifier) {
745
800
  const m = MODEL.equations_dataset.modifiers[UI.nameToID(v.attribute)];
746
801
  if(m instanceof DatasetModifier) {
747
802
  EQUATION_MANAGER.selected_modifier = m;
@@ -755,7 +810,7 @@ class GUIChartManager extends ChartManager {
755
810
  // Opens the expression editor for the selected variable (if equation)
756
811
  if(this.chart_index >= 0 && this.variable_index >= 0) {
757
812
  const v = MODEL.charts[this.chart_index].variables[this.variable_index];
758
- if(v.object === MODEL.equations_dataset) {
813
+ if(v.object === MODEL.equations_dataset || v.object instanceof DatasetModifier) {
759
814
  const m = MODEL.equations_dataset.modifiers[UI.nameToID(v.attribute)];
760
815
  if(m instanceof DatasetModifier) {
761
816
  EQUATION_MANAGER.selected_modifier = m;
@@ -168,6 +168,8 @@ class GroupPropertiesDialog extends ModalDialog {
168
168
  this.initial[name] = prop;
169
169
  if(token === 'bbtn') {
170
170
  el.className = (prop ? 'bbtn eq' : 'bbtn ne');
171
+ // NOTE: Update required to enable or disable UB field.
172
+ UI.updateEqualBounds(obj.type.toLowerCase());
171
173
  } else if(token === 'box') {
172
174
  el.className = (prop ? 'box checked' : 'box clear');
173
175
  } else if(propname === 'share_of_cost') {
@@ -1811,7 +1813,7 @@ class GUIController extends Controller {
1811
1813
  setTimeout(() => {
1812
1814
  const md = UI.modals['add-process'];
1813
1815
  md.element('name').value = '';
1814
- md.element('actor-name').value = '';
1816
+ md.element('actor').value = '';
1815
1817
  md.show('name');
1816
1818
  });
1817
1819
  } else if(obj === 'product') {
@@ -2761,7 +2763,44 @@ class GUIController extends Controller {
2761
2763
  } else {
2762
2764
  md = this.modals['add-product'];
2763
2765
  nn = md.element('name').value;
2764
- if(!this.validNames(nn)) {
2766
+ // NOTE: As of version 1.5, actor cash IN, chash OUT and cash FLOW
2767
+ // can be added as special data products. These products are indicated
2768
+ // by a leading dollar sign or euro sign, followed by an acceptable
2769
+ // flow indicator (I, O, F, CI, CO, CF, IN, OUT, FLOW), or none to
2770
+ // indicate the default "cash flow", followed by the name of an
2771
+ // actor already defined in the model.
2772
+ if(nn.startsWith('$') || nn.startsWith('\u20AC')) {
2773
+ const
2774
+ valid = {
2775
+ 'i': 'IN',
2776
+ 'ci': 'IN',
2777
+ 'in': 'IN',
2778
+ 'o': 'OUT',
2779
+ 'co': 'OUT',
2780
+ 'out': 'OUT',
2781
+ 'f': 'FLOW',
2782
+ 'cf': 'FLOW',
2783
+ 'flow': 'FLOW'
2784
+ },
2785
+ parts = nn.substring(1).trim().split(' ');
2786
+ let flow = valid[parts[0].toLowerCase()];
2787
+ if(flow === undefined) flow = '';
2788
+ // If first part indicates flow type, trim it from the name parts.
2789
+ if(flow) parts.shift();
2790
+ // Now the parts should identify an actor; this may be (no actor).
2791
+ const
2792
+ aid = this.nameToID(parts.join(' ').trim() || this.NO_ACTOR),
2793
+ a = MODEL.actorByID(aid);
2794
+ if(a) {
2795
+ // If so, and no flow type, assume the default (cash FLOW).
2796
+ if(!flow) flow = 'FLOW';
2797
+ // Change name to canonical.form, i.e., like "$FLOW actor name"
2798
+ nn = `$${flow} ${a.name}`;
2799
+ }
2800
+ }
2801
+ // Test if name is valid.
2802
+ const vn = this.validName(nn);
2803
+ if(!vn) {
2765
2804
  UNDO_STACK.pop();
2766
2805
  return false;
2767
2806
  }
@@ -2770,8 +2809,13 @@ class GUIController extends Controller {
2770
2809
  n = MODEL.addProduct(nn);
2771
2810
  if(n) {
2772
2811
  if(pp) {
2773
- // Do not change unit or data type of existing product
2812
+ // Do not change unit or data type of existing product.
2774
2813
  this.notify(`Added existing product <em>${pp.displayName}</em>`);
2814
+ } else if(nn.startsWith('$')) {
2815
+ // Actor cash flow products must be data products, and
2816
+ // must have the model's currency unit as scale unit.
2817
+ n.scale_unit = MODEL.currency_unit;
2818
+ n.is_data = true;
2775
2819
  } else {
2776
2820
  n.scale_unit = MODEL.addScaleUnit(md.element('unit').value);
2777
2821
  n.is_data = this.boxChecked('add-product-data');
@@ -3658,6 +3702,12 @@ console.log('HERE name conflicts', name_conflicts, mapping);
3658
3702
  p.lower_bound)) return false;
3659
3703
  if(!this.updateExpressionInput('product-UB', 'upper bound',
3660
3704
  p.upper_bound)) return false;
3705
+ if(p.name.startsWith('$')) {
3706
+ // NOTE: For actor cash flow data products, price and initial
3707
+ // level must remain blank.
3708
+ md.element('P').value = '';
3709
+ md.element('IL').value = '';
3710
+ }
3661
3711
  if(!this.updateExpressionInput('product-IL', 'initial level',
3662
3712
  p.initial_level)) return false;
3663
3713
  if(!this.updateExpressionInput('product-P', 'market price',
@@ -3673,15 +3723,23 @@ console.log('HERE name conflicts', name_conflicts, mapping);
3673
3723
  }
3674
3724
  // At this point, all input has been validated, so entity properties
3675
3725
  // can be modified.
3676
- p.changeScaleUnit(md.element('unit').value);
3677
- p.equal_bounds = this.getEqualBounds('product-UB-equal');
3678
- p.is_source = this.boxChecked('product-source');
3679
- p.is_sink = this.boxChecked('product-sink');
3680
- // NOTE: Do not unset is_data if product has ingoing data arrows.
3681
- p.is_data = p.hasDataInputs || this.boxChecked('product-data');
3682
- p.is_buffer = this.boxChecked('product-stock');
3683
- p.integer_level = this.boxChecked('product-integer');
3726
+ if(!p.name.startsWith('$')) {
3727
+ // NOTE: For actor cash flow data products, these properties must
3728
+ // also retain their initial value.
3729
+ p.changeScaleUnit(md.element('unit').value);
3730
+ p.is_source = this.boxChecked('product-source');
3731
+ p.is_sink = this.boxChecked('product-sink');
3732
+ // NOTE: Do not unset `is_data` if product has ingoing data arrows.
3733
+ p.is_data = p.hasDataInputs || this.boxChecked('product-data');
3734
+ p.is_buffer = this.boxChecked('product-stock');
3735
+ // NOTE: Integer constraint will typically not work because cash
3736
+ // flows are scaled when setting up the Simplex tableau, and hence
3737
+ // the values of their decision variable will differ from their
3738
+ // level in the model.
3739
+ p.integer_level = this.boxChecked('product-integer');
3740
+ }
3684
3741
  p.no_slack = this.boxChecked('product-no-slack');
3742
+ p.equal_bounds = this.getEqualBounds('product-UB-equal');
3685
3743
  const pnl = p.no_links;
3686
3744
  p.no_links = this.boxChecked('product-no-links');
3687
3745
  let must_redraw = (pnl !== p.no_links);
@@ -48,6 +48,8 @@ class EquationManager {
48
48
  'click', () => EQUATION_MANAGER.promptForEquation());
49
49
  document.getElementById('eq-rename-btn').addEventListener(
50
50
  'click', () => EQUATION_MANAGER.promptForName());
51
+ document.getElementById('eq-clone-btn').addEventListener(
52
+ 'click', () => EQUATION_MANAGER.promptToClone());
51
53
  document.getElementById('eq-edit-btn').addEventListener(
52
54
  'click', () => EQUATION_MANAGER.editEquation());
53
55
  document.getElementById('eq-delete-btn').addEventListener(
@@ -66,6 +68,12 @@ class EquationManager {
66
68
  this.rename_modal.cancel.addEventListener(
67
69
  'click', () => EQUATION_MANAGER.rename_modal.hide());
68
70
 
71
+ this.clone_modal = new ModalDialog('clone-equation');
72
+ this.clone_modal.ok.addEventListener(
73
+ 'click', () => EQUATION_MANAGER.cloneEquation());
74
+ this.clone_modal.cancel.addEventListener(
75
+ 'click', () => EQUATION_MANAGER.clone_modal.hide());
76
+
69
77
  // Initialize the dialog properties
70
78
  this.reset();
71
79
  }
@@ -146,7 +154,7 @@ class EquationManager {
146
154
  this.table.innerHTML = ml.join('');
147
155
  this.scroll_area.style.display = 'block';
148
156
  if(sm) UI.scrollIntoView(document.getElementById(smid));
149
- const btns = 'eq-rename eq-edit eq-delete';
157
+ const btns = 'eq-rename eq-clone eq-edit eq-delete';
150
158
  if(sm) {
151
159
  UI.enableButtons(btns);
152
160
  } else {
@@ -294,6 +302,43 @@ class EquationManager {
294
302
  this.updateDialog();
295
303
  }
296
304
 
305
+ promptToClone() {
306
+ // Prompts the modeler for the name of the clone to make of the
307
+ // selected equation (if any).
308
+ if(this.selected_modifier) {
309
+ this.clone_modal.element('name').value = this.selected_modifier.selector;
310
+ this.clone_modal.show('name');
311
+ }
312
+ }
313
+
314
+ cloneEquation() {
315
+ if(!this.selected_modifier) return;
316
+ const
317
+ s = this.clone_modal.element('name').value,
318
+ // New equation identifier must not equal some entity ID
319
+ obj = MODEL.objectByName(s);
320
+ if(obj) {
321
+ // NOTE: also pass selector, or warning will display dataset name.
322
+ UI.warningEntityExists(obj);
323
+ return null;
324
+ }
325
+ // Name is new and unique, so try to use it
326
+ const m = MODEL.equations_dataset.addModifier(s);
327
+ // NULL indicates invalid name, and modeler will have been warned.
328
+ if(!m) return;
329
+ // Give the new modifier the same expression as te selected one.
330
+ m.expression.text = this.selected_modifier.expression.text;
331
+ // Compile the expression. This may generate a warning when the new
332
+ // name does not provide adequate context.
333
+ m.expression.compile();
334
+ //
335
+ this.selected_modifier = m;
336
+ // Even if warning was given, close the name prompt dialog, and update
337
+ // the equation manager.
338
+ this.clone_modal.hide();
339
+ this.updateDialog();
340
+ }
341
+
297
342
  deleteEquation() {
298
343
  const m = this.selected_modifier;
299
344
  if(m) {
@@ -226,6 +226,9 @@ NOTE: Grouping groups results in a single group, e.g., (1;2);(3;4;5) evaluates a
226
226
  // Clear the "shortcut flag" that may be set by Shift-clicking the
227
227
  // "add chart variable" button in the chart dialog
228
228
  EQUATION_MANAGER.add_to_chart = false;
229
+ // CLear other properties that relate to the edited expression.
230
+ this.edited_input_id = '';
231
+ this.edited_expression = null;
229
232
  }
230
233
 
231
234
  parseExpression() {
@@ -294,7 +294,10 @@ class Finder {
294
294
  for(let i = 0; i < n; i++) {
295
295
  const e = this.entities[i];
296
296
  // Exclude "no actor" and top cluster.
297
- if(e.name !== '(no_actor)' && e.name !== '(top_cluster)') {
297
+ if(e.name !== '(no_actor)' && e.name !== '(top_cluster)' &&
298
+ // Also exclude actor cash flow data products because
299
+ // many of their properties should not be changed.
300
+ !e.name.startsWith('$')) {
298
301
  eg.push(e);
299
302
  }
300
303
  }
@@ -2262,8 +2262,11 @@ class Paper {
2262
2262
  rim_color,
2263
2263
  stroke_color,
2264
2264
  stroke_width,
2265
- // Draw border as dashed line if product is data product
2266
- sda = (prod.is_data ? UI.sda.dash : 'none'),
2265
+ // Draw border as dashed line if product is data product, and
2266
+ // for actor cash flow data as dotted line.
2267
+ sda = (prod.is_data ?
2268
+ (prod.name.startsWith('$') ? UI.sda.dot : UI.sda.dash) :
2269
+ 'none'),
2267
2270
  first_commit_option = prod.needsFirstCommitData,
2268
2271
  x = prod.x + dx,
2269
2272
  y = prod.y + dy,
@@ -2736,15 +2739,22 @@ class Paper {
2736
2739
  clstr.shape.addBlockArrow(x, y - hh, UI.BLOCK_IO,
2737
2740
  clstr.hidden_io.length);
2738
2741
  }
2739
- // Highlight shape if it has comments
2740
- clstr.shape.element.firstChild.setAttribute('style',
2741
- (DOCUMENTATION_MANAGER.visible && clstr.comments ?
2742
- this.documented_filter : ''));
2743
- // Highlight cluster if it is the drop target for the selection
2744
- if(clstr === this.target_cluster) {
2745
- clstr.shape.element.setAttribute('style', this.target_filter);
2742
+ if(clstr === UI.target_cluster) {
2743
+ // Highlight cluster if it is the drop target for the selection.
2744
+ clstr.shape.element.childNodes[0].setAttribute('style',
2745
+ this.target_filter);
2746
+ clstr.shape.element.childNodes[1].setAttribute('style',
2747
+ this.target_filter);
2748
+ } else if(DOCUMENTATION_MANAGER.visible && clstr.comments) {
2749
+ // Highlight shape if it has comments.
2750
+ clstr.shape.element.childNodes[0].setAttribute('style',
2751
+ this.documented_filter);
2752
+ clstr.shape.element.childNodes[1].setAttribute('style',
2753
+ this.documented_filter);
2746
2754
  } else {
2747
- clstr.shape.element.setAttribute('style', '');
2755
+ // No highlighting.
2756
+ clstr.shape.element.childNodes[0].setAttribute('style', '');
2757
+ clstr.shape.element.childNodes[1].setAttribute('style', '');
2748
2758
  }
2749
2759
  clstr.shape.element.setAttribute('opacity', 0.9);
2750
2760
  clstr.shape.appendToDOM();
@@ -728,17 +728,21 @@ class LinnyRModel {
728
728
 
729
729
  canLink(from, to) {
730
730
  // Return TRUE iff FROM-node can feature a "straight" link (i.e., a
731
- // product flow) to TO-node
731
+ // product flow) to TO-node.
732
732
  if(from.type === to.type) {
733
733
  // No "straight" link between nodes of same type (see canConstrain
734
- // for "curved" links) UNLESS TO-node is a data product
734
+ // for "curved" links) UNLESS TO-node is a data product.
735
735
  if(!to.is_data) return false;
736
736
  }
737
+ // No links to actor cash flow data products.
738
+ if(to.name.startsWith('$')) return false;
739
+ // No links from actor cash flow data to processes.
740
+ if(from.name.startsWith('$') && to instanceof Process) return false;
737
741
  // At most ONE link A --> B.
738
742
  for(let i = 0; i < from.outputs.length; i++) {
739
743
  if(from.outputs[i].to_node === to) return false;
740
744
  }
741
- // No link A --> B if there already exists a link B --> A
745
+ // No link A --> B if there already exists a link B --> A.
742
746
  for(let i = 0; i < to.outputs.length; i++) {
743
747
  if(to.outputs[i].to_node === from) return false;
744
748
  }
@@ -1188,6 +1192,7 @@ class LinnyRModel {
1188
1192
  // Product nodes have no actor
1189
1193
  let actor = this.addActor('');
1190
1194
  name = UI.cleanName(name);
1195
+ // Leading dollar sign indicates an actor cash flow data product.
1191
1196
  if(!UI.validName(name)) {
1192
1197
  UI.warningInvalidName(name);
1193
1198
  return null;
@@ -1211,6 +1216,12 @@ class LinnyRModel {
1211
1216
  if(node) p.initFromXML(node);
1212
1217
  p.setCode();
1213
1218
  this.products[p.identifier] = p;
1219
+ // NOTE: Cash flow products must be data, and must have the model's
1220
+ // currency unit as unit.
1221
+ if(p.name.startsWith('$')) {
1222
+ p.is_data = true;
1223
+ p.unit = MODEL.currency_unit;
1224
+ }
1214
1225
  p.resize();
1215
1226
  // New product => prepare for redraw
1216
1227
  p.cluster.clearAllProcesses();
@@ -2051,11 +2062,23 @@ class LinnyRModel {
2051
2062
  // that actually delete model elements append their XML to the XML attribute
2052
2063
  // of this UndoEdit
2053
2064
  let obj,
2054
- dc = false,
2055
2065
  fc = this.focal_cluster;
2056
2066
  // Update the documentation manager (GUI only) if selection contains the
2057
2067
  // current entity
2058
2068
  if(DOCUMENTATION_MANAGER) DOCUMENTATION_MANAGER.clearEntity(this.selection);
2069
+ // First delete links and constraints.
2070
+ for(let i = this.selection.length - 1; i >= 0; i--) {
2071
+ if(this.selection[i] instanceof Link ||
2072
+ this.selection[i] instanceof Constraint) {
2073
+ obj = this.selection.splice(i, 1)[0];
2074
+ if(obj instanceof Link) {
2075
+ this.deleteLink(obj);
2076
+ } else {
2077
+ this.deleteConstraint(obj);
2078
+ }
2079
+ }
2080
+ }
2081
+ // Then delete selected nodes.
2059
2082
  for(let i = this.selection.length - 1; i >= 0; i--) {
2060
2083
  obj = this.selection.splice(i, 1)[0];
2061
2084
  // NOTE: when deleting a selection, this selection has been made in the
@@ -2064,13 +2087,8 @@ class LinnyRModel {
2064
2087
  fc.deleteNote(obj);
2065
2088
  } else if(obj instanceof Product) {
2066
2089
  fc.deleteProduct(obj);
2067
- } else if(obj instanceof Link) {
2068
- this.deleteLink(obj);
2069
- } else if(obj instanceof Constraint) {
2070
- this.deleteConstraint(obj);
2071
2090
  } else if(obj instanceof Cluster) {
2072
2091
  this.deleteCluster(obj);
2073
- dc = true;
2074
2092
  } else {
2075
2093
  this.deleteNode(obj);
2076
2094
  }
@@ -2963,7 +2981,7 @@ class LinnyRModel {
2963
2981
  const cv = new ChartVariable(null);
2964
2982
  cv.setProperties(v.object, v.attribute, false, '#000000');
2965
2983
  vbls.push(cv);
2966
- names.push(uvn);
2984
+ names.push(vn);
2967
2985
  }
2968
2986
  } else if(names.indexOf(vn) < 0) {
2969
2987
  // Keep track of the dataset and dataset modifier variables,
@@ -7695,9 +7713,10 @@ class Product extends Node {
7695
7713
  }
7696
7714
 
7697
7715
  get isConstant() {
7698
- // Returns TRUE if this product is data, has no links to processes,
7699
- // and has set LB = UB
7700
- if(!this.is_data || !this.allOutputsAreData) return false;
7716
+ // Return TRUE if this product is data, has no links to processes,
7717
+ // is not an actor cash flow, and has set LB = UB
7718
+ if(!this.is_data || this.name.startsWith('$') ||
7719
+ !this.allOutputsAreData) return false;
7701
7720
  for(let i = 0; i < this.inputs.length; i++) {
7702
7721
  if(this.inputs[i].from_node instanceof Process) return false;
7703
7722
  }
@@ -8434,15 +8453,26 @@ class Dataset {
8434
8453
  }
8435
8454
 
8436
8455
  get plainSelectors() {
8437
- // Returns sorted list of selectors that do not contain wildcards
8438
- const sl = this.selectorList;
8439
- // NOTE: wildcard selectors will always be at the end of the list
8456
+ // Return sorted list of selectors that do not contain wildcards.
8457
+ const sl = this.selectorList.slice();
8458
+ // NOTE: Wildcard selectors will always be at the end of the list
8440
8459
  for(let i = sl.length - 1; i >= 0; i--) {
8441
8460
  if(sl[i].indexOf('*') >= 0 || sl[i].indexOf('?') >= 0) sl.pop();
8442
8461
  }
8443
8462
  return sl;
8444
8463
  }
8445
8464
 
8465
+ get wildcardSelectors() {
8466
+ // Return sorted list of selectors that DO contain wildcards.
8467
+ const sl = this.selectorList;
8468
+ // NOTE: Wildcard selectors will always be at the end of the list.
8469
+ let i = sl.length - 1;
8470
+ while(i >= 0 && (sl[i].indexOf('*') >= 0 || sl[i].indexOf('?') >= 0)) {
8471
+ i--;
8472
+ }
8473
+ return sl.slice(i+1);
8474
+ }
8475
+
8446
8476
  isWildcardSelector(s) {
8447
8477
  // Returns TRUE if `s` contains * or ?
8448
8478
  // NOTE: for equations, the wildcard must be ??
@@ -8898,7 +8928,7 @@ class ChartVariable {
8898
8928
  this.wildcard_index = false;
8899
8929
  }
8900
8930
 
8901
- setProperties(obj, attr, stck, clr, sf=1, lw=1, vis=true) {
8931
+ setProperties(obj, attr, stck, clr, sf=1, lw=1, vis=true, sort='not') {
8902
8932
  // Sets the defining properties for this chart variable.
8903
8933
  this.object = obj;
8904
8934
  this.attribute = attr;
@@ -8907,6 +8937,7 @@ class ChartVariable {
8907
8937
  this.scale_factor = sf;
8908
8938
  this.line_width = lw;
8909
8939
  this.visible = vis;
8940
+ this.sorted = sort;
8910
8941
  }
8911
8942
 
8912
8943
  get displayName() {
@@ -8915,16 +8946,22 @@ class ChartVariable {
8915
8946
  const sf = (this.scale_factor === 1 ? '' :
8916
8947
  ` (x${VM.sig4Dig(this.scale_factor)})`);
8917
8948
  //Display name of equation is just the equations dataset selector.
8918
- if(this.object instanceof DatasetModifier ||
8919
- // NOTE: Same holds for "dummy variables" added for wildcard
8920
- // dataset selectors.
8921
- this.object === MODEL.equations_dataset) {
8949
+ if(this.object instanceof DatasetModifier) {
8922
8950
  let eqn = this.object.selector;
8923
8951
  if(this.wildcard_index !== false) {
8924
8952
  eqn = eqn.replace('??', this.wildcard_index);
8925
8953
  }
8926
8954
  return eqn + sf;
8927
8955
  }
8956
+ // NOTE: Same holds for "dummy variables" added for wildcard
8957
+ // dataset selectors.
8958
+ if(this.object === MODEL.equations_dataset) {
8959
+ let eqn = this.attribute;
8960
+ if(this.wildcard_index !== false) {
8961
+ eqn = eqn.replace('??', this.wildcard_index);
8962
+ }
8963
+ return eqn + sf;
8964
+ }
8928
8965
  // NOTE: Do not display the vertical bar if no attribute is specified.
8929
8966
  if(!this.attribute) return this.object.displayName + sf;
8930
8967
  return this.object.displayName + UI.OA_SEPARATOR + this.attribute + sf;
@@ -8939,6 +8976,7 @@ class ChartVariable {
8939
8976
  }
8940
8977
  const xml = ['<chart-variable', (this.stacked ? ' stacked="1"' : ''),
8941
8978
  (this.visible ? ' visible="1"' : ''),
8979
+ ` sorted="${this.sorted}"`,
8942
8980
  '><object-id>', xmlEncoded(id),
8943
8981
  '</object-id><attribute>', this.attribute,
8944
8982
  '</attribute><color>', this.color,
@@ -8990,7 +9028,8 @@ class ChartVariable {
8990
9028
  nodeContentByTag(node, 'color'),
8991
9029
  safeStrToFloat(nodeContentByTag(node, 'scale-factor')),
8992
9030
  safeStrToFloat(nodeContentByTag(node, 'line-width')),
8993
- nodeParameterValue(node, 'visible') === '1');
9031
+ nodeParameterValue(node, 'visible') === '1',
9032
+ nodeParameterValue(node, 'sorted') || 'not');
8994
9033
  return true;
8995
9034
  }
8996
9035
 
@@ -9133,15 +9172,31 @@ class ChartVariable {
9133
9172
  }
9134
9173
 
9135
9174
  setLinePath(x0, y0, dx, dy) {
9136
- // Set SVG path unless already set or line not visible
9175
+ // Set SVG path unless already set or line not visible.
9137
9176
  if(this.line_path.length === 0 && this.visible) {
9177
+ // Vector may have to be sorted in some way.
9178
+ const vect = this.vector.slice();
9179
+ if(this.sorted === 'asc') {
9180
+ // Sort values in ascending order.
9181
+ vect.sort();
9182
+ } else if(this.sorted === 'desc') {
9183
+ // Sort values in descending order.
9184
+ vect.sort((a, b) => { return b - a; });
9185
+ } else if(this.chart.time_step_numbers) {
9186
+ // Fill vector with its values sorted by time step.
9187
+ const tsn = this.chart.time_step_numbers;
9188
+ for(let i = 0; i < this.vector.length; i++) {
9189
+ vect[i] = this.vector[tsn[i]];
9190
+ }
9191
+ }
9192
+ //
9138
9193
  let y = y0 - this.vector[0] * dy;
9139
9194
  const
9140
9195
  path = ['M', x0, ',', y],
9141
- l = this.vector.length;
9196
+ l = vect.length;
9142
9197
  // NOTE: Now we can use relative line coordinates
9143
9198
  for(let t = 1; t < l; t++) {
9144
- const new_y = y0 - this.vector[t] * dy;
9199
+ const new_y = y0 - vect[t] * dy;
9145
9200
  path.push(`v${new_y - y}h${dx}`);
9146
9201
  y = new_y;
9147
9202
  }
@@ -9352,6 +9407,37 @@ class Chart {
9352
9407
  }
9353
9408
  }
9354
9409
 
9410
+ sortLeadTimeVector() {
9411
+ // Set the time vector to NULL if no "lead-sort" varianles are
9412
+ // visible, or to a list of N+1 time steps (with N the run length)
9413
+ // sorted on the values of the lead-sorted variables in their order
9414
+ // of appearance in the chart.
9415
+ this.time_step_numbers = null;
9416
+ const
9417
+ lsv = [],
9418
+ lss = [];
9419
+ for(let i = 0; i < this.variables.length; i++) {
9420
+ const cv = this.variables[i];
9421
+ if(cv.visible && cv.sorted.endsWith('-lead')) {
9422
+ lsv.push(cv.vector);
9423
+ lss.push(cv.sorted.startsWith('asc') ? 1 : -1);
9424
+ }
9425
+ }
9426
+ // If no "lead sort" variables, then no need to set the time steps.
9427
+ if(lsv.length === 0) return;
9428
+ this.time_step_numbers = Array.apply(null,
9429
+ {length: this.total_time_steps + 1}).map(Number.call, Number);
9430
+ this.time_step_numbers.sort((a, b) => {
9431
+ let c = 0;
9432
+ for(let i = 0; c === 0 && i < lsv.length; i++) {
9433
+ // Multiply by `lss` (lead sort sign), which will be + 1 for
9434
+ // ascending order and -1 for descending order.
9435
+ c = (lsv[i][a] - lsv[i][b]) * lss[i];
9436
+ }
9437
+ return c;
9438
+ });
9439
+ }
9440
+
9355
9441
  resetVectors() {
9356
9442
  // Empties the vector arrays of all variables
9357
9443
  for(let i = 0; i < this.variables.length; i++) {
@@ -9489,14 +9575,15 @@ class Chart {
9489
9575
  }
9490
9576
  }
9491
9577
  if(this.legend_position != 'none') {
9492
- // Draw the legend items
9578
+ // Draw the legend items.
9493
9579
  let x = leg_left,
9494
- // NOTE: vertical text align is middle, so add half a font height
9580
+ // Vertical text align is middle, so add half a font height.
9495
9581
  y = leg_top + font_height;
9496
9582
  for(let i = 0; i < this.variables.length; i++) {
9497
9583
  let v = this.variables[i];
9498
9584
  if(v.visible) {
9499
- const vn = v.displayName;
9585
+ // Add arrow indicating sort direction to name if applicable.
9586
+ const vn = v.displayName + CHART_MANAGER.sort_arrows[v.sorted];
9500
9587
  if(v.stacked || this.histogram) {
9501
9588
  this.addSVG(['<rect x="', x, '" y="', y - sym_size + 2,
9502
9589
  '" width="', sym_size, '" height="', sym_size,
@@ -9645,6 +9732,9 @@ class Chart {
9645
9732
  }
9646
9733
  }
9647
9734
 
9735
+ // The time step vector is not used when making a histogram.
9736
+ if(!this.histogram) this.sortLeadTimeVector();
9737
+
9648
9738
  // Draw the grid rectangle
9649
9739
  this.addSVG(['<rect id="c_h_a_r_t__a_r_e_a__ID*" x="', rl, '" y="', rt,
9650
9740
  '" width="', rw, '" height="', rh,
@@ -9673,9 +9763,9 @@ class Chart {
9673
9763
  this.addText(x + 5, y, VM.sig2Dig(b), 'black', font_height,
9674
9764
  'text-anchor="end"');
9675
9765
  } else {
9676
- // Draw the time labels along the horizontal axis
9766
+ // Draw the time labels along the horizontal axis.
9677
9767
  // TO DO: convert to time units if modeler checks this additional option
9678
- // Draw the time step duration in bottom-left corner
9768
+ // Draw the time step duration in bottom-left corner.
9679
9769
  this.addText(1, y, 'dt = ' + this.timeScaleAsString(this.time_scale),
9680
9770
  'black', font_height, 'text-anchor="start"');
9681
9771
  // Calculate width corresponding to one half time step
@@ -9856,14 +9946,28 @@ class Chart {
9856
9946
  this.run_index = runs[0];
9857
9947
  v.computeVector();
9858
9948
  }
9949
+ // Vector may have to be sorted in some way.
9950
+ const vect = v.vector.slice();
9951
+ if(v.sorted === 'asc') {
9952
+ // Sort values in ascending order.
9953
+ vect.sort();
9954
+ } else if(v.sorted === 'desc') {
9955
+ // Sort values in descending order.
9956
+ vect.sort((a, b) => { return b - a; });
9957
+ } else if(this.time_step_numbers) {
9958
+ // Fill vector with its values sorted by time step.
9959
+ for(let i = 0; i < v.vector.length; i++) {
9960
+ vect[i] = v.vector[this.time_step_numbers[i]];
9961
+ }
9962
+ }
9859
9963
  // NOTE: add x-value to x0, but SUBTRACT y-value from y0!
9860
9964
  x = x0;
9861
- y = y0 - v.vector[0]*dy - offset[0];
9965
+ y = y0 - vect[0]*dy - offset[0];
9862
9966
  // Begin with the top contour
9863
9967
  const path = ['M', x, ',', y];
9864
- for(let t = 1; t < v.vector.length; t++) {
9968
+ for(let t = 1; t < vect.length; t++) {
9865
9969
  // First draw line to the Y for time step t
9866
- y = y0 - (v.vector[t] + offset[t])*dy;
9970
+ y = y0 - (vect[t] + offset[t])*dy;
9867
9971
  path.push(`L${x},${y}`);
9868
9972
  // Then move right for the duration of time step t
9869
9973
  x += dx;
@@ -9873,13 +9977,13 @@ class Chart {
9873
9977
  // chart variable
9874
9978
  v.line_path = path.join('');
9875
9979
  // Now add the path for the bottom contour (= offset) ...
9876
- for(let t = v.vector.length - 1; t > 0; t--) {
9980
+ for(let t = vect.length - 1; t > 0; t--) {
9877
9981
  y = y0 - offset[t]*dy;
9878
9982
  path.push(`L${x},${y}`);
9879
9983
  x -= dx;
9880
9984
  path.push(`L${x},${y}`);
9881
9985
  // ... while computing the new offset
9882
- offset[t] += v.vector[t];
9986
+ offset[t] += vect[t];
9883
9987
  }
9884
9988
  // Draw the filled area with semi-transparent color
9885
9989
  this.addSVG(['<path d="', path.join(''),
@@ -10115,8 +10219,8 @@ class ActorSelector {
10115
10219
  class ExperimentRunResult {
10116
10220
  constructor(r, v, a='') {
10117
10221
  // NOTE: constructor can be called with `v` a chart variable, a dataset,
10118
- // or an XML node; if `v` is the equations dataset, then `a` indicates the
10119
- // attribute to be used
10222
+ // or an XML node; if `v` is the equations dataset, then `a` is the
10223
+ // identifier of the dataset modifier to be used.
10120
10224
  this.run = r;
10121
10225
  if(v instanceof ChartVariable) {
10122
10226
  this.x_variable = true;
@@ -10188,7 +10292,7 @@ class ExperimentRunResult {
10188
10292
  this.exceptions = 0;
10189
10293
  const
10190
10294
  // NOTE: run result dataset selector will be plain (no wildcards)
10191
- x = v.attributeExpression(this.attribute),
10295
+ x = v.modifiers[this.attribute].expression,
10192
10296
  t_end = MODEL.end_period - MODEL.start_period + 1;
10193
10297
  // N = # time steps
10194
10298
  this.N = t_end;
@@ -10564,7 +10668,7 @@ class ExperimentRun {
10564
10668
  new ExperimentRunResult(this, this.experiment.variables[vi]));
10565
10669
  this.step++;
10566
10670
  UI.setProgressNeedle(this.step / this.steps);
10567
- setTimeout(function(x) { x.addChartResults(vi + 1); }, 0, this);
10671
+ setTimeout((x) => x.addChartResults(vi + 1), 0, this);
10568
10672
  } else {
10569
10673
  this.addOutcomeResults(0);
10570
10674
  }
@@ -10577,7 +10681,7 @@ class ExperimentRun {
10577
10681
  this.results.push(new ExperimentRunResult(this, MODEL.outcomes[oi]));
10578
10682
  this.step++;
10579
10683
  UI.setProgressNeedle(this.step / this.steps);
10580
- setTimeout(function(x) { x.addOutcomeResults(oi + 1); }, 0, this);
10684
+ setTimeout((x) => x.addOutcomeResults(oi + 1), 0, this);
10581
10685
  } else {
10582
10686
  this.addEquationResults(0);
10583
10687
  }
@@ -10592,7 +10696,7 @@ class ExperimentRun {
10592
10696
  new ExperimentRunResult(this, MODEL.equations_dataset, k));
10593
10697
  this.step++;
10594
10698
  UI.setProgressNeedle(this.step / this.steps);
10595
- setTimeout(function(x) { x.addEquationResults(ei + 1); }, 0, this);
10699
+ setTimeout((x) => x.addEquationResults(ei + 1), 0, this);
10596
10700
  } else {
10597
10701
  // Register when this result was stored
10598
10702
  this.time_recorded = new Date().getTime();
@@ -10602,8 +10706,10 @@ class ExperimentRun {
10602
10706
  // Log the time it took to compute all results
10603
10707
  VM.logMessage(VM.block_count - 1,
10604
10708
  `Processing run results took ${VM.elapsedTime} seconds.`);
10605
- // NOTE: addResults is called by either the experiment manager or the
10606
- // sensitivity analysis => proceed there
10709
+ // Report results if applicable.
10710
+ if(RECEIVER.solving || MODEL.report_results) RECEIVER.report();
10711
+ // NOTE: addResults is called by either the experiment manager or
10712
+ // the sensitivity analysis; hence proceed from there.
10607
10713
  if(SENSITIVITY_ANALYSIS.experiment) {
10608
10714
  SENSITIVITY_ANALYSIS.processRestOfRun();
10609
10715
  } else {
@@ -780,13 +780,15 @@ function escapedSingleQuotes(s) {
780
780
  }
781
781
 
782
782
  function nameToLines(name, actor_name = '') {
783
- // Returns the name of a Linny-R entity as a string-with-line-breaks that
784
- // fits nicely in an oblong box. For efficiency reasons, a fixed width/height
785
- // ratio is assumed, as this produces quite acceptable results
783
+ // Return the name of a Linny-R entity as a string-with-line-breaks
784
+ // that fits nicely in an oblong box. For efficiency reasons, a fixed
785
+ // width/height ratio is assumed, as this produces quite acceptable
786
+ // results. Actor names are not split, so their length may stretch
787
+ // the node box.
786
788
  let m = actor_name.length;
787
789
  const
788
790
  d = Math.floor(Math.sqrt(0.3 * name.length)),
789
- // Do not wrap strings shorter than 13 characters (about 50 pixels)
791
+ // Do not wrap strings shorter than 13 characters (about 50 pixels).
790
792
  limit = Math.max(Math.ceil(name.length / d), m, 13),
791
793
  a = name.split(' ');
792
794
  // Split words at '-' when wider than limit
@@ -810,7 +812,12 @@ function nameToLines(name, actor_name = '') {
810
812
  let n = 0,
811
813
  l = ww[n],
812
814
  space;
813
- for(let i = 1; i < a.length; i++) {
815
+ // Actor cash flow indicators like $FLOW have their own line.
816
+ if(a[0].startsWith('$')) {
817
+ n++;
818
+ lines[n] = a[1];
819
+ }
820
+ for(let i = 1 + n; i < a.length; i++) {
814
821
  if(l + ww[i] < limit) {
815
822
  space = (lines[n].endsWith('-') ? '' : ' ');
816
823
  lines[n] += space + a[i];
@@ -348,6 +348,8 @@ class Expression {
348
348
  v[t] = this.stack.pop();
349
349
  }
350
350
  this.trace('RESULT = ' + VM.sig4Dig(v[t]));
351
+ // Store wildcard result also in "normal" vector
352
+ this.vector[t] = v[t];
351
353
  // Pop the time step.
352
354
  this.step.pop();
353
355
  this.trace('--STOP: ' + this.variableName);
@@ -3349,8 +3351,42 @@ class VirtualMachine {
3349
3351
  k = product_keys[pi];
3350
3352
  if(!MODEL.ignored_entities[k]) {
3351
3353
  p = MODEL.products[k];
3354
+ // NOTE: Actor cash flow data products are a special case.
3355
+ if(p.name.startsWith('$')) {
3356
+ // Get the associated actor entity.
3357
+ const parts = p.name.substring(1).split(' ');
3358
+ parts.shift();
3359
+ const
3360
+ aid = UI.nameToID(parts.join(' ')),
3361
+ a = MODEL.actorByID(aid);
3362
+ if(a) {
3363
+ this.code.push([VMI_clear_coefficients, null]);
3364
+ // Use actor's cash variable indices w/o weight.
3365
+ if(p.name.startsWith('$IN ')) {
3366
+ // Add coefficient +1 for cash IN index.
3367
+ this.code.push([VMI_add_const_to_coefficient,
3368
+ [a.cash_in_var_index, 1, 0]]);
3369
+ } else if(p.name.startsWith('$OUT ')) {
3370
+ // Add coefficient +1 for cash OUT index.
3371
+ this.code.push([VMI_add_const_to_coefficient,
3372
+ [a.cash_out_var_index, 1, 0]]);
3373
+ } else if(p.name.startsWith('$FLOW ')) {
3374
+ // Add coefficient +1 for cash IN index.
3375
+ this.code.push([VMI_add_const_to_coefficient,
3376
+ [a.cash_in_var_index, 1, 0]]);
3377
+ // Add coefficient -1 for cash OUT index.
3378
+ this.code.push([VMI_add_const_to_coefficient,
3379
+ [a.cash_out_var_index, -1, 0]]);
3380
+ }
3381
+ // Add coefficient -1 for level index variable of `p`.
3382
+ this.code.push([VMI_add_const_to_coefficient,
3383
+ [p.level_var_index, -1, 0]]);
3384
+ this.code.push([VMI_add_constraint, VM.EQ]);
3385
+ } else {
3386
+ console.log('ANOMALY: no actor for cash flow product', p.displayName);
3387
+ }
3352
3388
  // NOTE: constants are not affected by their outgoing data (!) links
3353
- if(!p.isConstant) {
3389
+ } else if(!p.isConstant) {
3354
3390
 
3355
3391
  // FIRST: add a constraint that "computes" the product stock level
3356
3392
  // set coefficients vector to 0 (NOTE: this also sets RHS to 0)
@@ -5268,10 +5304,13 @@ Solver status = ${json.status}`);
5268
5304
  UI.drawDiagram(MODEL);
5269
5305
  // Show the reset button (GUI only)
5270
5306
  UI.readyToReset();
5271
- // If receiver is active, report results
5272
- if(RECEIVER.solving || MODEL.report_results) RECEIVER.report();
5273
- // If experiment is active, signal the manager
5274
- if(MODEL.running_experiment) EXPERIMENT_MANAGER.processRun();
5307
+ if(MODEL.running_experiment) {
5308
+ // If experiment is active, signal the manager.
5309
+ EXPERIMENT_MANAGER.processRun();
5310
+ } else if(RECEIVER.solving || MODEL.report_results) {
5311
+ // Otherwise report results now, if applicable.
5312
+ RECEIVER.report();
5313
+ }
5275
5314
  // Warn modeler if any issues occurred
5276
5315
  if(this.block_issues) {
5277
5316
  let msg = 'Issues occurred in ' +
@@ -5677,8 +5716,8 @@ function valueOfNumberSign(x) {
5677
5716
  // ending on digits, or tne number context of an entity. The latter typically
5678
5717
  // is the number its name or any of its prefixes ends on, but notes are
5679
5718
  // more "creative" and can return the number context of nearby entities.
5680
- let s = 'NO SELECTOR',
5681
- m = 'NO MATCH',
5719
+ let s = '!NO SELECTOR',
5720
+ m = '!NO MATCH',
5682
5721
  n = VM.UNDEFINED;
5683
5722
  // NOTE: Give wildcard selectors precedence over experiment selectors
5684
5723
  // because a wildcard selector is an immediate property of the dataset
@@ -5692,17 +5731,15 @@ function valueOfNumberSign(x) {
5692
5731
  // Check whether `x` is a dataset modifier expression.
5693
5732
  // NOTE: This includes equations.
5694
5733
  if(x.object instanceof Dataset) {
5695
- if(x.attribute) {
5696
- s = x.attribute;
5697
- } else {
5698
- // Selector may also be defined by a running experiment.
5699
- if(MODEL.running_experiment) {
5700
- // Let `m` be the primary selector for the current experiment run.
5701
- const sl = MODEL.running_experiment.activeCombination;
5702
- if(sl) {
5703
- m = sl[0];
5704
- s = 'experiment';
5705
- }
5734
+ if(x.attribute) s = x.attribute;
5735
+ // Selector may also be defined by a running experiment.
5736
+ if(MODEL.running_experiment) {
5737
+ const
5738
+ ac = MODEL.running_experiment.activeCombination,
5739
+ mn = matchingNumberInList(ac, s);
5740
+ if(mn !== false) {
5741
+ m = 'x-run';
5742
+ n = mn;
5706
5743
  }
5707
5744
  }
5708
5745
  }
@@ -5715,10 +5752,6 @@ function valueOfNumberSign(x) {
5715
5752
  m = d;
5716
5753
  n = parseInt(d);
5717
5754
  }
5718
- } else if(m !== 'NO MATCH') {
5719
- // If matching `m` against `s` do not yield an integer string,
5720
- // use UNDEFINED
5721
- n = matchingNumber(m, s) || VM.UNDEFINED;
5722
5755
  }
5723
5756
  }
5724
5757
  // For datasets, set the parent anchor to be the context-sensitive number
@@ -5941,7 +5974,7 @@ function VMI_push_dataset_modifier(x, args) {
5941
5974
  // If `s` is not specified, the modifier to be used must be inferred from
5942
5975
  // the running experiment UNLESS the field `ud` ("use data") is defined
5943
5976
  // for the first argument, and evaluates as TRUE.
5944
- // NOTE: Ensure that number 0 is not interpreted as FALSE.
5977
+ // NOTE: Ensure that number 0 is not interpreted as FALSE.
5945
5978
  let wcnr = (args[0].s === undefined ? false : args[0].s);
5946
5979
  const
5947
5980
  ds = args[0].d,
@@ -5960,7 +5993,6 @@ function VMI_push_dataset_modifier(x, args) {
5960
5993
  let t = tot[0],
5961
5994
  // By default, use the vector of the dataset to compute the value.
5962
5995
  obj = ds.vector;
5963
-
5964
5996
  if(ds.array) {
5965
5997
  // For array-type datasets, do NOT adjust "index" t to model run period.
5966
5998
  // NOTE: Indices start at 1, but arrays are zero-based, so subtract 1.
@@ -5994,16 +6026,17 @@ function VMI_push_dataset_modifier(x, args) {
5994
6026
  if(wcnr === '?') {
5995
6027
  wcnr = x.wildcard_vector_index;
5996
6028
  }
5997
- } else if(!ud ) {
6029
+ } else if(!ud) {
5998
6030
  // In no selector and not "use data", check whether a running experiment
5999
6031
  // defines the expression to use. If not, `obj` will be the dataset
6000
6032
  // vector (so same as when "use data" is set).
6001
6033
  obj = ds.activeModifierExpression;
6002
- if(wcnr === false && obj instanceof Expression && MODEL.running_experiment) {
6034
+ if(wcnr === false && MODEL.running_experiment) {
6003
6035
  // If experiment run defines the modifier selector, the active
6004
6036
  // combination may provide a context for #.
6005
- wcnr = matchingNumberInList(MODEL.running_experiment.activeCombination,
6006
- obj.attribute);
6037
+ const sel = (obj instanceof Expression ? obj.attribute : x.attribute);
6038
+ wcnr = matchingNumberInList(
6039
+ MODEL.running_experiment.activeCombination, sel);
6007
6040
  }
6008
6041
  }
6009
6042
  if(!obj) {
@@ -6040,7 +6073,9 @@ function VMI_push_dataset_modifier(x, args) {
6040
6073
  // `obj` is an expression.
6041
6074
  // NOTE: Readjust `t` when `obj` is an expression for an *array-type*
6042
6075
  // dataset modifier.
6043
- if(obj.object instanceof Dataset && obj.object.array) t++;
6076
+ if(obj.object instanceof Dataset && obj.object.array) {
6077
+ t++;
6078
+ }
6044
6079
  v = obj.result(t, wcnr);
6045
6080
  }
6046
6081
  // Trace only now that time step t has been computed.