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 +1 -1
- package/static/index.html +37 -3
- package/static/linny-r.css +49 -3
- package/static/scripts/linny-r-ctrl.js +28 -10
- 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-equation-manager.js +46 -1
- 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 +149 -43
- package/static/scripts/linny-r-utils.js +12 -5
- package/static/scripts/linny-r-vm.js +64 -29
package/package.json
CHANGED
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"
|
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"
|
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"
|
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" -->
|
package/static/linny-r.css
CHANGED
@@ -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:
|
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
|
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">
|
@@ -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
|
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);
|
@@ -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
|
-
|
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
|
}
|
@@ -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(
|
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
|
-
//
|
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
|
}
|
@@ -8434,15 +8453,26 @@ class Dataset {
|
|
8434
8453
|
}
|
8435
8454
|
|
8436
8455
|
get plainSelectors() {
|
8437
|
-
//
|
8438
|
-
const sl = this.selectorList;
|
8439
|
-
// NOTE:
|
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 =
|
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 -
|
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
|
-
//
|
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
|
-
|
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 -
|
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 <
|
9968
|
+
for(let t = 1; t < vect.length; t++) {
|
9865
9969
|
// First draw line to the Y for time step t
|
9866
|
-
y = y0 - (
|
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 =
|
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] +=
|
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`
|
10119
|
-
//
|
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.
|
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(
|
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(
|
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(
|
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
|
-
//
|
10606
|
-
|
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
|
-
//
|
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];
|
@@ -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
|
-
|
5272
|
-
|
5273
|
-
|
5274
|
-
if(MODEL.
|
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
|
-
|
5697
|
-
|
5698
|
-
|
5699
|
-
|
5700
|
-
|
5701
|
-
|
5702
|
-
|
5703
|
-
|
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 &&
|
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
|
-
|
6006
|
-
|
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)
|
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.
|