linny-r 1.1.22 → 1.2.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/README.md +7 -6
- package/package.json +1 -1
- package/static/images/combination.png +0 -0
- package/static/images/iterator.png +0 -0
- package/static/images/scale.png +0 -0
- package/static/index.html +201 -16
- package/static/linny-r.css +214 -33
- package/static/scripts/linny-r-config.js +6 -0
- package/static/scripts/linny-r-ctrl.js +28 -8
- package/static/scripts/linny-r-gui.js +709 -113
- package/static/scripts/linny-r-model.js +943 -273
- package/static/scripts/linny-r-utils.js +5 -0
- package/static/scripts/linny-r-vm.js +324 -93
@@ -589,7 +589,7 @@ class Paper {
|
|
589
589
|
}
|
590
590
|
|
591
591
|
numberSize(number, fsize=8, fweight=400) {
|
592
|
-
// Returns the boundingbox {width: ..., height: ...} of a
|
592
|
+
// Returns the boundingbox {width: ..., height: ...} of a numeric
|
593
593
|
// string (in pixels)
|
594
594
|
// NOTE: this routine is about 500x faster than textSize because it
|
595
595
|
// does not use the DOM tree
|
@@ -939,6 +939,8 @@ class Paper {
|
|
939
939
|
}
|
940
940
|
// Resize paper if necessary
|
941
941
|
this.extend();
|
942
|
+
// Display model name in browser
|
943
|
+
document.title = mdl.name || 'Linny-R';
|
942
944
|
}
|
943
945
|
|
944
946
|
drawSelection(mdl, dx=0, dy=0) {
|
@@ -2965,6 +2967,10 @@ class GUIController extends Controller {
|
|
2965
2967
|
// NOTE: responding to `mouseenter` is needed to update the cursor position
|
2966
2968
|
// after closing a modal dialog
|
2967
2969
|
this.cc.addEventListener('mouseenter', (event) => UI.mouseMove(event));
|
2970
|
+
// Products can be dragged from the Finder to add a placeholder for
|
2971
|
+
// it to the focal cluster
|
2972
|
+
this.cc.addEventListener('dragover', (event) => UI.dragOver(event));
|
2973
|
+
this.cc.addEventListener('drop', (event) => UI.drop(event));
|
2968
2974
|
|
2969
2975
|
// Disable dragging on all images
|
2970
2976
|
const
|
@@ -3123,6 +3129,10 @@ class GUIController extends Controller {
|
|
3123
3129
|
// Ensure that model documentation can no longer be edited
|
3124
3130
|
DOCUMENTATION_MANAGER.clearEntity([MODEL]);
|
3125
3131
|
});
|
3132
|
+
// Make the scale units button of the settings dialog responsive
|
3133
|
+
this.modals.settings.element('scale-units-btn').addEventListener('click',
|
3134
|
+
// Open the scale units modal dialog on top of the settings dialog
|
3135
|
+
() => SCALE_UNIT_MANAGER.show());
|
3126
3136
|
|
3127
3137
|
// Modals related to vertical toolbar buttons
|
3128
3138
|
this.modals['add-process'].ok.addEventListener('click',
|
@@ -3265,7 +3275,7 @@ class GUIController extends Controller {
|
|
3265
3275
|
if(letters.indexOf('C') >= 0) CHART_MANAGER.updateDialog();
|
3266
3276
|
if(letters.indexOf('D') >= 0) DATASET_MANAGER.updateDialog();
|
3267
3277
|
if(letters.indexOf('E') >= 0) EQUATION_MANAGER.updateDialog();
|
3268
|
-
if(letters.indexOf('F') >= 0) FINDER.
|
3278
|
+
if(letters.indexOf('F') >= 0) FINDER.updateDialog();
|
3269
3279
|
if(letters.indexOf('I') >= 0) DOCUMENTATION_MANAGER.updateDialog();
|
3270
3280
|
if(letters.indexOf('J') >= 0) SENSITIVITY_ANALYSIS.updateDialog();
|
3271
3281
|
if(letters.indexOf('X') >= 0) EXPERIMENT_MANAGER.updateDialog();
|
@@ -3276,6 +3286,7 @@ class GUIController extends Controller {
|
|
3276
3286
|
const loaded = MODEL.parseXML(xml);
|
3277
3287
|
// If not a valid Linny-R model, ensure that the current model is clean
|
3278
3288
|
if(!loaded) MODEL = new LinnyRModel();
|
3289
|
+
this.updateScaleUnitList();
|
3279
3290
|
this.drawDiagram(MODEL);
|
3280
3291
|
// Cursor may have been set to `waiting` when decrypting
|
3281
3292
|
this.normalCursor();
|
@@ -3798,15 +3809,26 @@ class GUIController extends Controller {
|
|
3798
3809
|
//
|
3799
3810
|
// Handlers for mouse/cursor events
|
3800
3811
|
//
|
3801
|
-
|
3802
|
-
|
3803
|
-
//
|
3804
|
-
this.on_node = null;
|
3812
|
+
|
3813
|
+
updateCursorPosition(e) {
|
3814
|
+
// Updates the cursor coordinates and displays them on the status bar
|
3805
3815
|
const cp = this.paper.cursorPosition(e.pageX, e.pageY);
|
3806
3816
|
this.mouse_x = cp[0];
|
3807
3817
|
this.mouse_y = cp[1];
|
3808
3818
|
document.getElementById('pos-x').innerHTML = 'X = ' + this.mouse_x;
|
3809
|
-
document.getElementById('pos-y').innerHTML = 'Y = ' + this.mouse_y;
|
3819
|
+
document.getElementById('pos-y').innerHTML = 'Y = ' + this.mouse_y;
|
3820
|
+
this.on_note = null;
|
3821
|
+
this.on_node = null;
|
3822
|
+
this.on_cluster = null;
|
3823
|
+
this.on_cluster_edge = false;
|
3824
|
+
this.on_arrow = null;
|
3825
|
+
this.on_link = null;
|
3826
|
+
this.on_constraint = false;
|
3827
|
+
}
|
3828
|
+
|
3829
|
+
mouseMove(e) {
|
3830
|
+
// Responds to mouse cursor moving over Linny-R diagram area
|
3831
|
+
this.updateCursorPosition(e);
|
3810
3832
|
|
3811
3833
|
// NOTE: check, as MODEL might still be undefined
|
3812
3834
|
if(!MODEL) return;
|
@@ -3829,8 +3851,6 @@ class GUIController extends Controller {
|
|
3829
3851
|
}
|
3830
3852
|
}
|
3831
3853
|
}
|
3832
|
-
this.on_arrow = null;
|
3833
|
-
this.on_link = null;
|
3834
3854
|
for(let i = 0; i < fc.arrows.length; i++) {
|
3835
3855
|
const arr = fc.arrows[i];
|
3836
3856
|
if(arr) {
|
@@ -3853,8 +3873,6 @@ class GUIController extends Controller {
|
|
3853
3873
|
}
|
3854
3874
|
}
|
3855
3875
|
}
|
3856
|
-
this.on_cluster = null;
|
3857
|
-
this.on_cluster_edge = false;
|
3858
3876
|
for(let i = fc.sub_clusters.length-1; i >= 0; i--) {
|
3859
3877
|
const obj = fc.sub_clusters[i];
|
3860
3878
|
// NOTE: ignore cluster that is being dragged, so that a cluster it is
|
@@ -3874,7 +3892,6 @@ class GUIController extends Controller {
|
|
3874
3892
|
// NOTE: element is persistent, so semi-transparency must also be undone
|
3875
3893
|
c.shape.element.setAttribute('opacity', 1);
|
3876
3894
|
}
|
3877
|
-
this.on_note = null;
|
3878
3895
|
for(let i = fc.notes.length-1; i >= 0; i--) {
|
3879
3896
|
const obj = fc.notes[i];
|
3880
3897
|
if(obj.containsPoint(this.mouse_x, this.mouse_y)) {
|
@@ -4294,6 +4311,29 @@ class GUIController extends Controller {
|
|
4294
4311
|
this.start_sel_y = -1;
|
4295
4312
|
this.updateButtons();
|
4296
4313
|
}
|
4314
|
+
|
4315
|
+
dragOver(e) {
|
4316
|
+
// Accepts products that are dragged from the Finder and do not have
|
4317
|
+
// a placeholder in the focal cluster
|
4318
|
+
this.updateCursorPosition(e);
|
4319
|
+
const p = MODEL.products[e.dataTransfer.getData('text')];
|
4320
|
+
if(p && MODEL.focal_cluster.indexOfProduct(p) < 0) e.preventDefault();
|
4321
|
+
}
|
4322
|
+
|
4323
|
+
drop(e) {
|
4324
|
+
// Adds a product that is dragged from the Finder to the focal cluster
|
4325
|
+
// at the cursor position if it does not have a placeholder yet
|
4326
|
+
const p = MODEL.products[e.dataTransfer.getData('text')];
|
4327
|
+
if(p && MODEL.focal_cluster.indexOfProduct(p) < 0) {
|
4328
|
+
e.preventDefault();
|
4329
|
+
MODEL.focal_cluster.addProductPosition(p, this.mouse_x, this.mouse_y);
|
4330
|
+
UNDO_STACK.push('add', p);
|
4331
|
+
this.selectNode(p);
|
4332
|
+
this.drawDiagram(MODEL);
|
4333
|
+
}
|
4334
|
+
// NOTE: update afterwards, as the modeler may target a precise (X, Y)
|
4335
|
+
this.updateCursorPosition(e);
|
4336
|
+
}
|
4297
4337
|
|
4298
4338
|
//
|
4299
4339
|
// Handler for keyboard events
|
@@ -4632,7 +4672,18 @@ class GUIController extends Controller {
|
|
4632
4672
|
if(name === 'initial level') x.is_static = true;
|
4633
4673
|
return true;
|
4634
4674
|
}
|
4635
|
-
|
4675
|
+
|
4676
|
+
updateScaleUnitList() {
|
4677
|
+
// Update the HTML datalist element to reflect all scale units
|
4678
|
+
const
|
4679
|
+
ul = [],
|
4680
|
+
keys = Object.keys(MODEL.scale_units).sort(ciCompare);
|
4681
|
+
for(let i = 0; i < keys.length; i++) {
|
4682
|
+
ul.push(`<option value="${MODEL.scale_units[keys[i]].name}">`);
|
4683
|
+
}
|
4684
|
+
document.getElementById('units-data').innerHTML = ul.join('');
|
4685
|
+
}
|
4686
|
+
|
4636
4687
|
//
|
4637
4688
|
// Navigation in the cluster hierarchy
|
4638
4689
|
//
|
@@ -4824,6 +4875,7 @@ class GUIController extends Controller {
|
|
4824
4875
|
// Create a brand new model with (optionally) specified name and author
|
4825
4876
|
MODEL = new LinnyRModel(
|
4826
4877
|
md.element('name').value.trim(), md.element('author').value.trim());
|
4878
|
+
MODEL.addPreconfiguredScaleUnits();
|
4827
4879
|
md.hide();
|
4828
4880
|
this.updateTimeStep(MODEL.simulationTimeStep);
|
4829
4881
|
this.drawDiagram(MODEL);
|
@@ -5135,9 +5187,15 @@ class GUIController extends Controller {
|
|
5135
5187
|
md.element('time-limit').focus();
|
5136
5188
|
return false;
|
5137
5189
|
}
|
5190
|
+
const
|
5191
|
+
e = md.element('product-unit'),
|
5192
|
+
dsu = UI.cleanName(e.value) || '1';
|
5138
5193
|
model.name = md.element('name').value.trim();
|
5194
|
+
// Display model name in browser unless blank
|
5195
|
+
document.title = model.name || 'Linny-R';
|
5139
5196
|
model.author = md.element('author').value.trim();
|
5140
|
-
model.
|
5197
|
+
if(!model.scale_units.hasOwnProperty(dsu)) model.addScaleUnit(dsu);
|
5198
|
+
model.default_unit = dsu;
|
5141
5199
|
model.currency_unit = md.element('currency-unit').value.trim();
|
5142
5200
|
model.encrypt = UI.boxChecked('settings-encrypt');
|
5143
5201
|
model.decimal_comma = UI.boxChecked('settings-decimal-comma');
|
@@ -5312,9 +5370,11 @@ class GUIController extends Controller {
|
|
5312
5370
|
this.setBox('product-sink', p.is_sink);
|
5313
5371
|
this.setBox('product-data', p.is_data);
|
5314
5372
|
this.setBox('product-stock', p.is_buffer);
|
5373
|
+
// NOTE: price label includes the currency unit and the product unit,
|
5374
|
+
// e.g., EUR/ton
|
5315
5375
|
md.element('P').value = p.price.text;
|
5316
5376
|
md.element('P-unit').innerHTML =
|
5317
|
-
(p.scale_unit === '1' ? '' : p.scale_unit);
|
5377
|
+
(p.scale_unit === '1' ? '' : '/' + p.scale_unit);
|
5318
5378
|
md.element('currency').innerHTML = MODEL.currency_unit;
|
5319
5379
|
md.element('IL').value = p.initial_level.text;
|
5320
5380
|
this.setBox('product-integer', p.integer_level);
|
@@ -5395,7 +5455,7 @@ class GUIController extends Controller {
|
|
5395
5455
|
}
|
5396
5456
|
}
|
5397
5457
|
// Update other properties
|
5398
|
-
p.
|
5458
|
+
p.changeScaleUnit(md.element('unit').value);
|
5399
5459
|
p.equal_bounds = this.getEqualBounds('product-UB-equal');
|
5400
5460
|
p.is_source = this.boxChecked('product-source');
|
5401
5461
|
p.is_sink = this.boxChecked('product-sink');
|
@@ -5851,8 +5911,12 @@ class GUIMonitor {
|
|
5851
5911
|
document.getElementById('call-stack-error').innerHTML =
|
5852
5912
|
`ERROR at t=${t}: ` + VM.errorMessage(err);
|
5853
5913
|
for(let i = 0; i < csl; i++) {
|
5854
|
-
const
|
5855
|
-
|
5914
|
+
const
|
5915
|
+
x = VM.call_stack[i],
|
5916
|
+
// For equations, only show the attribute
|
5917
|
+
ons = (x.object === MODEL.equations_dataset ? '' :
|
5918
|
+
x.object.displayName + '|');
|
5919
|
+
vlist.push(ons + x.attribute);
|
5856
5920
|
// Trim spaces around all object-attribute separators in the expression
|
5857
5921
|
xlist.push(x.text.replace(/\s*\|\s*/g, '|'));
|
5858
5922
|
}
|
@@ -6010,24 +6074,30 @@ class GUIMonitor {
|
|
6010
6074
|
return false;
|
6011
6075
|
}
|
6012
6076
|
|
6013
|
-
submitBlockToSolver(
|
6077
|
+
submitBlockToSolver() {
|
6014
6078
|
let top = MODEL.timeout_period;
|
6015
6079
|
if(VM.max_solver_time && top > VM.max_solver_time) {
|
6016
6080
|
top = VM.max_solver_time;
|
6017
6081
|
UI.notify('Solver time limit for this server is ' +
|
6018
6082
|
VM.max_solver_time + ' seconds');
|
6019
6083
|
}
|
6020
|
-
|
6084
|
+
UI.logHeapSize(`BEFORE creating post data`);
|
6085
|
+
const
|
6086
|
+
bwr = VM.blockWithRound,
|
6087
|
+
pd = postData({
|
6088
|
+
action: 'solve',
|
6089
|
+
user: VM.solver_user,
|
6090
|
+
token: VM.solver_token,
|
6091
|
+
block: VM.block_count,
|
6092
|
+
round: VM.round_sequence[VM.current_round],
|
6093
|
+
data: VM.lines,
|
6094
|
+
timeout: top
|
6095
|
+
});
|
6096
|
+
UI.logHeapSize(`AFTER creating post data`);
|
6097
|
+
// Immediately free the memory taken up by VM.lines
|
6098
|
+
VM.lines = '';
|
6021
6099
|
UI.logHeapSize(`BEFORE submitting block #${bwr} to solver`);
|
6022
|
-
fetch('solver/',
|
6023
|
-
action: 'solve',
|
6024
|
-
user: VM.solver_user,
|
6025
|
-
token: VM.solver_token,
|
6026
|
-
block: VM.block_count,
|
6027
|
-
round: VM.round_sequence[VM.current_round],
|
6028
|
-
data: bcode,
|
6029
|
-
timeout: top
|
6030
|
-
}))
|
6100
|
+
fetch('solver/', pd)
|
6031
6101
|
.then((response) => {
|
6032
6102
|
if(!response.ok) {
|
6033
6103
|
const msg = `ERROR ${response.status}: ${response.statusText}`;
|
@@ -6039,6 +6109,7 @@ class GUIMonitor {
|
|
6039
6109
|
.then((data) => {
|
6040
6110
|
try {
|
6041
6111
|
VM.processServerResponse(JSON.parse(data));
|
6112
|
+
UI.logHeapSize('After processing results for block #' + this.block_count);
|
6042
6113
|
// If no errors, solve next block (if any)
|
6043
6114
|
// NOTE: use setTimeout so that this calling function returns,
|
6044
6115
|
// and browser can update its DOM to display progress
|
@@ -6063,6 +6134,8 @@ class GUIMonitor {
|
|
6063
6134
|
UI.alert(msg);
|
6064
6135
|
VM.stopSolving();
|
6065
6136
|
});
|
6137
|
+
pd.body = '';
|
6138
|
+
UI.logHeapSize(`after calling FETCH and clearing POST data body`);
|
6066
6139
|
VM.logMessage(VM.block_count,
|
6067
6140
|
`POSTing block #${bwr} took ${VM.elapsedTime} seconds.`);
|
6068
6141
|
UI.logHeapSize(`AFTER posting block #${bwr} to solver`);
|
@@ -6466,20 +6539,24 @@ Attributes, however, are case sensitive!">[Actor X|CF]</code> for cash flow.
|
|
6466
6539
|
<code title="Number of rounds in the sequence">nr</code>,
|
6467
6540
|
<code title="Number of current experiment run (starts at 0)">x</code>,
|
6468
6541
|
<code title="Number of runs in the experiment">nx</code>,
|
6542
|
+
<span title="Index variables of iterator dimensions)">
|
6543
|
+
<code>i</code>, <code>j</code>, <code>k</code>,
|
6544
|
+
</span>
|
6469
6545
|
<code title="Number of time steps in 1 year)">yr</code>,
|
6470
6546
|
<code title="Number of time steps in 1 week)">wk</code>,
|
6471
6547
|
<code title="Number of time steps in 1 day)">d</code>,
|
6472
6548
|
<code title="Number of time steps in 1 hour)">h</code>,
|
6473
6549
|
<code title="Number of time steps in 1 minute)">m</code>,
|
6474
6550
|
<code title="Number of time steps in 1 second)">s</code>,
|
6475
|
-
<code title="A random number from the uniform distribution U(0, 1)">random</code>)
|
6476
|
-
|
6551
|
+
<code title="A random number from the uniform distribution U(0, 1)">random</code>),
|
6552
|
+
constants (<code title="Mathematical constant π = ${Math.PI}">pi</code>,
|
6477
6553
|
<code title="Logical constant true = 1
|
6478
6554
|
NOTE: any non-zero value evaluates as true">true</code>,
|
6479
6555
|
<code title="Logical constant false = 0">false</code>,
|
6480
6556
|
<code title="The value used for ‘unbounded’ variables (` +
|
6481
|
-
VM.PLUS_INFINITY.toExponential() + `)">infinity</code>)
|
6482
|
-
are <strong><em>not</em></strong> enclosed by brackets.
|
6557
|
+
VM.PLUS_INFINITY.toExponential() + `)">infinity</code>) and scale units
|
6558
|
+
are <strong><em>not</em></strong> enclosed by brackets. Scale units
|
6559
|
+
may be enclosed by single quotes.
|
6483
6560
|
</p>
|
6484
6561
|
<h4>Operators</h4>
|
6485
6562
|
<p><em>Monadic:</em>
|
@@ -6542,7 +6619,8 @@ considers X0, …, Xn as a variable cash flow time series.">npv</code><br>
|
|
6542
6619
|
<em>Grouping:</em>
|
6543
6620
|
<code title="X ; Y evaluates as a group or “tuple” (X, Y)
|
6544
6621
|
NOTE: Grouping groups results in a single group, e.g., (1;2);(3;4;5) evaluates as (1;2;3;4;5)">X ; Y</code>
|
6545
|
-
(use only in combination with <code>max</code>, <code>min</code
|
6622
|
+
(use only in combination with <code>max</code>, <code>min</code>, <code>npv</code>
|
6623
|
+
and probabilistic operators)<br>
|
6546
6624
|
</p>
|
6547
6625
|
<p>
|
6548
6626
|
Monadic operators take precedence over dyadic operators.
|
@@ -6574,7 +6652,7 @@ NOTE: Grouping groups results in a single group, e.g., (1;2);(3;4;5) evaluates a
|
|
6574
6652
|
UI.edited_object = UI.dbl_clicked_node;
|
6575
6653
|
this.edited_input_id = 'note-C';
|
6576
6654
|
if(UI.edited_object) {
|
6577
|
-
this.edited_expression = UI.edited_object.
|
6655
|
+
this.edited_expression = UI.edited_object.color;
|
6578
6656
|
} else {
|
6579
6657
|
this.edited_expression = null;
|
6580
6658
|
}
|
@@ -6702,7 +6780,7 @@ NOTE: Grouping groups results in a single group, e.g., (1;2);(3;4;5) evaluates a
|
|
6702
6780
|
// is passed to differentiate between the DOM elements to be used
|
6703
6781
|
const
|
6704
6782
|
type = document.getElementById(prefix + 'variable-obj').value,
|
6705
|
-
n_list = this.namesByType(VM.object_types[type]).sort(),
|
6783
|
+
n_list = this.namesByType(VM.object_types[type]).sort(ciCompare),
|
6706
6784
|
vn = document.getElementById(prefix + 'variable-name'),
|
6707
6785
|
options = [];
|
6708
6786
|
// Add "empty" as first and initial option, but disable it.
|
@@ -6744,7 +6822,7 @@ NOTE: Grouping groups results in a single group, e.g., (1;2);(3;4;5) evaluates a
|
|
6744
6822
|
slist.push(d.modifiers[m].selector);
|
6745
6823
|
}
|
6746
6824
|
// Sort to present equations in alphabetical order
|
6747
|
-
slist.sort();
|
6825
|
+
slist.sort(ciCompare);
|
6748
6826
|
for(let i = 0; i < slist.length; i++) {
|
6749
6827
|
options.push(`<option value="${slist[i]}">${slist[i]}</option>`);
|
6750
6828
|
}
|
@@ -7029,6 +7107,218 @@ class ModelAutoSaver {
|
|
7029
7107
|
} // END of class ModelAutoSaver
|
7030
7108
|
|
7031
7109
|
|
7110
|
+
// CLASS ScaleUnitManager (modal dialog!)
|
7111
|
+
class ScaleUnitManager {
|
7112
|
+
constructor() {
|
7113
|
+
// Add the scale units modal
|
7114
|
+
this.dialog = new ModalDialog('scale-units');
|
7115
|
+
this.dialog.close.addEventListener('click',
|
7116
|
+
() => SCALE_UNIT_MANAGER.dialog.hide());
|
7117
|
+
// Make the add, edit and delete buttons of this modal responsive
|
7118
|
+
this.dialog.element('new-btn').addEventListener('click',
|
7119
|
+
() => SCALE_UNIT_MANAGER.promptForScaleUnit());
|
7120
|
+
this.dialog.element('edit-btn').addEventListener('click',
|
7121
|
+
() => SCALE_UNIT_MANAGER.editScaleUnit());
|
7122
|
+
this.dialog.element('delete-btn').addEventListener('click',
|
7123
|
+
() => SCALE_UNIT_MANAGER.deleteScaleUnit());
|
7124
|
+
// Add the scale unit definition modal
|
7125
|
+
this.new_scale_unit_modal = new ModalDialog('new-scale-unit');
|
7126
|
+
this.new_scale_unit_modal.ok.addEventListener(
|
7127
|
+
'click', () => SCALE_UNIT_MANAGER.addNewScaleUnit());
|
7128
|
+
this.new_scale_unit_modal.cancel.addEventListener(
|
7129
|
+
'click', () => SCALE_UNIT_MANAGER.new_scale_unit_modal.hide());
|
7130
|
+
this.scroll_area = this.dialog.element('scroll-area');
|
7131
|
+
this.table = this.dialog.element('table');
|
7132
|
+
}
|
7133
|
+
|
7134
|
+
get selectedUnitIsBaseUnit() {
|
7135
|
+
// Returns TRUE iff selected unit is used as base unit for some unit
|
7136
|
+
for(let u in this.scale_units) if(this.scale_units.hasOwnProperty(u)) {
|
7137
|
+
if(this.scale_units[u].base_unit === this.selected_unit) return true;
|
7138
|
+
}
|
7139
|
+
return false;
|
7140
|
+
}
|
7141
|
+
|
7142
|
+
show() {
|
7143
|
+
// Show the user-defined scale units for the current model
|
7144
|
+
// NOTE: add/edit/delete actions operate on this list, so changes
|
7145
|
+
// take immediate effect
|
7146
|
+
MODEL.cleanUpScaleUnits();
|
7147
|
+
// NOTE: unit name is key in the scale units object
|
7148
|
+
this.selected_unit = '';
|
7149
|
+
this.last_time_selected = 0;
|
7150
|
+
this.updateDialog();
|
7151
|
+
this.dialog.show();
|
7152
|
+
}
|
7153
|
+
|
7154
|
+
updateDialog() {
|
7155
|
+
// Create the HTML for the scale units table and update the state
|
7156
|
+
// of the action buttons
|
7157
|
+
if(!MODEL.scale_units.hasOwnProperty(this.selected_unit)) {
|
7158
|
+
this.selected_unit = '';
|
7159
|
+
}
|
7160
|
+
const
|
7161
|
+
keys = Object.keys(MODEL.scale_units).sort(ciCompare),
|
7162
|
+
sl = [],
|
7163
|
+
ss = this.selected_unit;
|
7164
|
+
let ssid = 'scntr';
|
7165
|
+
if(keys.length <= 1) {
|
7166
|
+
// Only one key => must be the default '1'
|
7167
|
+
sl.push('<tr><td><em>No units defined</em></td></tr>');
|
7168
|
+
} else {
|
7169
|
+
for(let i = 1; i < keys.length; i++) {
|
7170
|
+
const
|
7171
|
+
s = keys[i],
|
7172
|
+
clk = '" onclick="SCALE_UNIT_MANAGER.selectScaleUnit(event, \'' +
|
7173
|
+
s + '\'';
|
7174
|
+
if(s === ss) ssid += i;
|
7175
|
+
sl.push(['<tr id="scntr', i, '" class="dataset-modif',
|
7176
|
+
(s === ss ? ' sel-set' : ''),
|
7177
|
+
'"><td class="dataset-selector', clk, ');">',
|
7178
|
+
s, '</td><td class="dataset-selector', clk, ', \'scalar\');">',
|
7179
|
+
MODEL.scale_units[s].scalar, '</td><td class="dataset-selector',
|
7180
|
+
clk, ', \'base\');">', MODEL.scale_units[s].base_unit,
|
7181
|
+
'</td></tr>'].join(''));
|
7182
|
+
}
|
7183
|
+
}
|
7184
|
+
this.table.innerHTML = sl.join('');
|
7185
|
+
if(ss) UI.scrollIntoView(document.getElementById(ssid));
|
7186
|
+
let btns = 'scale-units-edit';
|
7187
|
+
if(!this.selectedUnitIsBaseUnit) btns += ' scale-units-delete';
|
7188
|
+
if(ss) {
|
7189
|
+
UI.enableButtons(btns);
|
7190
|
+
} else {
|
7191
|
+
UI.disableButtons(btns);
|
7192
|
+
}
|
7193
|
+
}
|
7194
|
+
|
7195
|
+
selectScaleUnit(event, symbol, focus) {
|
7196
|
+
// Select scale unit, and when double-clicked, allow to edit it
|
7197
|
+
const
|
7198
|
+
ss = this.selected_unit,
|
7199
|
+
now = Date.now(),
|
7200
|
+
dt = now - this.last_time_selected,
|
7201
|
+
// NOTE: Alt-click and double-click indicate: edit
|
7202
|
+
// Consider click to be "double" if the same modifier was clicked
|
7203
|
+
// less than 300 ms ago
|
7204
|
+
edit = event.altKey || (symbol === ss && dt < 300);
|
7205
|
+
this.selected_unit = symbol;
|
7206
|
+
this.last_time_selected = now;
|
7207
|
+
if(edit) {
|
7208
|
+
this.last_time_selected = 0;
|
7209
|
+
this.promptForScaleUnit('Edit', focus);
|
7210
|
+
return;
|
7211
|
+
}
|
7212
|
+
this.updateDialog();
|
7213
|
+
}
|
7214
|
+
|
7215
|
+
promptForScaleUnit(action='Define new', focus='name') {
|
7216
|
+
// Show the Add/Edit scale unit dialog for the indicated action
|
7217
|
+
const md = this.new_scale_unit_modal;
|
7218
|
+
// NOTE: by default, let name and base unit be empty strings, not '1'
|
7219
|
+
let sv = {name: '', scalar: '1', base_unit: '' };
|
7220
|
+
if(action === 'Edit' && this.selected_unit) {
|
7221
|
+
sv = MODEL.scale_units[this.selected_unit];
|
7222
|
+
}
|
7223
|
+
md.element('action').innerText = action;
|
7224
|
+
md.element('name').value = sv.name;
|
7225
|
+
md.element('scalar').value = sv.scalar;
|
7226
|
+
md.element('base').value = sv.base_unit;
|
7227
|
+
UI.updateScaleUnitList();
|
7228
|
+
this.new_scale_unit_modal.show(focus);
|
7229
|
+
}
|
7230
|
+
|
7231
|
+
addNewScaleUnit() {
|
7232
|
+
// Add the new scale unit or update the one being edited
|
7233
|
+
const
|
7234
|
+
md = this.new_scale_unit_modal,
|
7235
|
+
edited = md.element('action').innerText === 'Edit',
|
7236
|
+
// NOTE: unit name cannot contain single quotes
|
7237
|
+
s = UI.cleanName(md.element('name').value).replace("'", ''),
|
7238
|
+
v = md.element('scalar').value.trim(),
|
7239
|
+
// NOTE: accept empty base unit to denote '1'
|
7240
|
+
b = md.element('base').value.trim() || '1';
|
7241
|
+
if(!s) {
|
7242
|
+
// Do not accept empty string as name
|
7243
|
+
UI.warn('Scale unit must have a name');
|
7244
|
+
md.element('name').focus();
|
7245
|
+
return;
|
7246
|
+
}
|
7247
|
+
if(MODEL.scale_units.hasOwnProperty(s) && !edited) {
|
7248
|
+
// Do not accept existing unit as name for new unit
|
7249
|
+
UI.warn(`Scale unit "${s}" is already defined`);
|
7250
|
+
md.element('name').focus();
|
7251
|
+
return;
|
7252
|
+
}
|
7253
|
+
if(b !== s && !MODEL.scale_units.hasOwnProperty(b)) {
|
7254
|
+
UI.warn(`Base unit "${b}" is undefined`);
|
7255
|
+
md.element('base').focus();
|
7256
|
+
return;
|
7257
|
+
}
|
7258
|
+
if(UI.validNumericInput('new-scale-unit-scalar', 'scalar')) {
|
7259
|
+
const ucs = Math.abs(safeStrToFloat(v));
|
7260
|
+
if(ucs < VM.NEAR_ZERO) {
|
7261
|
+
UI.warn(`Unit conversion scalar cannot be zero`);
|
7262
|
+
md.element('scalar').focus();
|
7263
|
+
return;
|
7264
|
+
}
|
7265
|
+
if(b === s && ucs !== 1) {
|
7266
|
+
UI.warn(`When base unit = scale unit, scalar must equal 1`);
|
7267
|
+
md.element('scalar').focus();
|
7268
|
+
return;
|
7269
|
+
}
|
7270
|
+
const selu = this.selected_unit;
|
7271
|
+
if(edited && b !== s) {
|
7272
|
+
// Prevent inconsistencies across scalars
|
7273
|
+
const cr = MODEL.scale_units[b].conversionRates();
|
7274
|
+
if(cr.hasOwnProperty(s)) {
|
7275
|
+
UI.warn(`Defining ${s} in terms of ${b} introduces a circular reference`);
|
7276
|
+
md.element('base').focus();
|
7277
|
+
return;
|
7278
|
+
}
|
7279
|
+
}
|
7280
|
+
if(edited && s !== selu) {
|
7281
|
+
// First rename base units
|
7282
|
+
for(let u in MODEL.scale_units) if(MODEL.scale_units.hasOwnProperty(u)) {
|
7283
|
+
if(MODEL.scale_units[u].base_unit === selu) {
|
7284
|
+
MODEL.scale_units[u].base_unit = s;
|
7285
|
+
}
|
7286
|
+
}
|
7287
|
+
// NOTE: renameScaleUnit replaces references to `s`, not the entry
|
7288
|
+
MODEL.renameScaleUnit(selu, s);
|
7289
|
+
delete MODEL.scale_units[this.selected_unit];
|
7290
|
+
}
|
7291
|
+
MODEL.scale_units[s] = new ScaleUnit(s, v, b);
|
7292
|
+
MODEL.selected_unit = s;
|
7293
|
+
this.new_scale_unit_modal.hide();
|
7294
|
+
UI.updateScaleUnitList();
|
7295
|
+
this.updateDialog();
|
7296
|
+
}
|
7297
|
+
}
|
7298
|
+
|
7299
|
+
editScaleUnit() {
|
7300
|
+
// Allow user to edit name and/or value
|
7301
|
+
if(this.selected_unit) this.promptForScaleUnit('Edit', 'scalar');
|
7302
|
+
}
|
7303
|
+
|
7304
|
+
deleteScaleUnit() {
|
7305
|
+
// Allow user to delete
|
7306
|
+
// @@@TO DO: check whether scale unit is used in the model
|
7307
|
+
if(this.selected_unit && !this.selectedUnitIsBaseUnit) {
|
7308
|
+
delete MODEL.scale_units[this.selected_unit];
|
7309
|
+
this.updateDialog();
|
7310
|
+
}
|
7311
|
+
}
|
7312
|
+
|
7313
|
+
updateScaleUnits() {
|
7314
|
+
// Replace scale unit definitions of model by the new definitions
|
7315
|
+
UI.updateScaleUnitList();
|
7316
|
+
this.dialog.hide();
|
7317
|
+
}
|
7318
|
+
|
7319
|
+
} // END of class ScaleUnitManager
|
7320
|
+
|
7321
|
+
|
7032
7322
|
// CLASS ActorManager (modal dialog!)
|
7033
7323
|
class ActorManager {
|
7034
7324
|
constructor() {
|
@@ -8833,21 +9123,26 @@ class GUIDatasetManager extends DatasetManager {
|
|
8833
9123
|
dnl.push(d);
|
8834
9124
|
}
|
8835
9125
|
}
|
8836
|
-
dnl.sort();
|
9126
|
+
dnl.sort(ciCompare);
|
8837
9127
|
let sdid = 'dstr';
|
8838
9128
|
for(let i = 0; i < dnl.length; i++) {
|
8839
9129
|
const d = MODEL.datasets[dnl[i]];
|
8840
9130
|
let cls = ioclass[MODEL.ioType(d)];
|
8841
9131
|
if(d.outcome) {
|
8842
|
-
cls
|
9132
|
+
cls += ' outcome';
|
8843
9133
|
} else if(d.array) {
|
8844
|
-
cls
|
9134
|
+
cls += ' array';
|
9135
|
+
} else if(d.data.length > 0) {
|
9136
|
+
cls += ' series';
|
8845
9137
|
}
|
8846
|
-
if(d.
|
9138
|
+
if(Object.keys(d.modifiers).length > 0) cls += ' modif';
|
9139
|
+
if(d.black_box) cls += ' blackbox';
|
9140
|
+
cls = cls.trim();
|
8847
9141
|
if(cls) cls = ' class="'+ cls + '"';
|
8848
9142
|
if(d === sd) sdid += i;
|
8849
9143
|
dl.push(['<tr id="dstr', i, '" class="dataset',
|
8850
9144
|
(d === sd ? ' sel-set' : ''),
|
9145
|
+
(d.default_selector ? ' def-sel' : ''),
|
8851
9146
|
'" onclick="DATASET_MANAGER.selectDataset(event, \'',
|
8852
9147
|
dnl[i], '\');" onmouseover="DATASET_MANAGER.showInfo(\'', dnl[i],
|
8853
9148
|
'\', event.shiftKey);"><td', cls, '>', d.displayName,
|
@@ -8859,7 +9154,8 @@ class GUIDatasetManager extends DatasetManager {
|
|
8859
9154
|
this.table.innerHTML = dl.join('');
|
8860
9155
|
this.properties.style.display = 'block';
|
8861
9156
|
document.getElementById('dataset-default').innerHTML =
|
8862
|
-
VM.sig4Dig(sd.default_value)
|
9157
|
+
VM.sig4Dig(sd.default_value) +
|
9158
|
+
(sd.scale_unit === '1' ? '' : ' ' + sd.scale_unit);
|
8863
9159
|
document.getElementById('dataset-count').innerHTML = sd.data.length;
|
8864
9160
|
document.getElementById('dataset-special').innerHTML = sd.propertiesString;
|
8865
9161
|
if(sd.data.length > 0) {
|
@@ -9012,11 +9308,16 @@ class GUIDatasetManager extends DatasetManager {
|
|
9012
9308
|
edit = event.altKey || (m === this.selected_modifier && dt < 300);
|
9013
9309
|
this.last_time_selected = now;
|
9014
9310
|
if(event.shiftKey) {
|
9311
|
+
// NOTE: prepare to update HTML class of selected dataset
|
9312
|
+
const el = document.getElementById('dataset-table')
|
9313
|
+
.getElementsByClassName('sel-set')[0];
|
9015
9314
|
// Toggle dataset default selector
|
9016
9315
|
if(m.selector === this.selected_dataset.default_selector) {
|
9017
9316
|
this.selected_dataset.default_selector = '';
|
9317
|
+
el.classList.remove('def-sel');
|
9018
9318
|
} else {
|
9019
9319
|
this.selected_dataset.default_selector = m.selector;
|
9320
|
+
el.classList.add('def-sel');
|
9020
9321
|
}
|
9021
9322
|
}
|
9022
9323
|
this.selected_modifier = m;
|
@@ -9094,6 +9395,7 @@ class GUIDatasetManager extends DatasetManager {
|
|
9094
9395
|
// Copy properties of d to nd
|
9095
9396
|
nd.comments = `${d.comments}`;
|
9096
9397
|
nd.default_value = d.default_value;
|
9398
|
+
nd.scale_unit = d.scale_unit;
|
9097
9399
|
nd.time_scale = d.time_scale;
|
9098
9400
|
nd.time_unit = d.time_unit;
|
9099
9401
|
nd.method = d.method;
|
@@ -9189,6 +9491,8 @@ class GUIDatasetManager extends DatasetManager {
|
|
9189
9491
|
const
|
9190
9492
|
hw = this.selected_modifier.hasWildcards,
|
9191
9493
|
sel = this.rename_selector_modal.element('name').value,
|
9494
|
+
// NOTE: normal dataset selector, so remove all invalid characters
|
9495
|
+
clean_sel = sel.replace(/[^a-zA-z0-9\%\+\-]/g, ''),
|
9192
9496
|
// Keep track of old name
|
9193
9497
|
oldm = this.selected_modifier,
|
9194
9498
|
// NOTE: addModifier returns existing one if selector not changed
|
@@ -9199,10 +9503,10 @@ class GUIDatasetManager extends DatasetManager {
|
|
9199
9503
|
if(oldm.selector === this.selected_dataset.default_selector) {
|
9200
9504
|
this.selected_dataset.default_selector = m.selector;
|
9201
9505
|
}
|
9506
|
+
MODEL.renameSelectorInExperiments(oldm.selector, clean_sel);
|
9202
9507
|
// If only case has changed, just update the selector
|
9203
|
-
// NOTE: normal dataset selector, so remove all invalid characters
|
9204
9508
|
if(m === oldm) {
|
9205
|
-
m.selector =
|
9509
|
+
m.selector = clean_sel;
|
9206
9510
|
this.updateDialog();
|
9207
9511
|
return;
|
9208
9512
|
}
|
@@ -9238,12 +9542,8 @@ class GUIDatasetManager extends DatasetManager {
|
|
9238
9542
|
if(msg.length) {
|
9239
9543
|
UI.notify('Updated ' + msg.join(' and '));
|
9240
9544
|
// Also update these stay-on-top dialogs, as they may display a
|
9241
|
-
// variable name for this dataset + modifier
|
9242
|
-
|
9243
|
-
DATASET_MANAGER.updateDialog();
|
9244
|
-
EQUATION_MANAGER.updateDialog();
|
9245
|
-
EXPERIMENT_MANAGER.updateDialog();
|
9246
|
-
FINDER.changeFilter();
|
9545
|
+
// variable name for this dataset + modifier
|
9546
|
+
UI.updateControllerDialogs('CDEFX');
|
9247
9547
|
}
|
9248
9548
|
// NOTE: update dimensions only if dataset now has 2 or more modifiers
|
9249
9549
|
// (ignoring those with wildcards)
|
@@ -9312,6 +9612,7 @@ class GUIDatasetManager extends DatasetManager {
|
|
9312
9612
|
cover = md.element('no-time-msg');
|
9313
9613
|
if(ds) {
|
9314
9614
|
md.element('default').value = ds.default_value;
|
9615
|
+
md.element('unit').value = ds.scale_unit;
|
9315
9616
|
cover.style.display = (ds.array ? 'block' : 'none');
|
9316
9617
|
md.element('time-scale').value = VM.sig4Dig(ds.time_scale);
|
9317
9618
|
// Add options for time unit selector
|
@@ -9376,6 +9677,7 @@ class GUIDatasetManager extends DatasetManager {
|
|
9376
9677
|
}
|
9377
9678
|
// Save the data
|
9378
9679
|
ds.default_value = dv;
|
9680
|
+
ds.changeScaleUnit(this.series_modal.element('unit').value);
|
9379
9681
|
ds.time_scale = ts;
|
9380
9682
|
ds.time_unit = this.series_modal.element('time-unit').value;
|
9381
9683
|
ds.method = this.series_modal.element('method').value;
|
@@ -9455,7 +9757,6 @@ class EquationManager {
|
|
9455
9757
|
for(let i = 0; i < msl.length; i++) {
|
9456
9758
|
const
|
9457
9759
|
m = ed.modifiers[UI.nameToID(msl[i])],
|
9458
|
-
mp = (m.parameters ? '\\' + m.parameters.join('\\') : ''),
|
9459
9760
|
clk = '" onclick="EQUATION_MANAGER.selectModifier(event, \'' +
|
9460
9761
|
m.selector + '\'';
|
9461
9762
|
if(m === sm) smid += i;
|
@@ -9464,7 +9765,7 @@ class EquationManager {
|
|
9464
9765
|
'"><td class="equation-selector',
|
9465
9766
|
(m.expression.isStatic ? '' : ' it'),
|
9466
9767
|
clk, ', false);">',
|
9467
|
-
m.selector,
|
9768
|
+
m.selector, '</td><td class="equation-expression',
|
9468
9769
|
clk, ');">', m.expression.text, '</td></tr>'].join(''));
|
9469
9770
|
}
|
9470
9771
|
this.table.innerHTML = ml.join('');
|
@@ -9590,7 +9891,6 @@ class EquationManager {
|
|
9590
9891
|
} else {
|
9591
9892
|
// When a new modifier has been added, more actions are needed
|
9592
9893
|
m.expression = oldm.expression;
|
9593
|
-
m.parameters = oldm.parameters;
|
9594
9894
|
this.deleteEquation();
|
9595
9895
|
this.selected_modifier = m;
|
9596
9896
|
}
|
@@ -9618,11 +9918,7 @@ class EquationManager {
|
|
9618
9918
|
UI.notify('Updated ' + msg.join(' and '));
|
9619
9919
|
// Also update these stay-on-top dialogs, as they may display a
|
9620
9920
|
// variable name for this dataset + modifier
|
9621
|
-
|
9622
|
-
DATASET_MANAGER.updateDialog();
|
9623
|
-
EQUATION_MANAGER.updateDialog();
|
9624
|
-
EXPERIMENT_MANAGER.updateDialog();
|
9625
|
-
FINDER.changeFilter();
|
9921
|
+
UI.updateControllerDialogs('CDEFX');
|
9626
9922
|
}
|
9627
9923
|
// Always close the name prompt dialog, and update the equation manager
|
9628
9924
|
this.rename_modal.hide();
|
@@ -9925,6 +10221,11 @@ class GUIChartManager extends ChartManager {
|
|
9925
10221
|
u_btn = 'chart-variable-up ',
|
9926
10222
|
d_btn = 'chart-variable-down ',
|
9927
10223
|
ed_btns = 'chart-edit-variable chart-delete-variable ';
|
10224
|
+
// Just in case variable index has not been adjusted after some
|
10225
|
+
// variables have been deleted
|
10226
|
+
if(this.variable_index >= c.variables.length) {
|
10227
|
+
this.variable_index = -1;
|
10228
|
+
}
|
9928
10229
|
if(this.variable_index < 0) {
|
9929
10230
|
UI.disableButtons(ed_btns + u_btn + d_btn);
|
9930
10231
|
} else {
|
@@ -9942,7 +10243,7 @@ class GUIChartManager extends ChartManager {
|
|
9942
10243
|
// If the Edit variable dialog is showing, update its header
|
9943
10244
|
if(this.variable_index >= 0 && !UI.hidden('variable-dlg')) {
|
9944
10245
|
document.getElementById('variable-dlg-name').innerHTML =
|
9945
|
-
|
10246
|
+
c.variables[this.variable_index].displayName;
|
9946
10247
|
}
|
9947
10248
|
}
|
9948
10249
|
this.add_variable_modal.element('obj').value = 0;
|
@@ -10047,8 +10348,7 @@ class GUIChartManager extends ChartManager {
|
|
10047
10348
|
if(c.show_title) this.drawChart();
|
10048
10349
|
}
|
10049
10350
|
// Update experiment viewer in case its current experiment uses this chart
|
10050
|
-
|
10051
|
-
FINDER.changeFilter();
|
10351
|
+
UI.updateControllerDialogs('CFX');
|
10052
10352
|
}
|
10053
10353
|
this.rename_chart_modal.hide();
|
10054
10354
|
}
|
@@ -10098,10 +10398,8 @@ class GUIChartManager extends ChartManager {
|
|
10098
10398
|
MODEL.charts.splice(this.chart_index, 1);
|
10099
10399
|
this.chart_index = -1;
|
10100
10400
|
}
|
10101
|
-
this.updateDialog();
|
10102
10401
|
// Also update the experiment viewer (charts define the output variables)
|
10103
|
-
|
10104
|
-
FINDER.changeFilter();
|
10402
|
+
UI.updateControllerDialogs('CFX');
|
10105
10403
|
}
|
10106
10404
|
}
|
10107
10405
|
|
@@ -10176,12 +10474,11 @@ class GUIChartManager extends ChartManager {
|
|
10176
10474
|
this.variable_index = MODEL.charts[this.chart_index].addVariable(o, a);
|
10177
10475
|
if(this.variable_index >= 0) {
|
10178
10476
|
this.add_variable_modal.hide();
|
10179
|
-
this.updateDialog();
|
10180
10477
|
// Also update the experiment viewer (charts define the output variables)
|
10181
10478
|
if(EXPERIMENT_MANAGER.selected_experiment) {
|
10182
10479
|
EXPERIMENT_MANAGER.selected_experiment.inferVariables();
|
10183
|
-
EXPERIMENT_MANAGER.updateDialog();
|
10184
10480
|
}
|
10481
|
+
UI.updateControllerDialogs('CFX');
|
10185
10482
|
}
|
10186
10483
|
}
|
10187
10484
|
}
|
@@ -10317,10 +10614,7 @@ class GUIChartManager extends ChartManager {
|
|
10317
10614
|
this.updateDialog();
|
10318
10615
|
// Also update the experiment viewer (charts define the output variables)
|
10319
10616
|
// and finder dialog
|
10320
|
-
if(EXPERIMENT_MANAGER.selected_experiment)
|
10321
|
-
EXPERIMENT_MANAGER.updateDialog();
|
10322
|
-
FINDER.changeFilter();
|
10323
|
-
}
|
10617
|
+
if(EXPERIMENT_MANAGER.selected_experiment) UI.updateControllerDialogs('FX');
|
10324
10618
|
}
|
10325
10619
|
this.variable_modal.hide();
|
10326
10620
|
}
|
@@ -10513,7 +10807,7 @@ class GUISensitivityAnalysis extends SensitivityAnalysis {
|
|
10513
10807
|
this.base_selectors.addEventListener(
|
10514
10808
|
'blur', () => SENSITIVITY_ANALYSIS.setBaseSelectors());
|
10515
10809
|
|
10516
|
-
this.delta = document.getElementById('
|
10810
|
+
this.delta = document.getElementById('sensitivity-delta');
|
10517
10811
|
this.delta.addEventListener(
|
10518
10812
|
'focus', () => SENSITIVITY_ANALYSIS.editDelta());
|
10519
10813
|
this.delta.addEventListener(
|
@@ -10610,6 +10904,8 @@ class GUISensitivityAnalysis extends SensitivityAnalysis {
|
|
10610
10904
|
}
|
10611
10905
|
|
10612
10906
|
updateControlPanel() {
|
10907
|
+
// Shows the control panel, or when the analysis is running the
|
10908
|
+
// legend to the outcomes (also to prevent changes to parameters)
|
10613
10909
|
this.base_selectors.value = MODEL.base_case_selectors;
|
10614
10910
|
this.delta.value = VM.sig4Dig(MODEL.sensitivity_delta);
|
10615
10911
|
const tr = [];
|
@@ -10708,12 +11004,12 @@ class GUISensitivityAnalysis extends SensitivityAnalysis {
|
|
10708
11004
|
this.showBaseCaseInfo();
|
10709
11005
|
return;
|
10710
11006
|
}
|
10711
|
-
// Otherwise, display list of all
|
11007
|
+
// Otherwise, display list of all dataset selectors in docu-viewer
|
10712
11008
|
if(DOCUMENTATION_MANAGER.visible) {
|
10713
11009
|
const
|
10714
11010
|
ds_dict = MODEL.listOfAllSelectors,
|
10715
11011
|
html = [],
|
10716
|
-
sl = Object.keys(ds_dict).sort();
|
11012
|
+
sl = Object.keys(ds_dict).sort(ciCompare);
|
10717
11013
|
for(let i = 0; i < sl.length; i++) {
|
10718
11014
|
const
|
10719
11015
|
s = sl[i],
|
@@ -10900,6 +11196,8 @@ class GUISensitivityAnalysis extends SensitivityAnalysis {
|
|
10900
11196
|
// NOTE: clusters have no suitable attributes, and equations are endogenous
|
10901
11197
|
md.element('cluster').style.display = 'none';
|
10902
11198
|
md.element('equation').style.display = 'none';
|
11199
|
+
// NOTE: update to ensure that valid attributes are selectable
|
11200
|
+
X_EDIT.updateVariableBar('add-sa-');
|
10903
11201
|
md.show();
|
10904
11202
|
}
|
10905
11203
|
|
@@ -10909,6 +11207,8 @@ class GUISensitivityAnalysis extends SensitivityAnalysis {
|
|
10909
11207
|
md.element('type').innerText = 'outcome';
|
10910
11208
|
md.element('cluster').style.display = 'block';
|
10911
11209
|
md.element('equation').style.display = 'block';
|
11210
|
+
// NOTE: update to ensure that valid attributes are selectable
|
11211
|
+
X_EDIT.updateVariableBar('add-sa-');
|
10912
11212
|
md.show();
|
10913
11213
|
}
|
10914
11214
|
|
@@ -10977,9 +11277,14 @@ class GUISensitivityAnalysis extends SensitivityAnalysis {
|
|
10977
11277
|
a = md.selectedOption('attr').text;
|
10978
11278
|
let n = '';
|
10979
11279
|
if(e === 'Equation' && a) {
|
11280
|
+
// For equations, the attribute denotes the name
|
10980
11281
|
n = a;
|
10981
11282
|
} else if(o && a) {
|
11283
|
+
// Most variables are defined by name + attribute ...
|
10982
11284
|
n = o + UI.OA_SEPARATOR + a;
|
11285
|
+
} else if(e === 'Dataset' && o) {
|
11286
|
+
// ... but for datasets the selector is optional
|
11287
|
+
n = o;
|
10983
11288
|
}
|
10984
11289
|
if(n) {
|
10985
11290
|
if(t === 'parameter' && MODEL.sensitivity_parameters.indexOf(n) < 0) {
|
@@ -11021,6 +11326,7 @@ class GUISensitivityAnalysis extends SensitivityAnalysis {
|
|
11021
11326
|
this.start_btn.classList.add('off');
|
11022
11327
|
this.pause_btn.classList.remove('off');
|
11023
11328
|
this.stop_btn.classList.add('off');
|
11329
|
+
this.must_pause = false;
|
11024
11330
|
return paused;
|
11025
11331
|
}
|
11026
11332
|
|
@@ -11029,6 +11335,7 @@ class GUISensitivityAnalysis extends SensitivityAnalysis {
|
|
11029
11335
|
this.pause_btn.classList.add('off');
|
11030
11336
|
this.stop_btn.classList.add('off');
|
11031
11337
|
this.start_btn.classList.remove('off', 'blink');
|
11338
|
+
this.must_pause = false;
|
11032
11339
|
}
|
11033
11340
|
|
11034
11341
|
pausedButtons(aci) {
|
@@ -11047,6 +11354,8 @@ class GUISensitivityAnalysis extends SensitivityAnalysis {
|
|
11047
11354
|
this.readyButtons();
|
11048
11355
|
this.reset_btn.classList.add('off');
|
11049
11356
|
this.selected_run = -1;
|
11357
|
+
this.must_pause = false;
|
11358
|
+
this.progress.innerHTML = '';
|
11050
11359
|
this.updateDialog();
|
11051
11360
|
}
|
11052
11361
|
|
@@ -11116,7 +11425,7 @@ class GUISensitivityAnalysis extends SensitivityAnalysis {
|
|
11116
11425
|
}
|
11117
11426
|
this.table.innerHTML = html.join('');
|
11118
11427
|
if(this.selected_run >= 0) document.getElementById(
|
11119
|
-
`sa-r${this.selected_run}c0`).
|
11428
|
+
`sa-r${this.selected_run}c0`).parentNode.classList.add('sa-p-sel');
|
11120
11429
|
this.updateData();
|
11121
11430
|
}
|
11122
11431
|
|
@@ -11179,7 +11488,7 @@ class GUISensitivityAnalysis extends SensitivityAnalysis {
|
|
11179
11488
|
} else if(n < MODEL.sensitivity_runs.length) {
|
11180
11489
|
this.selected_run = n;
|
11181
11490
|
if(n >= 0) document.getElementById(
|
11182
|
-
`sa-r${n}c0`).
|
11491
|
+
`sa-r${n}c0`).parentNode.classList.add('sa-p-sel');
|
11183
11492
|
}
|
11184
11493
|
VM.setRunMessages(this.selected_run);
|
11185
11494
|
}
|
@@ -11279,6 +11588,10 @@ class GUIExperimentManager extends ExperimentManager {
|
|
11279
11588
|
'click', () => EXPERIMENT_MANAGER.moveDimension(1));
|
11280
11589
|
document.getElementById('xp-d-settings-btn').addEventListener(
|
11281
11590
|
'click', () => EXPERIMENT_MANAGER.editSettingsDimensions());
|
11591
|
+
document.getElementById('xp-d-iterator-btn').addEventListener(
|
11592
|
+
'click', () => EXPERIMENT_MANAGER.editIteratorRanges());
|
11593
|
+
document.getElementById('xp-d-combination-btn').addEventListener(
|
11594
|
+
'click', () => EXPERIMENT_MANAGER.editCombinationDimensions());
|
11282
11595
|
document.getElementById('xp-d-actor-btn').addEventListener(
|
11283
11596
|
'click', () => EXPERIMENT_MANAGER.editActorDimension());
|
11284
11597
|
document.getElementById('xp-d-delete-btn').addEventListener(
|
@@ -11339,6 +11652,12 @@ class GUIExperimentManager extends ExperimentManager {
|
|
11339
11652
|
this.parameter_modal.cancel.addEventListener(
|
11340
11653
|
'click', () => EXPERIMENT_MANAGER.parameter_modal.hide());
|
11341
11654
|
|
11655
|
+
this.iterator_modal = new ModalDialog('xp-iterator');
|
11656
|
+
this.iterator_modal.ok.addEventListener(
|
11657
|
+
'click', () => EXPERIMENT_MANAGER.modifyIteratorRanges());
|
11658
|
+
this.iterator_modal.cancel.addEventListener(
|
11659
|
+
'click', () => EXPERIMENT_MANAGER.iterator_modal.hide());
|
11660
|
+
|
11342
11661
|
this.settings_modal = new ModalDialog('xp-settings');
|
11343
11662
|
this.settings_modal.close.addEventListener(
|
11344
11663
|
'click', () => EXPERIMENT_MANAGER.closeSettingsDimensions());
|
@@ -11359,6 +11678,26 @@ class GUIExperimentManager extends ExperimentManager {
|
|
11359
11678
|
this.settings_dimension_modal.cancel.addEventListener(
|
11360
11679
|
'click', () => EXPERIMENT_MANAGER.settings_dimension_modal.hide());
|
11361
11680
|
|
11681
|
+
this.combination_modal = new ModalDialog('xp-combination');
|
11682
|
+
this.combination_modal.close.addEventListener(
|
11683
|
+
'click', () => EXPERIMENT_MANAGER.closeCombinationDimensions());
|
11684
|
+
this.combination_modal.element('s-add-btn').addEventListener(
|
11685
|
+
'click', () => EXPERIMENT_MANAGER.editCombinationSelector(-1));
|
11686
|
+
this.combination_modal.element('d-add-btn').addEventListener(
|
11687
|
+
'click', () => EXPERIMENT_MANAGER.editCombinationDimension(-1));
|
11688
|
+
|
11689
|
+
this.combination_selector_modal = new ModalDialog('xp-combination-selector');
|
11690
|
+
this.combination_selector_modal.ok.addEventListener(
|
11691
|
+
'click', () => EXPERIMENT_MANAGER.modifyCombinationSelector());
|
11692
|
+
this.combination_selector_modal.cancel.addEventListener(
|
11693
|
+
'click', () => EXPERIMENT_MANAGER.combination_selector_modal.hide());
|
11694
|
+
|
11695
|
+
this.combination_dimension_modal = new ModalDialog('xp-combination-dimension');
|
11696
|
+
this.combination_dimension_modal.ok.addEventListener(
|
11697
|
+
'click', () => EXPERIMENT_MANAGER.modifyCombinationDimension());
|
11698
|
+
this.combination_dimension_modal.cancel.addEventListener(
|
11699
|
+
'click', () => EXPERIMENT_MANAGER.combination_dimension_modal.hide());
|
11700
|
+
|
11362
11701
|
this.actor_dimension_modal = new ModalDialog('xp-actor-dimension');
|
11363
11702
|
this.actor_dimension_modal.close.addEventListener(
|
11364
11703
|
'click', () => EXPERIMENT_MANAGER.closeActorDimension());
|
@@ -11408,6 +11747,7 @@ class GUIExperimentManager extends ExperimentManager {
|
|
11408
11747
|
this.selected_parameter = '';
|
11409
11748
|
this.edited_selector_index = -1;
|
11410
11749
|
this.edited_dimension_index = -1;
|
11750
|
+
this.edited_combi_selector_index = -1;
|
11411
11751
|
this.color_scale = new ColorScale('no');
|
11412
11752
|
this.designMode();
|
11413
11753
|
}
|
@@ -11433,7 +11773,7 @@ class GUIExperimentManager extends ExperimentManager {
|
|
11433
11773
|
for(let i = 0; i < MODEL.experiments.length; i++) {
|
11434
11774
|
xtl.push(MODEL.experiments[i].title);
|
11435
11775
|
}
|
11436
|
-
xtl.sort();
|
11776
|
+
xtl.sort(ciCompare);
|
11437
11777
|
for(let i = 0; i < xtl.length; i++) {
|
11438
11778
|
const
|
11439
11779
|
xi = MODEL.indexOfExperiment(xtl[i]),
|
@@ -11475,24 +11815,25 @@ class GUIExperimentManager extends ExperimentManager {
|
|
11475
11815
|
|
11476
11816
|
updateParameters() {
|
11477
11817
|
MODEL.inferDimensions();
|
11478
|
-
let
|
11479
|
-
canview = true;
|
11818
|
+
let canview = true;
|
11480
11819
|
const
|
11481
11820
|
dim_count = document.getElementById('experiment-dim-count'),
|
11482
11821
|
combi_count = document.getElementById('experiment-combi-count'),
|
11483
11822
|
header = document.getElementById('experiment-params-header'),
|
11484
11823
|
x = this.selected_experiment;
|
11485
11824
|
if(!x) {
|
11486
|
-
dim_count.innerHTML = pluralS(
|
11825
|
+
dim_count.innerHTML = pluralS(
|
11826
|
+
MODEL.dimensions.length, ' data dimension') + ' in model';
|
11487
11827
|
combi_count.innerHTML = '';
|
11488
11828
|
header.innerHTML = '(no experiment selected)';
|
11489
11829
|
this.params_div.style.display = 'none';
|
11490
11830
|
return;
|
11491
11831
|
}
|
11492
11832
|
x.updateActorDimension();
|
11493
|
-
|
11494
|
-
|
11495
|
-
dim_count.innerHTML = pluralS(
|
11833
|
+
x.updateIteratorDimensions();
|
11834
|
+
x.inferAvailableDimensions();
|
11835
|
+
dim_count.innerHTML = pluralS(x.available_dimensions.length,
|
11836
|
+
'more dimension');
|
11496
11837
|
x.inferActualDimensions();
|
11497
11838
|
x.inferCombinations();
|
11498
11839
|
combi_count.innerHTML = pluralS(x.combinations.length, 'combination');
|
@@ -11510,11 +11851,10 @@ class GUIExperimentManager extends ExperimentManager {
|
|
11510
11851
|
}
|
11511
11852
|
document.getElementById('experiment-dim-table').innerHTML = tr.join('');
|
11512
11853
|
// Add button must be enabled only if there still are unused dimensions
|
11513
|
-
if(x.
|
11514
|
-
x.settings_dimensions.length + x.actor_dimensions.length) {
|
11515
|
-
document.getElementById('xp-d-add-btn').classList.add('v-disab');
|
11516
|
-
} else {
|
11854
|
+
if(x.available_dimensions.length > 0) {
|
11517
11855
|
document.getElementById('xp-d-add-btn').classList.remove('v-disab');
|
11856
|
+
} else {
|
11857
|
+
document.getElementById('xp-d-add-btn').classList.add('v-disab');
|
11518
11858
|
}
|
11519
11859
|
this.updateUpDownButtons();
|
11520
11860
|
tr.length = 0;
|
@@ -11640,7 +11980,7 @@ class GUIExperimentManager extends ExperimentManager {
|
|
11640
11980
|
for(let i = 0; i < x.variables.length; i++) {
|
11641
11981
|
vl.push(x.variables[i].displayName);
|
11642
11982
|
}
|
11643
|
-
vl.sort();
|
11983
|
+
vl.sort(ciCompare);
|
11644
11984
|
for(let i = 0; i < vl.length; i++) {
|
11645
11985
|
ol.push(['<option value="', vl[i], '"',
|
11646
11986
|
(vl[i] == x.selected_variable ? ' selected="selected"' : ''),
|
@@ -11802,6 +12142,32 @@ class GUIExperimentManager extends ExperimentManager {
|
|
11802
12142
|
}
|
11803
12143
|
}
|
11804
12144
|
|
12145
|
+
toggleChartRow(r, n, shift) {
|
12146
|
+
// Toggle `n` consecutive rows, starting at row `r` (0 = top), to be
|
12147
|
+
// (no longer) part of the chart combination set
|
12148
|
+
const
|
12149
|
+
x = this.selected_experiment,
|
12150
|
+
// Let `n` be the number of the first run on row `r`
|
12151
|
+
nconf = r * this.nr_of_configurations;
|
12152
|
+
if(x && r < x.combinations.length / this.nr_of_configurations) {
|
12153
|
+
// NOTE: first cell of row determines ADD or REMOVE
|
12154
|
+
const add = x.chart_combinations.indexOf(n) < 0;
|
12155
|
+
for(let i = 0; i < this.nr_of_configurations; i++) {
|
12156
|
+
const ic = x.chart_combinations.indexOf(i);
|
12157
|
+
if(add) {
|
12158
|
+
if(ic < 0) x.chart_combinations.push(nconf + i);
|
12159
|
+
} else {
|
12160
|
+
if(!add) x.chart_combinations.splice(nconf + i, 1);
|
12161
|
+
}
|
12162
|
+
}
|
12163
|
+
this.updateData();
|
12164
|
+
}
|
12165
|
+
}
|
12166
|
+
|
12167
|
+
toggleChartColumn(c, shift) {
|
12168
|
+
// Toggle column `c` (0 = leftmost) to be part of the chart combination set
|
12169
|
+
}
|
12170
|
+
|
11805
12171
|
toggleChartCombi(n, shift, alt) {
|
11806
12172
|
// Set `n` to be the chart combination, or toggle if Shift-key is pressed,
|
11807
12173
|
// or execute single run if Alt-key is pressed
|
@@ -12209,6 +12575,61 @@ N = ${rr.N}, vector length = ${rr.vector.length}` : '')].join('');
|
|
12209
12575
|
}
|
12210
12576
|
}
|
12211
12577
|
|
12578
|
+
editIteratorRanges() {
|
12579
|
+
// Open dialog for editing iterator ranges
|
12580
|
+
const
|
12581
|
+
x = this.selected_experiment,
|
12582
|
+
md = this.iterator_modal,
|
12583
|
+
il = ['i', 'j', 'k'];
|
12584
|
+
if(x) {
|
12585
|
+
// NOTE: there are always 3 iterators (i, j k) so these have fixed
|
12586
|
+
// FROM and TO input fields in the dialog
|
12587
|
+
for(let i = 0; i < 3; i++) {
|
12588
|
+
const k = il[i];
|
12589
|
+
md.element(k + '-from').value = x.iterator_ranges[i][0];
|
12590
|
+
md.element(k + '-to').value = x.iterator_ranges[i][1];
|
12591
|
+
}
|
12592
|
+
this.iterator_modal.show();
|
12593
|
+
}
|
12594
|
+
}
|
12595
|
+
|
12596
|
+
modifyIteratorRanges() {
|
12597
|
+
const
|
12598
|
+
x = this.selected_experiment,
|
12599
|
+
md = this.iterator_modal;
|
12600
|
+
if(x) {
|
12601
|
+
// First validate all input fields (must be integer values)
|
12602
|
+
// NOTE: test using a copy so as not to overwrite values until OK
|
12603
|
+
const
|
12604
|
+
il = ['i', 'j', 'k'],
|
12605
|
+
ir = [[0, 0], [0, 0], [0, 0]],
|
12606
|
+
re = /^[\+\-]?[0-9]+$/;
|
12607
|
+
let el, f, t;
|
12608
|
+
for(let i = 0; i < 3; i++) {
|
12609
|
+
const k = il[i];
|
12610
|
+
el = md.element(k + '-from');
|
12611
|
+
f = el.value.trim() || '0';
|
12612
|
+
if(f === '' || re.test(f)) {
|
12613
|
+
el = md.element(k + '-to');
|
12614
|
+
t = el.value.trim() || '0';
|
12615
|
+
if(t === '' || re.test(t)) el = null;
|
12616
|
+
}
|
12617
|
+
// NULL value signals that field inputs are valid
|
12618
|
+
if(el === null) {
|
12619
|
+
ir[i] = [f, t];
|
12620
|
+
} else {
|
12621
|
+
el.focus();
|
12622
|
+
UI.warn('Iterator range limits must be integers (or default to 0)');
|
12623
|
+
return;
|
12624
|
+
}
|
12625
|
+
}
|
12626
|
+
// Input validated, so modify the iterator dimensions
|
12627
|
+
x.iterator_ranges = ir;
|
12628
|
+
this.updateDialog();
|
12629
|
+
}
|
12630
|
+
md.hide();
|
12631
|
+
}
|
12632
|
+
|
12212
12633
|
editSettingsDimensions() {
|
12213
12634
|
// Open dialog for editing model settings dimensions
|
12214
12635
|
const x = this.selected_experiment, rows = [];
|
@@ -12258,7 +12679,7 @@ N = ${rr.N}, vector length = ${rr.vector.length}` : '')].join('');
|
|
12258
12679
|
md.element('clear').innerHTML = clear;
|
12259
12680
|
md.element('code').value = sel[0];
|
12260
12681
|
md.element('string').value = sel[1];
|
12261
|
-
md.show('string');
|
12682
|
+
md.show(sel[0] ? 'string' : 'code');
|
12262
12683
|
}
|
12263
12684
|
|
12264
12685
|
modifySettingsSelector() {
|
@@ -12309,10 +12730,7 @@ N = ${rr.N}, vector length = ${rr.vector.length}` : '')].join('');
|
|
12309
12730
|
// NOTE: rename occurrence of code in dimension (should at most be 1)
|
12310
12731
|
const oc = x.settings_selectors[this.edited_selector_index].split('|')[0];
|
12311
12732
|
x.settings_selectors[this.edited_selector_index] = sel;
|
12312
|
-
|
12313
|
-
const si = x.settings_dimensions[i].indexOf(oc);
|
12314
|
-
if(si >= 0) x.settings_dimensions[i][si] = code;
|
12315
|
-
}
|
12733
|
+
x.renameSelectorInDimensions(oc, code);
|
12316
12734
|
}
|
12317
12735
|
}
|
12318
12736
|
md.hide();
|
@@ -12391,6 +12809,190 @@ N = ${rr.N}, vector length = ${rr.vector.length}` : '')].join('');
|
|
12391
12809
|
this.editSettingsDimensions();
|
12392
12810
|
}
|
12393
12811
|
|
12812
|
+
editCombinationDimensions() {
|
12813
|
+
// Open dialog for editing combination dimensions
|
12814
|
+
const
|
12815
|
+
x = this.selected_experiment,
|
12816
|
+
rows = [];
|
12817
|
+
if(x) {
|
12818
|
+
// Initialize selector list
|
12819
|
+
for(let i = 0; i < x.combination_selectors.length; i++) {
|
12820
|
+
const sel = x.combination_selectors[i].split('|');
|
12821
|
+
rows.push('<tr onclick="EXPERIMENT_MANAGER.editCombinationSelector(', i,
|
12822
|
+
');"><td width="25%">', sel[0], '</td><td>', sel[1], '</td></tr>');
|
12823
|
+
}
|
12824
|
+
this.combination_modal.element('s-table').innerHTML = rows.join('');
|
12825
|
+
// Initialize combination list
|
12826
|
+
rows.length = 0;
|
12827
|
+
for(let i = 0; i < x.combination_dimensions.length; i++) {
|
12828
|
+
const dim = x.combination_dimensions[i];
|
12829
|
+
rows.push('<tr onclick="EXPERIMENT_MANAGER.editCombinationDimension(', i,
|
12830
|
+
');"><td>', setString(dim), '</td></tr>');
|
12831
|
+
}
|
12832
|
+
this.combination_modal.element('d-table').innerHTML = rows.join('');
|
12833
|
+
this.combination_modal.show();
|
12834
|
+
// NOTE: clear infoline because dialog can generate warnings that would
|
12835
|
+
// otherwise remain visible while no longer relevant
|
12836
|
+
UI.setMessage('');
|
12837
|
+
}
|
12838
|
+
}
|
12839
|
+
|
12840
|
+
closeCombinationDimensions() {
|
12841
|
+
// Hide editor, and then update the experiment manager to reflect changes
|
12842
|
+
this.combination_modal.hide();
|
12843
|
+
this.updateDialog();
|
12844
|
+
}
|
12845
|
+
|
12846
|
+
editCombinationSelector(selnr) {
|
12847
|
+
const x = this.selected_experiment;
|
12848
|
+
if(!x) return;
|
12849
|
+
let action = 'Add',
|
12850
|
+
clear = '',
|
12851
|
+
sel = ['', ''];
|
12852
|
+
this.edited_combi_selector_index = selnr;
|
12853
|
+
if(selnr >= 0) {
|
12854
|
+
action = 'Edit';
|
12855
|
+
clear = '(clear to remove)';
|
12856
|
+
sel = x.combination_selectors[selnr].split('|');
|
12857
|
+
}
|
12858
|
+
const md = this.combination_selector_modal;
|
12859
|
+
md.element('action').innerHTML = action;
|
12860
|
+
md.element('clear').innerHTML = clear;
|
12861
|
+
md.element('code').value = sel[0];
|
12862
|
+
md.element('string').value = sel[1];
|
12863
|
+
md.show(sel[0] ? 'string' : 'code');
|
12864
|
+
}
|
12865
|
+
|
12866
|
+
modifyCombinationSelector() {
|
12867
|
+
// Accepts an "orthogonal" set of selectors
|
12868
|
+
let x = this.selected_experiment;
|
12869
|
+
if(x) {
|
12870
|
+
const
|
12871
|
+
md = this.combination_selector_modal,
|
12872
|
+
sc = md.element('code'),
|
12873
|
+
ss = md.element('string'),
|
12874
|
+
// Ignore invalid characters in the combination selector
|
12875
|
+
code = sc.value.replace(/[^\w\+\-\%]/g, ''),
|
12876
|
+
// Reduce comma's, semicolons and multiple spaces in the
|
12877
|
+
// combination string to a single space
|
12878
|
+
value = ss.value.trim().replace(/[\,\;\s]+/g, ' '),
|
12879
|
+
add = this.edited_combi_selector_index < 0;
|
12880
|
+
// Remove selector if either field has been cleared
|
12881
|
+
if(code.length === 0 || value.length === 0) {
|
12882
|
+
if(!add) {
|
12883
|
+
x.combination_selectors.splice(this.edited_combi_selector_index, 1);
|
12884
|
+
}
|
12885
|
+
} else {
|
12886
|
+
let ok = x.allDimensionSelectors.indexOf(code) < 0;
|
12887
|
+
if(ok) {
|
12888
|
+
// Check for uniqueness of code
|
12889
|
+
for(let i = 0; i < x.combination_selectors.length; i++) {
|
12890
|
+
// NOTE: ignore selector being edited, as this selector can be renamed
|
12891
|
+
if(i != this.edited_combi_selector_index &&
|
12892
|
+
x.combination_selectors[i].startsWith(code + '|')) ok = false;
|
12893
|
+
}
|
12894
|
+
}
|
12895
|
+
if(!ok) {
|
12896
|
+
UI.warn(`Combination selector "${code}" already defined`);
|
12897
|
+
sc.focus();
|
12898
|
+
return;
|
12899
|
+
}
|
12900
|
+
// Test for orthogonality (and existence!) of the selectors
|
12901
|
+
if(!x.orthogonalSelectors(value.split(' '))) {
|
12902
|
+
ss.focus();
|
12903
|
+
return;
|
12904
|
+
}
|
12905
|
+
// Combination selector has format code|space-separated selectors
|
12906
|
+
const sel = code + '|' + value;
|
12907
|
+
if(add) {
|
12908
|
+
x.combination_selectors.push(sel);
|
12909
|
+
} else {
|
12910
|
+
// NOTE: rename occurrence of code in dimension (should at most be 1)
|
12911
|
+
const oc = x.combination_selectors[this.edited_combi_selector_index].split('|')[0];
|
12912
|
+
x.combination_selectors[this.edited_combi_selector_index] = sel;
|
12913
|
+
for(let i = 0; i < x.combination_dimensions.length; i++) {
|
12914
|
+
const si = x.combination_dimensions[i].indexOf(oc);
|
12915
|
+
if(si >= 0) x.combination_dimensions[i][si] = code;
|
12916
|
+
}
|
12917
|
+
}
|
12918
|
+
}
|
12919
|
+
md.hide();
|
12920
|
+
}
|
12921
|
+
// Update combination dimensions dialog
|
12922
|
+
this.editCombinationDimensions();
|
12923
|
+
}
|
12924
|
+
|
12925
|
+
editCombinationDimension(dimnr) {
|
12926
|
+
const x = this.selected_experiment;
|
12927
|
+
if(!x) return;
|
12928
|
+
let action = 'Add',
|
12929
|
+
clear = '',
|
12930
|
+
value = '';
|
12931
|
+
this.edited_combi_dimension_index = dimnr;
|
12932
|
+
if(dimnr >= 0) {
|
12933
|
+
action = 'Edit';
|
12934
|
+
clear = '(clear to remove)';
|
12935
|
+
// NOTE: present to modeler as space-separated string
|
12936
|
+
value = x.combination_dimensions[dimnr].join(' ');
|
12937
|
+
}
|
12938
|
+
const md = this.combination_dimension_modal;
|
12939
|
+
md.element('action').innerHTML = action;
|
12940
|
+
md.element('clear').innerHTML = clear;
|
12941
|
+
md.element('string').value = value;
|
12942
|
+
md.show('string');
|
12943
|
+
}
|
12944
|
+
|
12945
|
+
modifyCombinationDimension() {
|
12946
|
+
let x = this.selected_experiment;
|
12947
|
+
if(x) {
|
12948
|
+
const
|
12949
|
+
add = this.edited_combi_dimension_index < 0,
|
12950
|
+
// Trim whitespace and reduce inner spacing to a single space
|
12951
|
+
dimstr = this.combination_dimension_modal.element('string').value.trim();
|
12952
|
+
// Remove dimension if field has been cleared
|
12953
|
+
if(dimstr.length === 0) {
|
12954
|
+
if(!add) {
|
12955
|
+
x.combination_dimensions.splice(this.edited_combi_dimension_index, 1);
|
12956
|
+
}
|
12957
|
+
} else {
|
12958
|
+
// Check for valid selector list
|
12959
|
+
const
|
12960
|
+
dim = dimstr.split(/\s+/g),
|
12961
|
+
ssl = [];
|
12962
|
+
// Get this experiment's combination selector list
|
12963
|
+
for(let i = 0; i < x.combination_selectors.length; i++) {
|
12964
|
+
ssl.push(x.combination_selectors[i].split('|')[0]);
|
12965
|
+
}
|
12966
|
+
// All selectors in string should have been defined
|
12967
|
+
let c = complement(dim, ssl);
|
12968
|
+
if(c.length > 0) {
|
12969
|
+
UI.warn('Combination dimension contains ' +
|
12970
|
+
pluralS(c.length, 'unknown selector') + ': ' + c.join(' '));
|
12971
|
+
return;
|
12972
|
+
}
|
12973
|
+
// All selectors should expand to non-overlapping selector sets
|
12974
|
+
if(!x.orthogonalCombinationDimensions(dim)) return;
|
12975
|
+
// Do not add when a (setwise) identical combination dimension exists
|
12976
|
+
for(let i = 0; i < x.combination_dimensions.length; i++) {
|
12977
|
+
const cd = x.combination_dimensions[i];
|
12978
|
+
if(intersection(dim, cd).length === dim.length) {
|
12979
|
+
UI.notify('Combination already defined: ' + setString(cd));
|
12980
|
+
return;
|
12981
|
+
}
|
12982
|
+
}
|
12983
|
+
// OK? Then add or modify
|
12984
|
+
if(add) {
|
12985
|
+
x.combination_dimensions.push(dim);
|
12986
|
+
} else {
|
12987
|
+
x.combination_dimensions[this.edited_combi_dimension_index] = dim;
|
12988
|
+
}
|
12989
|
+
}
|
12990
|
+
}
|
12991
|
+
this.combination_dimension_modal.hide();
|
12992
|
+
// Update combination dimensions dialog
|
12993
|
+
this.editCombinationDimensions();
|
12994
|
+
}
|
12995
|
+
|
12394
12996
|
editActorDimension() {
|
12395
12997
|
// Open dialog for editing the actor dimension
|
12396
12998
|
const x = this.selected_experiment, rows = [];
|
@@ -12614,22 +13216,10 @@ N = ${rr.N}, vector length = ${rr.vector.length}` : '')].join('');
|
|
12614
13216
|
const ol = [];
|
12615
13217
|
this.parameter_modal.element('type').innerHTML = type;
|
12616
13218
|
if(type === 'dimension') {
|
12617
|
-
|
12618
|
-
|
12619
|
-
|
12620
|
-
|
12621
|
-
dl.push(x.settings_dimensions[i]);
|
12622
|
-
}
|
12623
|
-
for(let i = 0; i < x.actor_dimensions.length; i++) {
|
12624
|
-
dl.push(x.actor_dimensions[i]);
|
12625
|
-
}
|
12626
|
-
for(let i = 0; i < dl.length; i++) {
|
12627
|
-
const d = dl[i];
|
12628
|
-
// NOTE: exclude dimensions already in the selected experiment
|
12629
|
-
if (x.hasDimension(d) < 0) {
|
12630
|
-
const ds = setString(d);
|
12631
|
-
ol.push(`<option value="${ds}">${ds}</option>`);
|
12632
|
-
}
|
13219
|
+
x.inferAvailableDimensions();
|
13220
|
+
for(let i = 0; i < x.available_dimensions.length; i++) {
|
13221
|
+
const ds = setString(x.available_dimensions[i]);
|
13222
|
+
ol.push(`<option value="${ds}">${ds}</option>`);
|
12633
13223
|
}
|
12634
13224
|
} else {
|
12635
13225
|
for(let i = 0; i < this.suitable_charts.length; i++) {
|
@@ -12696,7 +13286,7 @@ N = ${rr.N}, vector length = ${rr.vector.length}` : '')].join('');
|
|
12696
13286
|
if(x) {
|
12697
13287
|
x.excluded_selectors = this.exclude.value.replace(
|
12698
13288
|
/[\;\,]/g, ' ').trim().replace(
|
12699
|
-
/[^a-zA-Z0-9
|
13289
|
+
/[^a-zA-Z0-9\+\-\=\%\_\s]/g, '').split(/\s+/).join(' ');
|
12700
13290
|
this.exclude.value = x.excluded_selectors;
|
12701
13291
|
this.updateParameters();
|
12702
13292
|
}
|
@@ -12788,7 +13378,7 @@ N = ${rr.N}, vector length = ${rr.vector.length}` : '')].join('');
|
|
12788
13378
|
md.element('separator').value = ds.separator;
|
12789
13379
|
md.element('quotes').value = ds.quotes;
|
12790
13380
|
md.element('precision').value = ds.precision;
|
12791
|
-
md.element('var-count').innerText = x.
|
13381
|
+
md.element('var-count').innerText = x.runs[0].results.length;
|
12792
13382
|
md.element('run-count').innerText = runs;
|
12793
13383
|
md.element('run-s').innerText = (sruns === 1 ? '' : 's');
|
12794
13384
|
}
|
@@ -13343,7 +13933,7 @@ class DocumentationManager {
|
|
13343
13933
|
}
|
13344
13934
|
lis.push(`<li>${dn}</li>`);
|
13345
13935
|
}
|
13346
|
-
lis.sort();
|
13936
|
+
lis.sort(ciCompare);
|
13347
13937
|
this.viewer.innerHTML = `<ul>${lis.join('')}</ul>`;
|
13348
13938
|
}
|
13349
13939
|
}
|
@@ -13372,7 +13962,7 @@ class DocumentationManager {
|
|
13372
13962
|
for(let i = 0; i < iol.length; i++) {
|
13373
13963
|
lis.push(`<li>${iol[i].displayName}</li>`);
|
13374
13964
|
}
|
13375
|
-
lis.sort();
|
13965
|
+
lis.sort(ciCompare);
|
13376
13966
|
this.viewer.innerHTML = `<ul>${lis.join('')}</ul>`;
|
13377
13967
|
}
|
13378
13968
|
}
|
@@ -13690,7 +14280,7 @@ class Finder {
|
|
13690
14280
|
}
|
13691
14281
|
}
|
13692
14282
|
}
|
13693
|
-
enl.sort();
|
14283
|
+
enl.sort(ciCompare);
|
13694
14284
|
}
|
13695
14285
|
document.getElementById('finder-entity-imgs').innerHTML = imgs;
|
13696
14286
|
let seid = 'etr';
|
@@ -14543,6 +15133,9 @@ class UndoStack {
|
|
14543
15133
|
this.undoables.push(ue);
|
14544
15134
|
// Update the GUI buttons
|
14545
15135
|
UI.updateButtons();
|
15136
|
+
// NOTE: update the Finder only if needed, and with a delay because
|
15137
|
+
// the "prepare for undo" is performed before the actual change
|
15138
|
+
if(action !== 'move') setTimeout(() => { FINDER.updateDialog(); }, 5);
|
14546
15139
|
//console.log('push ' + action);
|
14547
15140
|
//console.log(UNDO_STACK);
|
14548
15141
|
}
|
@@ -14839,6 +15432,8 @@ if (MODEL.focal_cluster === fc) {
|
|
14839
15432
|
MODEL.focal_cluster.clearAllProcesses();
|
14840
15433
|
UI.drawDiagram(MODEL);
|
14841
15434
|
UI.updateButtons();
|
15435
|
+
// Update the Finder if needed
|
15436
|
+
if(ue.action !== 'move') FINDER.updateDialog();
|
14842
15437
|
}
|
14843
15438
|
//console.log('undo');
|
14844
15439
|
//console.log(UNDO_STACK);
|
@@ -14890,6 +15485,7 @@ if (MODEL.focal_cluster === fc) {
|
|
14890
15485
|
MODEL.focal_cluster.clearAllProcesses();
|
14891
15486
|
UI.drawDiagram(MODEL);
|
14892
15487
|
UI.updateButtons();
|
15488
|
+
if(re.action !== 'move') FINDER.updateDialog();
|
14893
15489
|
}
|
14894
15490
|
}
|
14895
15491
|
} // END of class UndoStack
|