linny-r 1.4.6 → 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 +1 -1
- package/static/index.html +23 -3
- package/static/linny-r.css +45 -1
- package/static/scripts/linny-r-ctrl.js +27 -9
- package/static/scripts/linny-r-gui-chart-manager.js +62 -7
- package/static/scripts/linny-r-gui-controller.js +69 -11
- package/static/scripts/linny-r-gui-expression-editor.js +3 -0
- package/static/scripts/linny-r-gui-finder.js +4 -1
- package/static/scripts/linny-r-gui-paper.js +20 -10
- package/static/scripts/linny-r-model.js +114 -27
- package/static/scripts/linny-r-utils.js +12 -5
- package/static/scripts/linny-r-vm.js +35 -1
package/package.json
CHANGED
package/static/index.html
CHANGED
@@ -1854,16 +1854,19 @@ NOTE: * and ? will be interpreted as wildcards"
|
|
1854
1854
|
src="images/add.png"
|
1855
1855
|
title="Add variable (Shift-click to add a new equation)">
|
1856
1856
|
<img id="chart-variable-up-btn" class="btn disab"
|
1857
|
-
src="images/up.png"
|
1857
|
+
src="images/up.png"
|
1858
1858
|
title="Move selected variable up in list">
|
1859
1859
|
<img id="chart-variable-down-btn" class="btn disab"
|
1860
1860
|
src="images/down.png"
|
1861
1861
|
title="Move selected variable down in list">
|
1862
1862
|
<img id="chart-edit-variable-btn" class="btn disab"
|
1863
|
-
src="images/edit.png"
|
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"
|
1864
1867
|
title="Edit selected variable">
|
1865
1868
|
<img id="chart-delete-variable-btn" class="btn disab"
|
1866
|
-
src="images/remove.png"
|
1869
|
+
src="images/remove.png"
|
1867
1870
|
title="Delete selected variable">
|
1868
1871
|
</div>
|
1869
1872
|
</div>
|
@@ -1871,6 +1874,23 @@ NOTE: * and ? will be interpreted as wildcards"
|
|
1871
1874
|
<table id="chart-variables-table">
|
1872
1875
|
</table>
|
1873
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>
|
1874
1894
|
</div>
|
1875
1895
|
<div id="chart-display-panel">
|
1876
1896
|
<!-- NOTE: the scroller scrolls when the container is "stretched" -->
|
package/static/linny-r.css
CHANGED
@@ -2750,15 +2750,59 @@ td.equation-expression {
|
|
2750
2750
|
width: 146px;
|
2751
2751
|
display: inline-block;
|
2752
2752
|
position: absolute;
|
2753
|
-
left:
|
2753
|
+
left: 55px;
|
2754
2754
|
top: -2px;
|
2755
2755
|
}
|
2756
2756
|
|
2757
2757
|
#chart-variable-buttons > img.btn {
|
2758
2758
|
height: 18px;
|
2759
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;
|
2760
2786
|
}
|
2761
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
|
+
|
2762
2806
|
#chart-variables {
|
2763
2807
|
position: absolute;
|
2764
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
|
722
|
-
// for the SVG units for line width = 1
|
723
|
-
// defined to work for images of this
|
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
|
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
|
-
//
|
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:
|
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">
|
@@ -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
|
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
|
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
|
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
|
-
|
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.
|
3677
|
-
|
3678
|
-
|
3679
|
-
|
3680
|
-
|
3681
|
-
|
3682
|
-
|
3683
|
-
|
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);
|
@@ -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
|
-
|
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
|
-
|
2740
|
-
|
2741
|
-
|
2742
|
-
|
2743
|
-
|
2744
|
-
|
2745
|
-
|
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
|
-
|
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
|
}
|
@@ -7695,9 +7713,10 @@ class Product extends Node {
|
|
7695
7713
|
}
|
7696
7714
|
|
7697
7715
|
get isConstant() {
|
7698
|
-
//
|
7699
|
-
// and has set LB = UB
|
7700
|
-
if(!this.is_data ||
|
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
|
}
|
@@ -8909,7 +8928,7 @@ class ChartVariable {
|
|
8909
8928
|
this.wildcard_index = false;
|
8910
8929
|
}
|
8911
8930
|
|
8912
|
-
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') {
|
8913
8932
|
// Sets the defining properties for this chart variable.
|
8914
8933
|
this.object = obj;
|
8915
8934
|
this.attribute = attr;
|
@@ -8918,6 +8937,7 @@ class ChartVariable {
|
|
8918
8937
|
this.scale_factor = sf;
|
8919
8938
|
this.line_width = lw;
|
8920
8939
|
this.visible = vis;
|
8940
|
+
this.sorted = sort;
|
8921
8941
|
}
|
8922
8942
|
|
8923
8943
|
get displayName() {
|
@@ -8956,6 +8976,7 @@ class ChartVariable {
|
|
8956
8976
|
}
|
8957
8977
|
const xml = ['<chart-variable', (this.stacked ? ' stacked="1"' : ''),
|
8958
8978
|
(this.visible ? ' visible="1"' : ''),
|
8979
|
+
` sorted="${this.sorted}"`,
|
8959
8980
|
'><object-id>', xmlEncoded(id),
|
8960
8981
|
'</object-id><attribute>', this.attribute,
|
8961
8982
|
'</attribute><color>', this.color,
|
@@ -9007,7 +9028,8 @@ class ChartVariable {
|
|
9007
9028
|
nodeContentByTag(node, 'color'),
|
9008
9029
|
safeStrToFloat(nodeContentByTag(node, 'scale-factor')),
|
9009
9030
|
safeStrToFloat(nodeContentByTag(node, 'line-width')),
|
9010
|
-
nodeParameterValue(node, 'visible') === '1'
|
9031
|
+
nodeParameterValue(node, 'visible') === '1',
|
9032
|
+
nodeParameterValue(node, 'sorted') || 'not');
|
9011
9033
|
return true;
|
9012
9034
|
}
|
9013
9035
|
|
@@ -9150,15 +9172,31 @@ class ChartVariable {
|
|
9150
9172
|
}
|
9151
9173
|
|
9152
9174
|
setLinePath(x0, y0, dx, dy) {
|
9153
|
-
// Set SVG path unless already set or line not visible
|
9175
|
+
// Set SVG path unless already set or line not visible.
|
9154
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
|
+
//
|
9155
9193
|
let y = y0 - this.vector[0] * dy;
|
9156
9194
|
const
|
9157
9195
|
path = ['M', x0, ',', y],
|
9158
|
-
l =
|
9196
|
+
l = vect.length;
|
9159
9197
|
// NOTE: Now we can use relative line coordinates
|
9160
9198
|
for(let t = 1; t < l; t++) {
|
9161
|
-
const new_y = y0 -
|
9199
|
+
const new_y = y0 - vect[t] * dy;
|
9162
9200
|
path.push(`v${new_y - y}h${dx}`);
|
9163
9201
|
y = new_y;
|
9164
9202
|
}
|
@@ -9369,6 +9407,37 @@ class Chart {
|
|
9369
9407
|
}
|
9370
9408
|
}
|
9371
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
|
+
|
9372
9441
|
resetVectors() {
|
9373
9442
|
// Empties the vector arrays of all variables
|
9374
9443
|
for(let i = 0; i < this.variables.length; i++) {
|
@@ -9506,14 +9575,15 @@ class Chart {
|
|
9506
9575
|
}
|
9507
9576
|
}
|
9508
9577
|
if(this.legend_position != 'none') {
|
9509
|
-
// Draw the legend items
|
9578
|
+
// Draw the legend items.
|
9510
9579
|
let x = leg_left,
|
9511
|
-
//
|
9580
|
+
// Vertical text align is middle, so add half a font height.
|
9512
9581
|
y = leg_top + font_height;
|
9513
9582
|
for(let i = 0; i < this.variables.length; i++) {
|
9514
9583
|
let v = this.variables[i];
|
9515
9584
|
if(v.visible) {
|
9516
|
-
|
9585
|
+
// Add arrow indicating sort direction to name if applicable.
|
9586
|
+
const vn = v.displayName + CHART_MANAGER.sort_arrows[v.sorted];
|
9517
9587
|
if(v.stacked || this.histogram) {
|
9518
9588
|
this.addSVG(['<rect x="', x, '" y="', y - sym_size + 2,
|
9519
9589
|
'" width="', sym_size, '" height="', sym_size,
|
@@ -9662,6 +9732,9 @@ class Chart {
|
|
9662
9732
|
}
|
9663
9733
|
}
|
9664
9734
|
|
9735
|
+
// The time step vector is not used when making a histogram.
|
9736
|
+
if(!this.histogram) this.sortLeadTimeVector();
|
9737
|
+
|
9665
9738
|
// Draw the grid rectangle
|
9666
9739
|
this.addSVG(['<rect id="c_h_a_r_t__a_r_e_a__ID*" x="', rl, '" y="', rt,
|
9667
9740
|
'" width="', rw, '" height="', rh,
|
@@ -9690,9 +9763,9 @@ class Chart {
|
|
9690
9763
|
this.addText(x + 5, y, VM.sig2Dig(b), 'black', font_height,
|
9691
9764
|
'text-anchor="end"');
|
9692
9765
|
} else {
|
9693
|
-
// Draw the time labels along the horizontal axis
|
9766
|
+
// Draw the time labels along the horizontal axis.
|
9694
9767
|
// TO DO: convert to time units if modeler checks this additional option
|
9695
|
-
// Draw the time step duration in bottom-left corner
|
9768
|
+
// Draw the time step duration in bottom-left corner.
|
9696
9769
|
this.addText(1, y, 'dt = ' + this.timeScaleAsString(this.time_scale),
|
9697
9770
|
'black', font_height, 'text-anchor="start"');
|
9698
9771
|
// Calculate width corresponding to one half time step
|
@@ -9873,14 +9946,28 @@ class Chart {
|
|
9873
9946
|
this.run_index = runs[0];
|
9874
9947
|
v.computeVector();
|
9875
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
|
+
}
|
9876
9963
|
// NOTE: add x-value to x0, but SUBTRACT y-value from y0!
|
9877
9964
|
x = x0;
|
9878
|
-
y = y0 -
|
9965
|
+
y = y0 - vect[0]*dy - offset[0];
|
9879
9966
|
// Begin with the top contour
|
9880
9967
|
const path = ['M', x, ',', y];
|
9881
|
-
for(let t = 1; t <
|
9968
|
+
for(let t = 1; t < vect.length; t++) {
|
9882
9969
|
// First draw line to the Y for time step t
|
9883
|
-
y = y0 - (
|
9970
|
+
y = y0 - (vect[t] + offset[t])*dy;
|
9884
9971
|
path.push(`L${x},${y}`);
|
9885
9972
|
// Then move right for the duration of time step t
|
9886
9973
|
x += dx;
|
@@ -9890,13 +9977,13 @@ class Chart {
|
|
9890
9977
|
// chart variable
|
9891
9978
|
v.line_path = path.join('');
|
9892
9979
|
// Now add the path for the bottom contour (= offset) ...
|
9893
|
-
for(let t =
|
9980
|
+
for(let t = vect.length - 1; t > 0; t--) {
|
9894
9981
|
y = y0 - offset[t]*dy;
|
9895
9982
|
path.push(`L${x},${y}`);
|
9896
9983
|
x -= dx;
|
9897
9984
|
path.push(`L${x},${y}`);
|
9898
9985
|
// ... while computing the new offset
|
9899
|
-
offset[t] +=
|
9986
|
+
offset[t] += vect[t];
|
9900
9987
|
}
|
9901
9988
|
// Draw the filled area with semi-transparent color
|
9902
9989
|
this.addSVG(['<path d="', path.join(''),
|
@@ -780,13 +780,15 @@ function escapedSingleQuotes(s) {
|
|
780
780
|
}
|
781
781
|
|
782
782
|
function nameToLines(name, actor_name = '') {
|
783
|
-
//
|
784
|
-
// fits nicely in an oblong box. For efficiency reasons, a fixed
|
785
|
-
// ratio is assumed, as this produces quite acceptable
|
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
|
-
|
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];
|
@@ -3351,8 +3351,42 @@ class VirtualMachine {
|
|
3351
3351
|
k = product_keys[pi];
|
3352
3352
|
if(!MODEL.ignored_entities[k]) {
|
3353
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
|
+
}
|
3354
3388
|
// NOTE: constants are not affected by their outgoing data (!) links
|
3355
|
-
if(!p.isConstant) {
|
3389
|
+
} else if(!p.isConstant) {
|
3356
3390
|
|
3357
3391
|
// FIRST: add a constraint that "computes" the product stock level
|
3358
3392
|
// set coefficients vector to 0 (NOTE: this also sets RHS to 0)
|