linny-r 1.1.23 → 1.2.1
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 +218 -13
- package/static/linny-r.css +220 -33
- package/static/scripts/linny-r-config.js +6 -0
- package/static/scripts/linny-r-ctrl.js +27 -7
- package/static/scripts/linny-r-gui.js +968 -166
- package/static/scripts/linny-r-model.js +889 -230
- package/static/scripts/linny-r-utils.js +5 -0
- package/static/scripts/linny-r-vm.js +310 -89
@@ -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
|
@@ -4338,6 +4378,13 @@ class GUIController extends Controller {
|
|
4338
4378
|
const btns = topmod.getElementsByClassName('ok-btn');
|
4339
4379
|
if(btns.length > 0) btns[0].dispatchEvent(new Event('click'));
|
4340
4380
|
}
|
4381
|
+
} else if(this.dr_dialog_order.length > 0) {
|
4382
|
+
// Send ENTER key event to the top draggable dialog
|
4383
|
+
const last = this.dr_dialog_order.length - 1;
|
4384
|
+
if(last >= 0) {
|
4385
|
+
const mgr = window[this.dr_dialog_order[last].dataset.manager];
|
4386
|
+
if(mgr && 'enterKey' in mgr) mgr.enterKey();
|
4387
|
+
}
|
4341
4388
|
}
|
4342
4389
|
} else if(e.keyCode === 8 &&
|
4343
4390
|
ttype !== 'text' && ttype !== 'password' && ttype !== 'textarea') {
|
@@ -4352,7 +4399,18 @@ class GUIController extends Controller {
|
|
4352
4399
|
return;
|
4353
4400
|
}
|
4354
4401
|
}
|
4355
|
-
//
|
4402
|
+
// Up and down arrow keys
|
4403
|
+
if([38, 40].indexOf(e.keyCode) >= 0) {
|
4404
|
+
e.preventDefault();
|
4405
|
+
// Send event to the top draggable dialog
|
4406
|
+
const last = this.dr_dialog_order.length - 1;
|
4407
|
+
if(last >= 0) {
|
4408
|
+
const mgr = window[this.dr_dialog_order[last].dataset.manager];
|
4409
|
+
// NOTE: pass key direction as -1 for UP and +1 for DOWN
|
4410
|
+
if(mgr && 'upDownKey' in mgr) mgr.upDownKey(e.keyCode - 39);
|
4411
|
+
}
|
4412
|
+
}
|
4413
|
+
// end, home, Left and right arrow keys
|
4356
4414
|
if([35, 36, 37, 39].indexOf(e.keyCode) >= 0) e.preventDefault();
|
4357
4415
|
if(e.keyCode === 35) {
|
4358
4416
|
MODEL.t = MODEL.end_period - MODEL.start_period + 1;
|
@@ -4632,7 +4690,18 @@ class GUIController extends Controller {
|
|
4632
4690
|
if(name === 'initial level') x.is_static = true;
|
4633
4691
|
return true;
|
4634
4692
|
}
|
4635
|
-
|
4693
|
+
|
4694
|
+
updateScaleUnitList() {
|
4695
|
+
// Update the HTML datalist element to reflect all scale units
|
4696
|
+
const
|
4697
|
+
ul = [],
|
4698
|
+
keys = Object.keys(MODEL.scale_units).sort(ciCompare);
|
4699
|
+
for(let i = 0; i < keys.length; i++) {
|
4700
|
+
ul.push(`<option value="${MODEL.scale_units[keys[i]].name}">`);
|
4701
|
+
}
|
4702
|
+
document.getElementById('units-data').innerHTML = ul.join('');
|
4703
|
+
}
|
4704
|
+
|
4636
4705
|
//
|
4637
4706
|
// Navigation in the cluster hierarchy
|
4638
4707
|
//
|
@@ -4824,6 +4893,7 @@ class GUIController extends Controller {
|
|
4824
4893
|
// Create a brand new model with (optionally) specified name and author
|
4825
4894
|
MODEL = new LinnyRModel(
|
4826
4895
|
md.element('name').value.trim(), md.element('author').value.trim());
|
4896
|
+
MODEL.addPreconfiguredScaleUnits();
|
4827
4897
|
md.hide();
|
4828
4898
|
this.updateTimeStep(MODEL.simulationTimeStep);
|
4829
4899
|
this.drawDiagram(MODEL);
|
@@ -5135,9 +5205,15 @@ class GUIController extends Controller {
|
|
5135
5205
|
md.element('time-limit').focus();
|
5136
5206
|
return false;
|
5137
5207
|
}
|
5208
|
+
const
|
5209
|
+
e = md.element('product-unit'),
|
5210
|
+
dsu = UI.cleanName(e.value) || '1';
|
5138
5211
|
model.name = md.element('name').value.trim();
|
5212
|
+
// Display model name in browser unless blank
|
5213
|
+
document.title = model.name || 'Linny-R';
|
5139
5214
|
model.author = md.element('author').value.trim();
|
5140
|
-
model.
|
5215
|
+
if(!model.scale_units.hasOwnProperty(dsu)) model.addScaleUnit(dsu);
|
5216
|
+
model.default_unit = dsu;
|
5141
5217
|
model.currency_unit = md.element('currency-unit').value.trim();
|
5142
5218
|
model.encrypt = UI.boxChecked('settings-encrypt');
|
5143
5219
|
model.decimal_comma = UI.boxChecked('settings-decimal-comma');
|
@@ -5312,9 +5388,11 @@ class GUIController extends Controller {
|
|
5312
5388
|
this.setBox('product-sink', p.is_sink);
|
5313
5389
|
this.setBox('product-data', p.is_data);
|
5314
5390
|
this.setBox('product-stock', p.is_buffer);
|
5391
|
+
// NOTE: price label includes the currency unit and the product unit,
|
5392
|
+
// e.g., EUR/ton
|
5315
5393
|
md.element('P').value = p.price.text;
|
5316
5394
|
md.element('P-unit').innerHTML =
|
5317
|
-
(p.scale_unit === '1' ? '' : p.scale_unit);
|
5395
|
+
(p.scale_unit === '1' ? '' : '/' + p.scale_unit);
|
5318
5396
|
md.element('currency').innerHTML = MODEL.currency_unit;
|
5319
5397
|
md.element('IL').value = p.initial_level.text;
|
5320
5398
|
this.setBox('product-integer', p.integer_level);
|
@@ -5395,7 +5473,7 @@ class GUIController extends Controller {
|
|
5395
5473
|
}
|
5396
5474
|
}
|
5397
5475
|
// Update other properties
|
5398
|
-
p.
|
5476
|
+
p.changeScaleUnit(md.element('unit').value);
|
5399
5477
|
p.equal_bounds = this.getEqualBounds('product-UB-equal');
|
5400
5478
|
p.is_source = this.boxChecked('product-source');
|
5401
5479
|
p.is_sink = this.boxChecked('product-sink');
|
@@ -5851,8 +5929,12 @@ class GUIMonitor {
|
|
5851
5929
|
document.getElementById('call-stack-error').innerHTML =
|
5852
5930
|
`ERROR at t=${t}: ` + VM.errorMessage(err);
|
5853
5931
|
for(let i = 0; i < csl; i++) {
|
5854
|
-
const
|
5855
|
-
|
5932
|
+
const
|
5933
|
+
x = VM.call_stack[i],
|
5934
|
+
// For equations, only show the attribute
|
5935
|
+
ons = (x.object === MODEL.equations_dataset ? '' :
|
5936
|
+
x.object.displayName + '|');
|
5937
|
+
vlist.push(ons + x.attribute);
|
5856
5938
|
// Trim spaces around all object-attribute separators in the expression
|
5857
5939
|
xlist.push(x.text.replace(/\s*\|\s*/g, '|'));
|
5858
5940
|
}
|
@@ -6010,24 +6092,30 @@ class GUIMonitor {
|
|
6010
6092
|
return false;
|
6011
6093
|
}
|
6012
6094
|
|
6013
|
-
submitBlockToSolver(
|
6095
|
+
submitBlockToSolver() {
|
6014
6096
|
let top = MODEL.timeout_period;
|
6015
6097
|
if(VM.max_solver_time && top > VM.max_solver_time) {
|
6016
6098
|
top = VM.max_solver_time;
|
6017
6099
|
UI.notify('Solver time limit for this server is ' +
|
6018
6100
|
VM.max_solver_time + ' seconds');
|
6019
6101
|
}
|
6020
|
-
|
6102
|
+
UI.logHeapSize(`BEFORE creating post data`);
|
6103
|
+
const
|
6104
|
+
bwr = VM.blockWithRound,
|
6105
|
+
pd = postData({
|
6106
|
+
action: 'solve',
|
6107
|
+
user: VM.solver_user,
|
6108
|
+
token: VM.solver_token,
|
6109
|
+
block: VM.block_count,
|
6110
|
+
round: VM.round_sequence[VM.current_round],
|
6111
|
+
data: VM.lines,
|
6112
|
+
timeout: top
|
6113
|
+
});
|
6114
|
+
UI.logHeapSize(`AFTER creating post data`);
|
6115
|
+
// Immediately free the memory taken up by VM.lines
|
6116
|
+
VM.lines = '';
|
6021
6117
|
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
|
-
}))
|
6118
|
+
fetch('solver/', pd)
|
6031
6119
|
.then((response) => {
|
6032
6120
|
if(!response.ok) {
|
6033
6121
|
const msg = `ERROR ${response.status}: ${response.statusText}`;
|
@@ -6039,6 +6127,7 @@ class GUIMonitor {
|
|
6039
6127
|
.then((data) => {
|
6040
6128
|
try {
|
6041
6129
|
VM.processServerResponse(JSON.parse(data));
|
6130
|
+
UI.logHeapSize('After processing results for block #' + this.block_count);
|
6042
6131
|
// If no errors, solve next block (if any)
|
6043
6132
|
// NOTE: use setTimeout so that this calling function returns,
|
6044
6133
|
// and browser can update its DOM to display progress
|
@@ -6063,6 +6152,8 @@ class GUIMonitor {
|
|
6063
6152
|
UI.alert(msg);
|
6064
6153
|
VM.stopSolving();
|
6065
6154
|
});
|
6155
|
+
pd.body = '';
|
6156
|
+
UI.logHeapSize(`after calling FETCH and clearing POST data body`);
|
6066
6157
|
VM.logMessage(VM.block_count,
|
6067
6158
|
`POSTing block #${bwr} took ${VM.elapsedTime} seconds.`);
|
6068
6159
|
UI.logHeapSize(`AFTER posting block #${bwr} to solver`);
|
@@ -6466,20 +6557,24 @@ Attributes, however, are case sensitive!">[Actor X|CF]</code> for cash flow.
|
|
6466
6557
|
<code title="Number of rounds in the sequence">nr</code>,
|
6467
6558
|
<code title="Number of current experiment run (starts at 0)">x</code>,
|
6468
6559
|
<code title="Number of runs in the experiment">nx</code>,
|
6560
|
+
<span title="Index variables of iterator dimensions)">
|
6561
|
+
<code>i</code>, <code>j</code>, <code>k</code>,
|
6562
|
+
</span>
|
6469
6563
|
<code title="Number of time steps in 1 year)">yr</code>,
|
6470
6564
|
<code title="Number of time steps in 1 week)">wk</code>,
|
6471
6565
|
<code title="Number of time steps in 1 day)">d</code>,
|
6472
6566
|
<code title="Number of time steps in 1 hour)">h</code>,
|
6473
6567
|
<code title="Number of time steps in 1 minute)">m</code>,
|
6474
6568
|
<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
|
-
|
6569
|
+
<code title="A random number from the uniform distribution U(0, 1)">random</code>),
|
6570
|
+
constants (<code title="Mathematical constant π = ${Math.PI}">pi</code>,
|
6477
6571
|
<code title="Logical constant true = 1
|
6478
6572
|
NOTE: any non-zero value evaluates as true">true</code>,
|
6479
6573
|
<code title="Logical constant false = 0">false</code>,
|
6480
6574
|
<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.
|
6575
|
+
VM.PLUS_INFINITY.toExponential() + `)">infinity</code>) and scale units
|
6576
|
+
are <strong><em>not</em></strong> enclosed by brackets. Scale units
|
6577
|
+
may be enclosed by single quotes.
|
6483
6578
|
</p>
|
6484
6579
|
<h4>Operators</h4>
|
6485
6580
|
<p><em>Monadic:</em>
|
@@ -6542,7 +6637,8 @@ considers X0, …, Xn as a variable cash flow time series.">npv</code><br>
|
|
6542
6637
|
<em>Grouping:</em>
|
6543
6638
|
<code title="X ; Y evaluates as a group or “tuple” (X, Y)
|
6544
6639
|
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
|
6640
|
+
(use only in combination with <code>max</code>, <code>min</code>, <code>npv</code>
|
6641
|
+
and probabilistic operators)<br>
|
6546
6642
|
</p>
|
6547
6643
|
<p>
|
6548
6644
|
Monadic operators take precedence over dyadic operators.
|
@@ -6574,7 +6670,7 @@ NOTE: Grouping groups results in a single group, e.g., (1;2);(3;4;5) evaluates a
|
|
6574
6670
|
UI.edited_object = UI.dbl_clicked_node;
|
6575
6671
|
this.edited_input_id = 'note-C';
|
6576
6672
|
if(UI.edited_object) {
|
6577
|
-
this.edited_expression = UI.edited_object.
|
6673
|
+
this.edited_expression = UI.edited_object.color;
|
6578
6674
|
} else {
|
6579
6675
|
this.edited_expression = null;
|
6580
6676
|
}
|
@@ -6702,7 +6798,7 @@ NOTE: Grouping groups results in a single group, e.g., (1;2);(3;4;5) evaluates a
|
|
6702
6798
|
// is passed to differentiate between the DOM elements to be used
|
6703
6799
|
const
|
6704
6800
|
type = document.getElementById(prefix + 'variable-obj').value,
|
6705
|
-
n_list = this.namesByType(VM.object_types[type]).sort(),
|
6801
|
+
n_list = this.namesByType(VM.object_types[type]).sort(ciCompare),
|
6706
6802
|
vn = document.getElementById(prefix + 'variable-name'),
|
6707
6803
|
options = [];
|
6708
6804
|
// Add "empty" as first and initial option, but disable it.
|
@@ -6744,7 +6840,7 @@ NOTE: Grouping groups results in a single group, e.g., (1;2);(3;4;5) evaluates a
|
|
6744
6840
|
slist.push(d.modifiers[m].selector);
|
6745
6841
|
}
|
6746
6842
|
// Sort to present equations in alphabetical order
|
6747
|
-
slist.sort();
|
6843
|
+
slist.sort(ciCompare);
|
6748
6844
|
for(let i = 0; i < slist.length; i++) {
|
6749
6845
|
options.push(`<option value="${slist[i]}">${slist[i]}</option>`);
|
6750
6846
|
}
|
@@ -7029,6 +7125,218 @@ class ModelAutoSaver {
|
|
7029
7125
|
} // END of class ModelAutoSaver
|
7030
7126
|
|
7031
7127
|
|
7128
|
+
// CLASS ScaleUnitManager (modal dialog!)
|
7129
|
+
class ScaleUnitManager {
|
7130
|
+
constructor() {
|
7131
|
+
// Add the scale units modal
|
7132
|
+
this.dialog = new ModalDialog('scale-units');
|
7133
|
+
this.dialog.close.addEventListener('click',
|
7134
|
+
() => SCALE_UNIT_MANAGER.dialog.hide());
|
7135
|
+
// Make the add, edit and delete buttons of this modal responsive
|
7136
|
+
this.dialog.element('new-btn').addEventListener('click',
|
7137
|
+
() => SCALE_UNIT_MANAGER.promptForScaleUnit());
|
7138
|
+
this.dialog.element('edit-btn').addEventListener('click',
|
7139
|
+
() => SCALE_UNIT_MANAGER.editScaleUnit());
|
7140
|
+
this.dialog.element('delete-btn').addEventListener('click',
|
7141
|
+
() => SCALE_UNIT_MANAGER.deleteScaleUnit());
|
7142
|
+
// Add the scale unit definition modal
|
7143
|
+
this.new_scale_unit_modal = new ModalDialog('new-scale-unit');
|
7144
|
+
this.new_scale_unit_modal.ok.addEventListener(
|
7145
|
+
'click', () => SCALE_UNIT_MANAGER.addNewScaleUnit());
|
7146
|
+
this.new_scale_unit_modal.cancel.addEventListener(
|
7147
|
+
'click', () => SCALE_UNIT_MANAGER.new_scale_unit_modal.hide());
|
7148
|
+
this.scroll_area = this.dialog.element('scroll-area');
|
7149
|
+
this.table = this.dialog.element('table');
|
7150
|
+
}
|
7151
|
+
|
7152
|
+
get selectedUnitIsBaseUnit() {
|
7153
|
+
// Returns TRUE iff selected unit is used as base unit for some unit
|
7154
|
+
for(let u in this.scale_units) if(this.scale_units.hasOwnProperty(u)) {
|
7155
|
+
if(this.scale_units[u].base_unit === this.selected_unit) return true;
|
7156
|
+
}
|
7157
|
+
return false;
|
7158
|
+
}
|
7159
|
+
|
7160
|
+
show() {
|
7161
|
+
// Show the user-defined scale units for the current model
|
7162
|
+
// NOTE: add/edit/delete actions operate on this list, so changes
|
7163
|
+
// take immediate effect
|
7164
|
+
MODEL.cleanUpScaleUnits();
|
7165
|
+
// NOTE: unit name is key in the scale units object
|
7166
|
+
this.selected_unit = '';
|
7167
|
+
this.last_time_selected = 0;
|
7168
|
+
this.updateDialog();
|
7169
|
+
this.dialog.show();
|
7170
|
+
}
|
7171
|
+
|
7172
|
+
updateDialog() {
|
7173
|
+
// Create the HTML for the scale units table and update the state
|
7174
|
+
// of the action buttons
|
7175
|
+
if(!MODEL.scale_units.hasOwnProperty(this.selected_unit)) {
|
7176
|
+
this.selected_unit = '';
|
7177
|
+
}
|
7178
|
+
const
|
7179
|
+
keys = Object.keys(MODEL.scale_units).sort(ciCompare),
|
7180
|
+
sl = [],
|
7181
|
+
ss = this.selected_unit;
|
7182
|
+
let ssid = 'scntr';
|
7183
|
+
if(keys.length <= 1) {
|
7184
|
+
// Only one key => must be the default '1'
|
7185
|
+
sl.push('<tr><td><em>No units defined</em></td></tr>');
|
7186
|
+
} else {
|
7187
|
+
for(let i = 1; i < keys.length; i++) {
|
7188
|
+
const
|
7189
|
+
s = keys[i],
|
7190
|
+
clk = '" onclick="SCALE_UNIT_MANAGER.selectScaleUnit(event, \'' +
|
7191
|
+
s + '\'';
|
7192
|
+
if(s === ss) ssid += i;
|
7193
|
+
sl.push(['<tr id="scntr', i, '" class="dataset-modif',
|
7194
|
+
(s === ss ? ' sel-set' : ''),
|
7195
|
+
'"><td class="dataset-selector', clk, ');">',
|
7196
|
+
s, '</td><td class="dataset-selector', clk, ', \'scalar\');">',
|
7197
|
+
MODEL.scale_units[s].scalar, '</td><td class="dataset-selector',
|
7198
|
+
clk, ', \'base\');">', MODEL.scale_units[s].base_unit,
|
7199
|
+
'</td></tr>'].join(''));
|
7200
|
+
}
|
7201
|
+
}
|
7202
|
+
this.table.innerHTML = sl.join('');
|
7203
|
+
if(ss) UI.scrollIntoView(document.getElementById(ssid));
|
7204
|
+
let btns = 'scale-units-edit';
|
7205
|
+
if(!this.selectedUnitIsBaseUnit) btns += ' scale-units-delete';
|
7206
|
+
if(ss) {
|
7207
|
+
UI.enableButtons(btns);
|
7208
|
+
} else {
|
7209
|
+
UI.disableButtons(btns);
|
7210
|
+
}
|
7211
|
+
}
|
7212
|
+
|
7213
|
+
selectScaleUnit(event, symbol, focus) {
|
7214
|
+
// Select scale unit, and when double-clicked, allow to edit it
|
7215
|
+
const
|
7216
|
+
ss = this.selected_unit,
|
7217
|
+
now = Date.now(),
|
7218
|
+
dt = now - this.last_time_selected,
|
7219
|
+
// NOTE: Alt-click and double-click indicate: edit
|
7220
|
+
// Consider click to be "double" if the same modifier was clicked
|
7221
|
+
// less than 300 ms ago
|
7222
|
+
edit = event.altKey || (symbol === ss && dt < 300);
|
7223
|
+
this.selected_unit = symbol;
|
7224
|
+
this.last_time_selected = now;
|
7225
|
+
if(edit) {
|
7226
|
+
this.last_time_selected = 0;
|
7227
|
+
this.promptForScaleUnit('Edit', focus);
|
7228
|
+
return;
|
7229
|
+
}
|
7230
|
+
this.updateDialog();
|
7231
|
+
}
|
7232
|
+
|
7233
|
+
promptForScaleUnit(action='Define new', focus='name') {
|
7234
|
+
// Show the Add/Edit scale unit dialog for the indicated action
|
7235
|
+
const md = this.new_scale_unit_modal;
|
7236
|
+
// NOTE: by default, let name and base unit be empty strings, not '1'
|
7237
|
+
let sv = {name: '', scalar: '1', base_unit: '' };
|
7238
|
+
if(action === 'Edit' && this.selected_unit) {
|
7239
|
+
sv = MODEL.scale_units[this.selected_unit];
|
7240
|
+
}
|
7241
|
+
md.element('action').innerText = action;
|
7242
|
+
md.element('name').value = sv.name;
|
7243
|
+
md.element('scalar').value = sv.scalar;
|
7244
|
+
md.element('base').value = sv.base_unit;
|
7245
|
+
UI.updateScaleUnitList();
|
7246
|
+
this.new_scale_unit_modal.show(focus);
|
7247
|
+
}
|
7248
|
+
|
7249
|
+
addNewScaleUnit() {
|
7250
|
+
// Add the new scale unit or update the one being edited
|
7251
|
+
const
|
7252
|
+
md = this.new_scale_unit_modal,
|
7253
|
+
edited = md.element('action').innerText === 'Edit',
|
7254
|
+
// NOTE: unit name cannot contain single quotes
|
7255
|
+
s = UI.cleanName(md.element('name').value).replace("'", ''),
|
7256
|
+
v = md.element('scalar').value.trim(),
|
7257
|
+
// NOTE: accept empty base unit to denote '1'
|
7258
|
+
b = md.element('base').value.trim() || '1';
|
7259
|
+
if(!s) {
|
7260
|
+
// Do not accept empty string as name
|
7261
|
+
UI.warn('Scale unit must have a name');
|
7262
|
+
md.element('name').focus();
|
7263
|
+
return;
|
7264
|
+
}
|
7265
|
+
if(MODEL.scale_units.hasOwnProperty(s) && !edited) {
|
7266
|
+
// Do not accept existing unit as name for new unit
|
7267
|
+
UI.warn(`Scale unit "${s}" is already defined`);
|
7268
|
+
md.element('name').focus();
|
7269
|
+
return;
|
7270
|
+
}
|
7271
|
+
if(b !== s && !MODEL.scale_units.hasOwnProperty(b)) {
|
7272
|
+
UI.warn(`Base unit "${b}" is undefined`);
|
7273
|
+
md.element('base').focus();
|
7274
|
+
return;
|
7275
|
+
}
|
7276
|
+
if(UI.validNumericInput('new-scale-unit-scalar', 'scalar')) {
|
7277
|
+
const ucs = Math.abs(safeStrToFloat(v));
|
7278
|
+
if(ucs < VM.NEAR_ZERO) {
|
7279
|
+
UI.warn(`Unit conversion scalar cannot be zero`);
|
7280
|
+
md.element('scalar').focus();
|
7281
|
+
return;
|
7282
|
+
}
|
7283
|
+
if(b === s && ucs !== 1) {
|
7284
|
+
UI.warn(`When base unit = scale unit, scalar must equal 1`);
|
7285
|
+
md.element('scalar').focus();
|
7286
|
+
return;
|
7287
|
+
}
|
7288
|
+
const selu = this.selected_unit;
|
7289
|
+
if(edited && b !== s) {
|
7290
|
+
// Prevent inconsistencies across scalars
|
7291
|
+
const cr = MODEL.scale_units[b].conversionRates();
|
7292
|
+
if(cr.hasOwnProperty(s)) {
|
7293
|
+
UI.warn(`Defining ${s} in terms of ${b} introduces a circular reference`);
|
7294
|
+
md.element('base').focus();
|
7295
|
+
return;
|
7296
|
+
}
|
7297
|
+
}
|
7298
|
+
if(edited && s !== selu) {
|
7299
|
+
// First rename base units
|
7300
|
+
for(let u in MODEL.scale_units) if(MODEL.scale_units.hasOwnProperty(u)) {
|
7301
|
+
if(MODEL.scale_units[u].base_unit === selu) {
|
7302
|
+
MODEL.scale_units[u].base_unit = s;
|
7303
|
+
}
|
7304
|
+
}
|
7305
|
+
// NOTE: renameScaleUnit replaces references to `s`, not the entry
|
7306
|
+
MODEL.renameScaleUnit(selu, s);
|
7307
|
+
delete MODEL.scale_units[this.selected_unit];
|
7308
|
+
}
|
7309
|
+
MODEL.scale_units[s] = new ScaleUnit(s, v, b);
|
7310
|
+
MODEL.selected_unit = s;
|
7311
|
+
this.new_scale_unit_modal.hide();
|
7312
|
+
UI.updateScaleUnitList();
|
7313
|
+
this.updateDialog();
|
7314
|
+
}
|
7315
|
+
}
|
7316
|
+
|
7317
|
+
editScaleUnit() {
|
7318
|
+
// Allow user to edit name and/or value
|
7319
|
+
if(this.selected_unit) this.promptForScaleUnit('Edit', 'scalar');
|
7320
|
+
}
|
7321
|
+
|
7322
|
+
deleteScaleUnit() {
|
7323
|
+
// Allow user to delete
|
7324
|
+
// @@@TO DO: check whether scale unit is used in the model
|
7325
|
+
if(this.selected_unit && !this.selectedUnitIsBaseUnit) {
|
7326
|
+
delete MODEL.scale_units[this.selected_unit];
|
7327
|
+
this.updateDialog();
|
7328
|
+
}
|
7329
|
+
}
|
7330
|
+
|
7331
|
+
updateScaleUnits() {
|
7332
|
+
// Replace scale unit definitions of model by the new definitions
|
7333
|
+
UI.updateScaleUnitList();
|
7334
|
+
this.dialog.hide();
|
7335
|
+
}
|
7336
|
+
|
7337
|
+
} // END of class ScaleUnitManager
|
7338
|
+
|
7339
|
+
|
7032
7340
|
// CLASS ActorManager (modal dialog!)
|
7033
7341
|
class ActorManager {
|
7034
7342
|
constructor() {
|
@@ -8188,7 +8496,7 @@ class GUIRepositoryBrowser extends RepositoryBrowser {
|
|
8188
8496
|
document.getElementById('repo-include-btn').addEventListener(
|
8189
8497
|
'click', () => REPOSITORY_BROWSER.includeModule());
|
8190
8498
|
document.getElementById('repo-load-btn').addEventListener(
|
8191
|
-
'click', () => REPOSITORY_BROWSER.
|
8499
|
+
'click', () => REPOSITORY_BROWSER.confirmLoadModuleAsModel());
|
8192
8500
|
document.getElementById('repo-store-btn').addEventListener(
|
8193
8501
|
'click', () => REPOSITORY_BROWSER.promptForStoring());
|
8194
8502
|
document.getElementById('repo-black-box-btn').addEventListener(
|
@@ -8235,6 +8543,12 @@ class GUIRepositoryBrowser extends RepositoryBrowser {
|
|
8235
8543
|
this.include_modal.element('actor').addEventListener(
|
8236
8544
|
'blur', () => REPOSITORY_BROWSER.updateActors());
|
8237
8545
|
|
8546
|
+
this.confirm_load_modal = new ModalDialog('confirm-load-from-repo');
|
8547
|
+
this.confirm_load_modal.ok.addEventListener(
|
8548
|
+
'click', () => REPOSITORY_BROWSER.loadModuleAsModel());
|
8549
|
+
this.confirm_load_modal.cancel.addEventListener(
|
8550
|
+
'click', () => REPOSITORY_BROWSER.confirm_load_modal.hide());
|
8551
|
+
|
8238
8552
|
this.confirm_delete_modal = new ModalDialog('confirm-delete-from-repo');
|
8239
8553
|
this.confirm_delete_modal.ok.addEventListener(
|
8240
8554
|
'click', () => REPOSITORY_BROWSER.deleteFromRepository());
|
@@ -8246,6 +8560,31 @@ class GUIRepositoryBrowser extends RepositoryBrowser {
|
|
8246
8560
|
super.reset();
|
8247
8561
|
this.last_time_selected = 0;
|
8248
8562
|
}
|
8563
|
+
|
8564
|
+
enterKey() {
|
8565
|
+
// Open "edit properties" dialog for the selected entity
|
8566
|
+
const srl = this.modules_table.getElementsByClassName('sel-set');
|
8567
|
+
if(srl.length > 0) {
|
8568
|
+
const r = this.modules_table.rows[srl[0].rowIndex];
|
8569
|
+
if(r) {
|
8570
|
+
// Ensure that click will be interpreted as double-click
|
8571
|
+
this.last_time_selected = Date.now();
|
8572
|
+
r.dispatchEvent(new Event('click'));
|
8573
|
+
}
|
8574
|
+
}
|
8575
|
+
}
|
8576
|
+
|
8577
|
+
upDownKey(dir) {
|
8578
|
+
// Select row above or below the selected one (if possible)
|
8579
|
+
const srl = this.modules_table.getElementsByClassName('sel-set');
|
8580
|
+
if(srl.length > 0) {
|
8581
|
+
const r = this.modules_table.rows[srl[0].rowIndex + dir];
|
8582
|
+
if(r) {
|
8583
|
+
UI.scrollIntoView(r);
|
8584
|
+
r.dispatchEvent(new Event('click'));
|
8585
|
+
}
|
8586
|
+
}
|
8587
|
+
}
|
8249
8588
|
|
8250
8589
|
get isLocalHost() {
|
8251
8590
|
// Returns TRUE if first repository on the list is 'local host'
|
@@ -8428,7 +8767,7 @@ class GUIRepositoryBrowser extends RepositoryBrowser {
|
|
8428
8767
|
// Consider click to be "double" if it occurred less than 300 ms ago
|
8429
8768
|
if(dt < 300) {
|
8430
8769
|
this.last_time_selected = 0;
|
8431
|
-
this.
|
8770
|
+
this.includeModule();
|
8432
8771
|
return;
|
8433
8772
|
}
|
8434
8773
|
}
|
@@ -8677,6 +9016,7 @@ class GUIRepositoryBrowser extends RepositoryBrowser {
|
|
8677
9016
|
|
8678
9017
|
loadModuleAsModel() {
|
8679
9018
|
// Loads selected module as model
|
9019
|
+
this.confirm_load_modal.hide();
|
8680
9020
|
if(this.repository_index >= 0 && this.module_index >= 0) {
|
8681
9021
|
// NOTE: when loading new model, the stay-on-top dialogs must be reset
|
8682
9022
|
UI.hideStayOnTopDialogs();
|
@@ -8693,6 +9033,17 @@ class GUIRepositoryBrowser extends RepositoryBrowser {
|
|
8693
9033
|
r.loadModule(this.module_index, true);
|
8694
9034
|
}
|
8695
9035
|
}
|
9036
|
+
|
9037
|
+
confirmLoadModuleAsModel() {
|
9038
|
+
// Prompts modeler to confirm loading the selected module as model
|
9039
|
+
if(this.repository_index >= 0 && this.module_index >= 0 &&
|
9040
|
+
document.getElementById('repo-load-btn').classList.contains('enab')) {
|
9041
|
+
const r = this.repositories[this.repository_index];
|
9042
|
+
this.confirm_load_modal.element('mod-name').innerText =
|
9043
|
+
r.module_names[this.module_index];
|
9044
|
+
this.confirm_load_modal.show();
|
9045
|
+
}
|
9046
|
+
}
|
8696
9047
|
|
8697
9048
|
confirmDeleteFromRepository() {
|
8698
9049
|
// Prompts modeler to confirm deletion of the selected module
|
@@ -8744,7 +9095,7 @@ class GUIDatasetManager extends DatasetManager {
|
|
8744
9095
|
this.filter_text = document.getElementById('ds-filter-text');
|
8745
9096
|
this.filter_text.addEventListener(
|
8746
9097
|
'input', () => DATASET_MANAGER.changeFilter());
|
8747
|
-
this.
|
9098
|
+
this.dataset_table = document.getElementById('dataset-table');
|
8748
9099
|
// Data properties pane
|
8749
9100
|
this.properties = document.getElementById('dataset-properties');
|
8750
9101
|
// Toggle buttons at bottom of dialog
|
@@ -8766,6 +9117,8 @@ class GUIDatasetManager extends DatasetManager {
|
|
8766
9117
|
'click', () => DATASET_MANAGER.editExpression());
|
8767
9118
|
document.getElementById('ds-delete-modif-btn').addEventListener(
|
8768
9119
|
'click', () => DATASET_MANAGER.deleteModifier());
|
9120
|
+
// Modifier table
|
9121
|
+
this.modifier_table = document.getElementById('dataset-modif-table');
|
8769
9122
|
// Modal dialogs
|
8770
9123
|
this.new_modal = new ModalDialog('new-dataset');
|
8771
9124
|
this.new_modal.ok.addEventListener(
|
@@ -8814,7 +9167,59 @@ class GUIDatasetManager extends DatasetManager {
|
|
8814
9167
|
this.selected_modifier = null;
|
8815
9168
|
this.edited_expression = null;
|
8816
9169
|
this.filter_pattern = null;
|
8817
|
-
this.
|
9170
|
+
this.clicked_object = null;
|
9171
|
+
this.last_time_clicked = 0;
|
9172
|
+
this.focal_table = null;
|
9173
|
+
}
|
9174
|
+
|
9175
|
+
doubleClicked(obj) {
|
9176
|
+
const
|
9177
|
+
now = Date.now(),
|
9178
|
+
dt = now - this.last_time_clicked;
|
9179
|
+
this.last_time_clicked = now;
|
9180
|
+
if(obj === this.clicked_object) {
|
9181
|
+
// Consider click to be "double" if it occurred less than 300 ms ago
|
9182
|
+
if(dt < 300) {
|
9183
|
+
this.last_time_clicked = 0;
|
9184
|
+
return true;
|
9185
|
+
}
|
9186
|
+
}
|
9187
|
+
this.clicked_object = obj;
|
9188
|
+
return false;
|
9189
|
+
}
|
9190
|
+
|
9191
|
+
enterKey() {
|
9192
|
+
// Open "edit" dialog for the selected dataset or modifier expression
|
9193
|
+
const srl = this.focal_table.getElementsByClassName('sel-set');
|
9194
|
+
if(srl.length > 0) {
|
9195
|
+
const r = this.focal_table.rows[srl[0].rowIndex];
|
9196
|
+
if(r) {
|
9197
|
+
const e = new Event('click');
|
9198
|
+
if(this.focal_table === this.dataset_table) {
|
9199
|
+
// Emulate Alt-click in the table to open the time series dialog
|
9200
|
+
e.altKey = true;
|
9201
|
+
r.dispatchEvent(e);
|
9202
|
+
} else if(this.focal_table === this.modifier_table) {
|
9203
|
+
// Emulate a double-click on the second cell to edit the expression
|
9204
|
+
this.last_time_clicked = Date.now();
|
9205
|
+
r.cells[1].dispatchEvent(e);
|
9206
|
+
}
|
9207
|
+
}
|
9208
|
+
}
|
9209
|
+
}
|
9210
|
+
|
9211
|
+
upDownKey(dir) {
|
9212
|
+
// Select row above or below the selected one (if possible)
|
9213
|
+
const srl = this.focal_table.getElementsByClassName('sel-set');
|
9214
|
+
if(srl.length > 0) {
|
9215
|
+
let r = this.focal_table.rows[srl[0].rowIndex + dir];
|
9216
|
+
if(r) {
|
9217
|
+
UI.scrollIntoView(r);
|
9218
|
+
// NOTE: cell, not row, listens for onclick event
|
9219
|
+
if(this.focal_table === this.modifier_table) r = r.cells[1];
|
9220
|
+
r.dispatchEvent(new Event('click'));
|
9221
|
+
}
|
9222
|
+
}
|
8818
9223
|
}
|
8819
9224
|
|
8820
9225
|
updateDialog() {
|
@@ -8833,33 +9238,39 @@ class GUIDatasetManager extends DatasetManager {
|
|
8833
9238
|
dnl.push(d);
|
8834
9239
|
}
|
8835
9240
|
}
|
8836
|
-
dnl.sort();
|
9241
|
+
dnl.sort(ciCompare);
|
8837
9242
|
let sdid = 'dstr';
|
8838
9243
|
for(let i = 0; i < dnl.length; i++) {
|
8839
9244
|
const d = MODEL.datasets[dnl[i]];
|
8840
9245
|
let cls = ioclass[MODEL.ioType(d)];
|
8841
9246
|
if(d.outcome) {
|
8842
|
-
cls
|
9247
|
+
cls += ' outcome';
|
8843
9248
|
} else if(d.array) {
|
8844
|
-
cls
|
9249
|
+
cls += ' array';
|
9250
|
+
} else if(d.data.length > 0) {
|
9251
|
+
cls += ' series';
|
8845
9252
|
}
|
8846
|
-
if(d.
|
9253
|
+
if(Object.keys(d.modifiers).length > 0) cls += ' modif';
|
9254
|
+
if(d.black_box) cls += ' blackbox';
|
9255
|
+
cls = cls.trim();
|
8847
9256
|
if(cls) cls = ' class="'+ cls + '"';
|
8848
9257
|
if(d === sd) sdid += i;
|
8849
9258
|
dl.push(['<tr id="dstr', i, '" class="dataset',
|
8850
9259
|
(d === sd ? ' sel-set' : ''),
|
9260
|
+
(d.default_selector ? ' def-sel' : ''),
|
8851
9261
|
'" onclick="DATASET_MANAGER.selectDataset(event, \'',
|
8852
9262
|
dnl[i], '\');" onmouseover="DATASET_MANAGER.showInfo(\'', dnl[i],
|
8853
9263
|
'\', event.shiftKey);"><td', cls, '>', d.displayName,
|
8854
9264
|
'</td></tr>'].join(''));
|
8855
9265
|
}
|
8856
|
-
this.
|
9266
|
+
this.dataset_table.innerHTML = dl.join('');
|
8857
9267
|
const btns = 'ds-data ds-rename ds-clone ds-delete';
|
8858
9268
|
if(sd) {
|
8859
|
-
this.
|
9269
|
+
this.dataset_table.innerHTML = dl.join('');
|
8860
9270
|
this.properties.style.display = 'block';
|
8861
9271
|
document.getElementById('dataset-default').innerHTML =
|
8862
|
-
VM.sig4Dig(sd.default_value)
|
9272
|
+
VM.sig4Dig(sd.default_value) +
|
9273
|
+
(sd.scale_unit === '1' ? '' : ' ' + sd.scale_unit);
|
8863
9274
|
document.getElementById('dataset-count').innerHTML = sd.data.length;
|
8864
9275
|
document.getElementById('dataset-special').innerHTML = sd.propertiesString;
|
8865
9276
|
if(sd.data.length > 0) {
|
@@ -8938,7 +9349,7 @@ class GUIDatasetManager extends DatasetManager {
|
|
8938
9349
|
m.selector, '</td><td class="dataset-expression',
|
8939
9350
|
clk, ');">', m.expression.text, '</td></tr>'].join(''));
|
8940
9351
|
}
|
8941
|
-
|
9352
|
+
this.modifier_table.innerHTML = ml.join('');
|
8942
9353
|
ttls.style.display = 'block';
|
8943
9354
|
msa.style.display = 'block';
|
8944
9355
|
mbtns.style.display = 'block';
|
@@ -8983,16 +9394,13 @@ class GUIDatasetManager extends DatasetManager {
|
|
8983
9394
|
|
8984
9395
|
selectDataset(event, id) {
|
8985
9396
|
// Select dataset, or edit it when Alt- or double-clicked
|
9397
|
+
this.focal_table = this.dataset_table;
|
8986
9398
|
const
|
8987
9399
|
d = MODEL.datasets[id] || null,
|
8988
|
-
|
8989
|
-
dt = now - this.last_time_selected,
|
8990
|
-
// Consider click to be "double" if it occurred less than 300 ms ago
|
8991
|
-
edit = event.altKey || (d === this.selected_dataset && dt < 300);
|
9400
|
+
edit = event.altKey || this.doubleClicked(d);
|
8992
9401
|
this.selected_dataset = d;
|
8993
|
-
this.last_time_selected = now;
|
8994
9402
|
if(d && edit) {
|
8995
|
-
this.
|
9403
|
+
this.last_time_clicked = 0;
|
8996
9404
|
this.editData();
|
8997
9405
|
return;
|
8998
9406
|
}
|
@@ -9002,26 +9410,25 @@ class GUIDatasetManager extends DatasetManager {
|
|
9002
9410
|
selectModifier(event, id, x=true) {
|
9003
9411
|
// Select modifier, or when double-clicked, edit its expression or the
|
9004
9412
|
// name of the modifier
|
9413
|
+
this.focal_table = this.modifier_table;
|
9005
9414
|
if(this.selected_dataset) {
|
9006
9415
|
const m = this.selected_dataset.modifiers[UI.nameToID(id)],
|
9007
|
-
|
9008
|
-
dt = now - this.last_time_selected,
|
9009
|
-
// NOTE: Alt-click and double-click indicate: edit
|
9010
|
-
// Consider click to be "double" if the same modifier was clicked
|
9011
|
-
// less than 300 ms ago
|
9012
|
-
edit = event.altKey || (m === this.selected_modifier && dt < 300);
|
9013
|
-
this.last_time_selected = now;
|
9416
|
+
edit = event.altKey || this.doubleClicked(m);
|
9014
9417
|
if(event.shiftKey) {
|
9418
|
+
// NOTE: prepare to update HTML class of selected dataset
|
9419
|
+
const el = this.dataset_table.getElementsByClassName('sel-set')[0];
|
9015
9420
|
// Toggle dataset default selector
|
9016
9421
|
if(m.selector === this.selected_dataset.default_selector) {
|
9017
9422
|
this.selected_dataset.default_selector = '';
|
9423
|
+
el.classList.remove('def-sel');
|
9018
9424
|
} else {
|
9019
9425
|
this.selected_dataset.default_selector = m.selector;
|
9426
|
+
el.classList.add('def-sel');
|
9020
9427
|
}
|
9021
9428
|
}
|
9022
9429
|
this.selected_modifier = m;
|
9023
9430
|
if(edit) {
|
9024
|
-
this.
|
9431
|
+
this.last_time_clicked = 0;
|
9025
9432
|
if(x) {
|
9026
9433
|
this.editExpression();
|
9027
9434
|
} else {
|
@@ -9094,6 +9501,7 @@ class GUIDatasetManager extends DatasetManager {
|
|
9094
9501
|
// Copy properties of d to nd
|
9095
9502
|
nd.comments = `${d.comments}`;
|
9096
9503
|
nd.default_value = d.default_value;
|
9504
|
+
nd.scale_unit = d.scale_unit;
|
9097
9505
|
nd.time_scale = d.time_scale;
|
9098
9506
|
nd.time_unit = d.time_unit;
|
9099
9507
|
nd.method = d.method;
|
@@ -9189,6 +9597,8 @@ class GUIDatasetManager extends DatasetManager {
|
|
9189
9597
|
const
|
9190
9598
|
hw = this.selected_modifier.hasWildcards,
|
9191
9599
|
sel = this.rename_selector_modal.element('name').value,
|
9600
|
+
// NOTE: normal dataset selector, so remove all invalid characters
|
9601
|
+
clean_sel = sel.replace(/[^a-zA-z0-9\%\+\-]/g, ''),
|
9192
9602
|
// Keep track of old name
|
9193
9603
|
oldm = this.selected_modifier,
|
9194
9604
|
// NOTE: addModifier returns existing one if selector not changed
|
@@ -9199,10 +9609,10 @@ class GUIDatasetManager extends DatasetManager {
|
|
9199
9609
|
if(oldm.selector === this.selected_dataset.default_selector) {
|
9200
9610
|
this.selected_dataset.default_selector = m.selector;
|
9201
9611
|
}
|
9612
|
+
MODEL.renameSelectorInExperiments(oldm.selector, clean_sel);
|
9202
9613
|
// If only case has changed, just update the selector
|
9203
|
-
// NOTE: normal dataset selector, so remove all invalid characters
|
9204
9614
|
if(m === oldm) {
|
9205
|
-
m.selector =
|
9615
|
+
m.selector = clean_sel;
|
9206
9616
|
this.updateDialog();
|
9207
9617
|
return;
|
9208
9618
|
}
|
@@ -9238,12 +9648,8 @@ class GUIDatasetManager extends DatasetManager {
|
|
9238
9648
|
if(msg.length) {
|
9239
9649
|
UI.notify('Updated ' + msg.join(' and '));
|
9240
9650
|
// 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();
|
9651
|
+
// variable name for this dataset + modifier
|
9652
|
+
UI.updateControllerDialogs('CDEFX');
|
9247
9653
|
}
|
9248
9654
|
// NOTE: update dimensions only if dataset now has 2 or more modifiers
|
9249
9655
|
// (ignoring those with wildcards)
|
@@ -9312,6 +9718,7 @@ class GUIDatasetManager extends DatasetManager {
|
|
9312
9718
|
cover = md.element('no-time-msg');
|
9313
9719
|
if(ds) {
|
9314
9720
|
md.element('default').value = ds.default_value;
|
9721
|
+
md.element('unit').value = ds.scale_unit;
|
9315
9722
|
cover.style.display = (ds.array ? 'block' : 'none');
|
9316
9723
|
md.element('time-scale').value = VM.sig4Dig(ds.time_scale);
|
9317
9724
|
// Add options for time unit selector
|
@@ -9376,6 +9783,7 @@ class GUIDatasetManager extends DatasetManager {
|
|
9376
9783
|
}
|
9377
9784
|
// Save the data
|
9378
9785
|
ds.default_value = dv;
|
9786
|
+
ds.changeScaleUnit(this.series_modal.element('unit').value);
|
9379
9787
|
ds.time_scale = ts;
|
9380
9788
|
ds.time_unit = this.series_modal.element('time-unit').value;
|
9381
9789
|
ds.method = this.series_modal.element('method').value;
|
@@ -9441,7 +9849,49 @@ class EquationManager {
|
|
9441
9849
|
this.visible = false;
|
9442
9850
|
this.selected_modifier = null;
|
9443
9851
|
this.edited_expression = null;
|
9444
|
-
this.
|
9852
|
+
this.last_time_clicked = 0;
|
9853
|
+
}
|
9854
|
+
|
9855
|
+
doubleClicked(obj) {
|
9856
|
+
const
|
9857
|
+
now = Date.now(),
|
9858
|
+
dt = now - this.last_time_clicked;
|
9859
|
+
this.last_time_clicked = now;
|
9860
|
+
if(obj === this.clicked_object) {
|
9861
|
+
// Consider click to be "double" if it occurred less than 300 ms ago
|
9862
|
+
if(dt < 300) {
|
9863
|
+
this.last_time_clicked = 0;
|
9864
|
+
return true;
|
9865
|
+
}
|
9866
|
+
}
|
9867
|
+
this.clicked_object = obj;
|
9868
|
+
return false;
|
9869
|
+
}
|
9870
|
+
|
9871
|
+
enterKey() {
|
9872
|
+
// Open the expression editor for the selected equation
|
9873
|
+
const srl = this.table.getElementsByClassName('sel-set');
|
9874
|
+
if(srl.length > 0) {
|
9875
|
+
const r = this.table.rows[srl[0].rowIndex];
|
9876
|
+
if(r) {
|
9877
|
+
// Emulate a double-click on the second cell to edit the expression
|
9878
|
+
this.last_time_clicked = Date.now();
|
9879
|
+
r.cells[1].dispatchEvent(new Event('click'));
|
9880
|
+
}
|
9881
|
+
}
|
9882
|
+
}
|
9883
|
+
|
9884
|
+
upDownKey(dir) {
|
9885
|
+
// Select row above or below the selected one (if possible)
|
9886
|
+
const srl = this.table.getElementsByClassName('sel-set');
|
9887
|
+
if(srl.length > 0) {
|
9888
|
+
const r = this.table.rows[srl[0].rowIndex + dir];
|
9889
|
+
if(r) {
|
9890
|
+
UI.scrollIntoView(r);
|
9891
|
+
// NOTE: not row but cell listens for onclick
|
9892
|
+
r.cells[1].dispatchEvent(new Event('click'));
|
9893
|
+
}
|
9894
|
+
}
|
9445
9895
|
}
|
9446
9896
|
|
9447
9897
|
updateDialog() {
|
@@ -9455,7 +9905,6 @@ class EquationManager {
|
|
9455
9905
|
for(let i = 0; i < msl.length; i++) {
|
9456
9906
|
const
|
9457
9907
|
m = ed.modifiers[UI.nameToID(msl[i])],
|
9458
|
-
mp = (m.parameters ? '\\' + m.parameters.join('\\') : ''),
|
9459
9908
|
clk = '" onclick="EQUATION_MANAGER.selectModifier(event, \'' +
|
9460
9909
|
m.selector + '\'';
|
9461
9910
|
if(m === sm) smid += i;
|
@@ -9464,7 +9913,7 @@ class EquationManager {
|
|
9464
9913
|
'"><td class="equation-selector',
|
9465
9914
|
(m.expression.isStatic ? '' : ' it'),
|
9466
9915
|
clk, ', false);">',
|
9467
|
-
m.selector,
|
9916
|
+
m.selector, '</td><td class="equation-expression',
|
9468
9917
|
clk, ');">', m.expression.text, '</td></tr>'].join(''));
|
9469
9918
|
}
|
9470
9919
|
this.table.innerHTML = ml.join('');
|
@@ -9488,14 +9937,9 @@ class EquationManager {
|
|
9488
9937
|
if(MODEL.equations_dataset) {
|
9489
9938
|
const
|
9490
9939
|
m = MODEL.equations_dataset.modifiers[UI.nameToID(id)] || null,
|
9491
|
-
|
9492
|
-
dt = now - this.last_time_selected,
|
9493
|
-
// Consider click to be "double" if it occurred less than 300 ms ago
|
9494
|
-
edit = event.altKey || (m === this.selected_modifier && dt < 300);
|
9495
|
-
this.last_time_selected = now;
|
9940
|
+
edit = event.altKey || this.doubleClicked(m);
|
9496
9941
|
this.selected_modifier = m;
|
9497
9942
|
if(m && edit) {
|
9498
|
-
this.last_time_selected = 0;
|
9499
9943
|
if(x) {
|
9500
9944
|
this.editEquation();
|
9501
9945
|
} else {
|
@@ -9590,7 +10034,6 @@ class EquationManager {
|
|
9590
10034
|
} else {
|
9591
10035
|
// When a new modifier has been added, more actions are needed
|
9592
10036
|
m.expression = oldm.expression;
|
9593
|
-
m.parameters = oldm.parameters;
|
9594
10037
|
this.deleteEquation();
|
9595
10038
|
this.selected_modifier = m;
|
9596
10039
|
}
|
@@ -9618,11 +10061,7 @@ class EquationManager {
|
|
9618
10061
|
UI.notify('Updated ' + msg.join(' and '));
|
9619
10062
|
// Also update these stay-on-top dialogs, as they may display a
|
9620
10063
|
// variable name for this dataset + modifier
|
9621
|
-
|
9622
|
-
DATASET_MANAGER.updateDialog();
|
9623
|
-
EQUATION_MANAGER.updateDialog();
|
9624
|
-
EXPERIMENT_MANAGER.updateDialog();
|
9625
|
-
FINDER.changeFilter();
|
10064
|
+
UI.updateControllerDialogs('CDEFX');
|
9626
10065
|
}
|
9627
10066
|
// Always close the name prompt dialog, and update the equation manager
|
9628
10067
|
this.rename_modal.hide();
|
@@ -9792,6 +10231,31 @@ class GUIChartManager extends ChartManager {
|
|
9792
10231
|
this.last_time_selected = 0;
|
9793
10232
|
}
|
9794
10233
|
|
10234
|
+
enterKey() {
|
10235
|
+
// Open "edit" dialog for the selected chart variable
|
10236
|
+
const srl = this.variables_table.getElementsByClassName('sel-set');
|
10237
|
+
if(srl.length > 0) {
|
10238
|
+
const r = this.variables_table.rows[srl[0].rowIndex];
|
10239
|
+
if(r) {
|
10240
|
+
// Emulate a double-click to edit the variable properties
|
10241
|
+
this.last_time_selected = Date.now();
|
10242
|
+
r.dispatchEvent(new Event('click'));
|
10243
|
+
}
|
10244
|
+
}
|
10245
|
+
}
|
10246
|
+
|
10247
|
+
upDownKey(dir) {
|
10248
|
+
// Select row above or below the selected one (if possible)
|
10249
|
+
const srl = this.variables_table.getElementsByClassName('sel-set');
|
10250
|
+
if(srl.length > 0) {
|
10251
|
+
const r = this.variables_table.rows[srl[0].rowIndex + dir];
|
10252
|
+
if(r) {
|
10253
|
+
UI.scrollIntoView(r);
|
10254
|
+
r.dispatchEvent(new Event('click'));
|
10255
|
+
}
|
10256
|
+
}
|
10257
|
+
}
|
10258
|
+
|
9795
10259
|
setRunsChart(show) {
|
9796
10260
|
// Indicates whether the chart manager should display a run result chart
|
9797
10261
|
this.runs_chart = show;
|
@@ -9925,6 +10389,11 @@ class GUIChartManager extends ChartManager {
|
|
9925
10389
|
u_btn = 'chart-variable-up ',
|
9926
10390
|
d_btn = 'chart-variable-down ',
|
9927
10391
|
ed_btns = 'chart-edit-variable chart-delete-variable ';
|
10392
|
+
// Just in case variable index has not been adjusted after some
|
10393
|
+
// variables have been deleted
|
10394
|
+
if(this.variable_index >= c.variables.length) {
|
10395
|
+
this.variable_index = -1;
|
10396
|
+
}
|
9928
10397
|
if(this.variable_index < 0) {
|
9929
10398
|
UI.disableButtons(ed_btns + u_btn + d_btn);
|
9930
10399
|
} else {
|
@@ -9942,7 +10411,7 @@ class GUIChartManager extends ChartManager {
|
|
9942
10411
|
// If the Edit variable dialog is showing, update its header
|
9943
10412
|
if(this.variable_index >= 0 && !UI.hidden('variable-dlg')) {
|
9944
10413
|
document.getElementById('variable-dlg-name').innerHTML =
|
9945
|
-
|
10414
|
+
c.variables[this.variable_index].displayName;
|
9946
10415
|
}
|
9947
10416
|
}
|
9948
10417
|
this.add_variable_modal.element('obj').value = 0;
|
@@ -10047,8 +10516,7 @@ class GUIChartManager extends ChartManager {
|
|
10047
10516
|
if(c.show_title) this.drawChart();
|
10048
10517
|
}
|
10049
10518
|
// Update experiment viewer in case its current experiment uses this chart
|
10050
|
-
|
10051
|
-
FINDER.changeFilter();
|
10519
|
+
UI.updateControllerDialogs('CFX');
|
10052
10520
|
}
|
10053
10521
|
this.rename_chart_modal.hide();
|
10054
10522
|
}
|
@@ -10098,10 +10566,8 @@ class GUIChartManager extends ChartManager {
|
|
10098
10566
|
MODEL.charts.splice(this.chart_index, 1);
|
10099
10567
|
this.chart_index = -1;
|
10100
10568
|
}
|
10101
|
-
this.updateDialog();
|
10102
10569
|
// Also update the experiment viewer (charts define the output variables)
|
10103
|
-
|
10104
|
-
FINDER.changeFilter();
|
10570
|
+
UI.updateControllerDialogs('CFX');
|
10105
10571
|
}
|
10106
10572
|
}
|
10107
10573
|
|
@@ -10176,12 +10642,11 @@ class GUIChartManager extends ChartManager {
|
|
10176
10642
|
this.variable_index = MODEL.charts[this.chart_index].addVariable(o, a);
|
10177
10643
|
if(this.variable_index >= 0) {
|
10178
10644
|
this.add_variable_modal.hide();
|
10179
|
-
this.updateDialog();
|
10180
10645
|
// Also update the experiment viewer (charts define the output variables)
|
10181
10646
|
if(EXPERIMENT_MANAGER.selected_experiment) {
|
10182
10647
|
EXPERIMENT_MANAGER.selected_experiment.inferVariables();
|
10183
|
-
EXPERIMENT_MANAGER.updateDialog();
|
10184
10648
|
}
|
10649
|
+
UI.updateControllerDialogs('CFX');
|
10185
10650
|
}
|
10186
10651
|
}
|
10187
10652
|
}
|
@@ -10317,10 +10782,7 @@ class GUIChartManager extends ChartManager {
|
|
10317
10782
|
this.updateDialog();
|
10318
10783
|
// Also update the experiment viewer (charts define the output variables)
|
10319
10784
|
// and finder dialog
|
10320
|
-
if(EXPERIMENT_MANAGER.selected_experiment)
|
10321
|
-
EXPERIMENT_MANAGER.updateDialog();
|
10322
|
-
FINDER.changeFilter();
|
10323
|
-
}
|
10785
|
+
if(EXPERIMENT_MANAGER.selected_experiment) UI.updateControllerDialogs('FX');
|
10324
10786
|
}
|
10325
10787
|
this.variable_modal.hide();
|
10326
10788
|
}
|
@@ -10710,12 +11172,12 @@ class GUISensitivityAnalysis extends SensitivityAnalysis {
|
|
10710
11172
|
this.showBaseCaseInfo();
|
10711
11173
|
return;
|
10712
11174
|
}
|
10713
|
-
// Otherwise, display list of all
|
11175
|
+
// Otherwise, display list of all dataset selectors in docu-viewer
|
10714
11176
|
if(DOCUMENTATION_MANAGER.visible) {
|
10715
11177
|
const
|
10716
11178
|
ds_dict = MODEL.listOfAllSelectors,
|
10717
11179
|
html = [],
|
10718
|
-
sl = Object.keys(ds_dict).sort();
|
11180
|
+
sl = Object.keys(ds_dict).sort(ciCompare);
|
10719
11181
|
for(let i = 0; i < sl.length; i++) {
|
10720
11182
|
const
|
10721
11183
|
s = sl[i],
|
@@ -11131,7 +11593,7 @@ class GUISensitivityAnalysis extends SensitivityAnalysis {
|
|
11131
11593
|
}
|
11132
11594
|
this.table.innerHTML = html.join('');
|
11133
11595
|
if(this.selected_run >= 0) document.getElementById(
|
11134
|
-
`sa-r${this.selected_run}c0`).
|
11596
|
+
`sa-r${this.selected_run}c0`).parentNode.classList.add('sa-p-sel');
|
11135
11597
|
this.updateData();
|
11136
11598
|
}
|
11137
11599
|
|
@@ -11194,7 +11656,7 @@ class GUISensitivityAnalysis extends SensitivityAnalysis {
|
|
11194
11656
|
} else if(n < MODEL.sensitivity_runs.length) {
|
11195
11657
|
this.selected_run = n;
|
11196
11658
|
if(n >= 0) document.getElementById(
|
11197
|
-
`sa-r${n}c0`).
|
11659
|
+
`sa-r${n}c0`).parentNode.classList.add('sa-p-sel');
|
11198
11660
|
}
|
11199
11661
|
VM.setRunMessages(this.selected_run);
|
11200
11662
|
}
|
@@ -11258,7 +11720,10 @@ class GUIExperimentManager extends ExperimentManager {
|
|
11258
11720
|
this.default_message = document.getElementById('experiment-default-message');
|
11259
11721
|
|
11260
11722
|
this.design = document.getElementById('experiment-design');
|
11723
|
+
this.experiment_table = document.getElementById('experiment-table');
|
11261
11724
|
this.params_div = document.getElementById('experiment-params-div');
|
11725
|
+
this.dimension_table = document.getElementById('experiment-dim-table');
|
11726
|
+
this.chart_table = document.getElementById('experiment-chart-table');
|
11262
11727
|
// NOTE: the Exclude input field responds to several events
|
11263
11728
|
this.exclude = document.getElementById('experiment-exclude');
|
11264
11729
|
this.exclude.addEventListener(
|
@@ -11294,6 +11759,10 @@ class GUIExperimentManager extends ExperimentManager {
|
|
11294
11759
|
'click', () => EXPERIMENT_MANAGER.moveDimension(1));
|
11295
11760
|
document.getElementById('xp-d-settings-btn').addEventListener(
|
11296
11761
|
'click', () => EXPERIMENT_MANAGER.editSettingsDimensions());
|
11762
|
+
document.getElementById('xp-d-iterator-btn').addEventListener(
|
11763
|
+
'click', () => EXPERIMENT_MANAGER.editIteratorRanges());
|
11764
|
+
document.getElementById('xp-d-combination-btn').addEventListener(
|
11765
|
+
'click', () => EXPERIMENT_MANAGER.editCombinationDimensions());
|
11297
11766
|
document.getElementById('xp-d-actor-btn').addEventListener(
|
11298
11767
|
'click', () => EXPERIMENT_MANAGER.editActorDimension());
|
11299
11768
|
document.getElementById('xp-d-delete-btn').addEventListener(
|
@@ -11354,6 +11823,12 @@ class GUIExperimentManager extends ExperimentManager {
|
|
11354
11823
|
this.parameter_modal.cancel.addEventListener(
|
11355
11824
|
'click', () => EXPERIMENT_MANAGER.parameter_modal.hide());
|
11356
11825
|
|
11826
|
+
this.iterator_modal = new ModalDialog('xp-iterator');
|
11827
|
+
this.iterator_modal.ok.addEventListener(
|
11828
|
+
'click', () => EXPERIMENT_MANAGER.modifyIteratorRanges());
|
11829
|
+
this.iterator_modal.cancel.addEventListener(
|
11830
|
+
'click', () => EXPERIMENT_MANAGER.iterator_modal.hide());
|
11831
|
+
|
11357
11832
|
this.settings_modal = new ModalDialog('xp-settings');
|
11358
11833
|
this.settings_modal.close.addEventListener(
|
11359
11834
|
'click', () => EXPERIMENT_MANAGER.closeSettingsDimensions());
|
@@ -11374,6 +11849,26 @@ class GUIExperimentManager extends ExperimentManager {
|
|
11374
11849
|
this.settings_dimension_modal.cancel.addEventListener(
|
11375
11850
|
'click', () => EXPERIMENT_MANAGER.settings_dimension_modal.hide());
|
11376
11851
|
|
11852
|
+
this.combination_modal = new ModalDialog('xp-combination');
|
11853
|
+
this.combination_modal.close.addEventListener(
|
11854
|
+
'click', () => EXPERIMENT_MANAGER.closeCombinationDimensions());
|
11855
|
+
this.combination_modal.element('s-add-btn').addEventListener(
|
11856
|
+
'click', () => EXPERIMENT_MANAGER.editCombinationSelector(-1));
|
11857
|
+
this.combination_modal.element('d-add-btn').addEventListener(
|
11858
|
+
'click', () => EXPERIMENT_MANAGER.editCombinationDimension(-1));
|
11859
|
+
|
11860
|
+
this.combination_selector_modal = new ModalDialog('xp-combination-selector');
|
11861
|
+
this.combination_selector_modal.ok.addEventListener(
|
11862
|
+
'click', () => EXPERIMENT_MANAGER.modifyCombinationSelector());
|
11863
|
+
this.combination_selector_modal.cancel.addEventListener(
|
11864
|
+
'click', () => EXPERIMENT_MANAGER.combination_selector_modal.hide());
|
11865
|
+
|
11866
|
+
this.combination_dimension_modal = new ModalDialog('xp-combination-dimension');
|
11867
|
+
this.combination_dimension_modal.ok.addEventListener(
|
11868
|
+
'click', () => EXPERIMENT_MANAGER.modifyCombinationDimension());
|
11869
|
+
this.combination_dimension_modal.cancel.addEventListener(
|
11870
|
+
'click', () => EXPERIMENT_MANAGER.combination_dimension_modal.hide());
|
11871
|
+
|
11377
11872
|
this.actor_dimension_modal = new ModalDialog('xp-actor-dimension');
|
11378
11873
|
this.actor_dimension_modal.close.addEventListener(
|
11379
11874
|
'click', () => EXPERIMENT_MANAGER.closeActorDimension());
|
@@ -11423,10 +11918,24 @@ class GUIExperimentManager extends ExperimentManager {
|
|
11423
11918
|
this.selected_parameter = '';
|
11424
11919
|
this.edited_selector_index = -1;
|
11425
11920
|
this.edited_dimension_index = -1;
|
11921
|
+
this.edited_combi_selector_index = -1;
|
11426
11922
|
this.color_scale = new ColorScale('no');
|
11923
|
+
this.focal_table = null;
|
11427
11924
|
this.designMode();
|
11428
11925
|
}
|
11429
11926
|
|
11927
|
+
upDownKey(dir) {
|
11928
|
+
// Select row above or below the selected one (if possible)
|
11929
|
+
const srl = this.focal_table.getElementsByClassName('sel-set');
|
11930
|
+
if(srl.length > 0) {
|
11931
|
+
const r = this.focal_table.rows[srl[0].rowIndex + dir];
|
11932
|
+
if(r) {
|
11933
|
+
UI.scrollIntoView(r);
|
11934
|
+
r.dispatchEvent(new Event('click'));
|
11935
|
+
}
|
11936
|
+
}
|
11937
|
+
}
|
11938
|
+
|
11430
11939
|
updateDialog() {
|
11431
11940
|
this.updateChartList();
|
11432
11941
|
// Warn modeler if no meaningful experiments can be defined
|
@@ -11448,7 +11957,7 @@ class GUIExperimentManager extends ExperimentManager {
|
|
11448
11957
|
for(let i = 0; i < MODEL.experiments.length; i++) {
|
11449
11958
|
xtl.push(MODEL.experiments[i].title);
|
11450
11959
|
}
|
11451
|
-
xtl.sort();
|
11960
|
+
xtl.sort(ciCompare);
|
11452
11961
|
for(let i = 0; i < xtl.length; i++) {
|
11453
11962
|
const
|
11454
11963
|
xi = MODEL.indexOfExperiment(xtl[i]),
|
@@ -11460,7 +11969,7 @@ class GUIExperimentManager extends ExperimentManager {
|
|
11460
11969
|
'\');" onmouseover="EXPERIMENT_MANAGER.showInfo(', xi,
|
11461
11970
|
', event.shiftKey);"><td>', x.title, '</td></tr>'].join(''));
|
11462
11971
|
}
|
11463
|
-
|
11972
|
+
this.experiment_table.innerHTML = xl.join('');
|
11464
11973
|
const
|
11465
11974
|
btns = 'xp-rename xp-view xp-delete xp-ignore',
|
11466
11975
|
icnt = document.getElementById('xp-ignore-count');
|
@@ -11490,24 +11999,25 @@ class GUIExperimentManager extends ExperimentManager {
|
|
11490
11999
|
|
11491
12000
|
updateParameters() {
|
11492
12001
|
MODEL.inferDimensions();
|
11493
|
-
let
|
11494
|
-
canview = true;
|
12002
|
+
let canview = true;
|
11495
12003
|
const
|
11496
12004
|
dim_count = document.getElementById('experiment-dim-count'),
|
11497
12005
|
combi_count = document.getElementById('experiment-combi-count'),
|
11498
12006
|
header = document.getElementById('experiment-params-header'),
|
11499
12007
|
x = this.selected_experiment;
|
11500
12008
|
if(!x) {
|
11501
|
-
dim_count.innerHTML = pluralS(
|
12009
|
+
dim_count.innerHTML = pluralS(
|
12010
|
+
MODEL.dimensions.length, ' data dimension') + ' in model';
|
11502
12011
|
combi_count.innerHTML = '';
|
11503
12012
|
header.innerHTML = '(no experiment selected)';
|
11504
12013
|
this.params_div.style.display = 'none';
|
11505
12014
|
return;
|
11506
12015
|
}
|
11507
12016
|
x.updateActorDimension();
|
11508
|
-
|
11509
|
-
|
11510
|
-
dim_count.innerHTML = pluralS(
|
12017
|
+
x.updateIteratorDimensions();
|
12018
|
+
x.inferAvailableDimensions();
|
12019
|
+
dim_count.innerHTML = pluralS(x.available_dimensions.length,
|
12020
|
+
'more dimension');
|
11511
12021
|
x.inferActualDimensions();
|
11512
12022
|
x.inferCombinations();
|
11513
12023
|
combi_count.innerHTML = pluralS(x.combinations.length, 'combination');
|
@@ -11523,13 +12033,12 @@ class GUIExperimentManager extends ExperimentManager {
|
|
11523
12033
|
setString(x.dimensions[i]),
|
11524
12034
|
'</td></tr>'].join(''));
|
11525
12035
|
}
|
11526
|
-
|
12036
|
+
this.dimension_table.innerHTML = tr.join('');
|
11527
12037
|
// Add button must be enabled only if there still are unused dimensions
|
11528
|
-
if(x.
|
11529
|
-
x.settings_dimensions.length + x.actor_dimensions.length) {
|
11530
|
-
document.getElementById('xp-d-add-btn').classList.add('v-disab');
|
11531
|
-
} else {
|
12038
|
+
if(x.available_dimensions.length > 0) {
|
11532
12039
|
document.getElementById('xp-d-add-btn').classList.remove('v-disab');
|
12040
|
+
} else {
|
12041
|
+
document.getElementById('xp-d-add-btn').classList.add('v-disab');
|
11533
12042
|
}
|
11534
12043
|
this.updateUpDownButtons();
|
11535
12044
|
tr.length = 0;
|
@@ -11540,7 +12049,7 @@ class GUIExperimentManager extends ExperimentManager {
|
|
11540
12049
|
i, '\');"><td>',
|
11541
12050
|
x.charts[i].title, '</td></tr>'].join(''));
|
11542
12051
|
}
|
11543
|
-
|
12052
|
+
this.chart_table.innerHTML = tr.join('');
|
11544
12053
|
if(x.charts.length === 0) canview = false;
|
11545
12054
|
if(tr.length >= this.suitable_charts.length) {
|
11546
12055
|
document.getElementById('xp-c-add-btn').classList.add('v-disab');
|
@@ -11655,7 +12164,7 @@ class GUIExperimentManager extends ExperimentManager {
|
|
11655
12164
|
for(let i = 0; i < x.variables.length; i++) {
|
11656
12165
|
vl.push(x.variables[i].displayName);
|
11657
12166
|
}
|
11658
|
-
vl.sort();
|
12167
|
+
vl.sort(ciCompare);
|
11659
12168
|
for(let i = 0; i < vl.length; i++) {
|
11660
12169
|
ol.push(['<option value="', vl[i], '"',
|
11661
12170
|
(vl[i] == x.selected_variable ? ' selected="selected"' : ''),
|
@@ -12200,6 +12709,8 @@ N = ${rr.N}, vector length = ${rr.vector.length}` : '')].join('');
|
|
12200
12709
|
|
12201
12710
|
selectParameter(p) {
|
12202
12711
|
this.selected_parameter = p;
|
12712
|
+
this.focal_table = (p.startsWith('d') ? this.dimension_table :
|
12713
|
+
this.chart_table);
|
12203
12714
|
this.updateDialog();
|
12204
12715
|
}
|
12205
12716
|
|
@@ -12250,6 +12761,61 @@ N = ${rr.N}, vector length = ${rr.vector.length}` : '')].join('');
|
|
12250
12761
|
}
|
12251
12762
|
}
|
12252
12763
|
|
12764
|
+
editIteratorRanges() {
|
12765
|
+
// Open dialog for editing iterator ranges
|
12766
|
+
const
|
12767
|
+
x = this.selected_experiment,
|
12768
|
+
md = this.iterator_modal,
|
12769
|
+
il = ['i', 'j', 'k'];
|
12770
|
+
if(x) {
|
12771
|
+
// NOTE: there are always 3 iterators (i, j k) so these have fixed
|
12772
|
+
// FROM and TO input fields in the dialog
|
12773
|
+
for(let i = 0; i < 3; i++) {
|
12774
|
+
const k = il[i];
|
12775
|
+
md.element(k + '-from').value = x.iterator_ranges[i][0];
|
12776
|
+
md.element(k + '-to').value = x.iterator_ranges[i][1];
|
12777
|
+
}
|
12778
|
+
this.iterator_modal.show();
|
12779
|
+
}
|
12780
|
+
}
|
12781
|
+
|
12782
|
+
modifyIteratorRanges() {
|
12783
|
+
const
|
12784
|
+
x = this.selected_experiment,
|
12785
|
+
md = this.iterator_modal;
|
12786
|
+
if(x) {
|
12787
|
+
// First validate all input fields (must be integer values)
|
12788
|
+
// NOTE: test using a copy so as not to overwrite values until OK
|
12789
|
+
const
|
12790
|
+
il = ['i', 'j', 'k'],
|
12791
|
+
ir = [[0, 0], [0, 0], [0, 0]],
|
12792
|
+
re = /^[\+\-]?[0-9]+$/;
|
12793
|
+
let el, f, t;
|
12794
|
+
for(let i = 0; i < 3; i++) {
|
12795
|
+
const k = il[i];
|
12796
|
+
el = md.element(k + '-from');
|
12797
|
+
f = el.value.trim() || '0';
|
12798
|
+
if(f === '' || re.test(f)) {
|
12799
|
+
el = md.element(k + '-to');
|
12800
|
+
t = el.value.trim() || '0';
|
12801
|
+
if(t === '' || re.test(t)) el = null;
|
12802
|
+
}
|
12803
|
+
// NULL value signals that field inputs are valid
|
12804
|
+
if(el === null) {
|
12805
|
+
ir[i] = [f, t];
|
12806
|
+
} else {
|
12807
|
+
el.focus();
|
12808
|
+
UI.warn('Iterator range limits must be integers (or default to 0)');
|
12809
|
+
return;
|
12810
|
+
}
|
12811
|
+
}
|
12812
|
+
// Input validated, so modify the iterator dimensions
|
12813
|
+
x.iterator_ranges = ir;
|
12814
|
+
this.updateDialog();
|
12815
|
+
}
|
12816
|
+
md.hide();
|
12817
|
+
}
|
12818
|
+
|
12253
12819
|
editSettingsDimensions() {
|
12254
12820
|
// Open dialog for editing model settings dimensions
|
12255
12821
|
const x = this.selected_experiment, rows = [];
|
@@ -12299,7 +12865,7 @@ N = ${rr.N}, vector length = ${rr.vector.length}` : '')].join('');
|
|
12299
12865
|
md.element('clear').innerHTML = clear;
|
12300
12866
|
md.element('code').value = sel[0];
|
12301
12867
|
md.element('string').value = sel[1];
|
12302
|
-
md.show('string');
|
12868
|
+
md.show(sel[0] ? 'string' : 'code');
|
12303
12869
|
}
|
12304
12870
|
|
12305
12871
|
modifySettingsSelector() {
|
@@ -12350,10 +12916,7 @@ N = ${rr.N}, vector length = ${rr.vector.length}` : '')].join('');
|
|
12350
12916
|
// NOTE: rename occurrence of code in dimension (should at most be 1)
|
12351
12917
|
const oc = x.settings_selectors[this.edited_selector_index].split('|')[0];
|
12352
12918
|
x.settings_selectors[this.edited_selector_index] = sel;
|
12353
|
-
|
12354
|
-
const si = x.settings_dimensions[i].indexOf(oc);
|
12355
|
-
if(si >= 0) x.settings_dimensions[i][si] = code;
|
12356
|
-
}
|
12919
|
+
x.renameSelectorInDimensions(oc, code);
|
12357
12920
|
}
|
12358
12921
|
}
|
12359
12922
|
md.hide();
|
@@ -12432,6 +12995,190 @@ N = ${rr.N}, vector length = ${rr.vector.length}` : '')].join('');
|
|
12432
12995
|
this.editSettingsDimensions();
|
12433
12996
|
}
|
12434
12997
|
|
12998
|
+
editCombinationDimensions() {
|
12999
|
+
// Open dialog for editing combination dimensions
|
13000
|
+
const
|
13001
|
+
x = this.selected_experiment,
|
13002
|
+
rows = [];
|
13003
|
+
if(x) {
|
13004
|
+
// Initialize selector list
|
13005
|
+
for(let i = 0; i < x.combination_selectors.length; i++) {
|
13006
|
+
const sel = x.combination_selectors[i].split('|');
|
13007
|
+
rows.push('<tr onclick="EXPERIMENT_MANAGER.editCombinationSelector(', i,
|
13008
|
+
');"><td width="25%">', sel[0], '</td><td>', sel[1], '</td></tr>');
|
13009
|
+
}
|
13010
|
+
this.combination_modal.element('s-table').innerHTML = rows.join('');
|
13011
|
+
// Initialize combination list
|
13012
|
+
rows.length = 0;
|
13013
|
+
for(let i = 0; i < x.combination_dimensions.length; i++) {
|
13014
|
+
const dim = x.combination_dimensions[i];
|
13015
|
+
rows.push('<tr onclick="EXPERIMENT_MANAGER.editCombinationDimension(', i,
|
13016
|
+
');"><td>', setString(dim), '</td></tr>');
|
13017
|
+
}
|
13018
|
+
this.combination_modal.element('d-table').innerHTML = rows.join('');
|
13019
|
+
this.combination_modal.show();
|
13020
|
+
// NOTE: clear infoline because dialog can generate warnings that would
|
13021
|
+
// otherwise remain visible while no longer relevant
|
13022
|
+
UI.setMessage('');
|
13023
|
+
}
|
13024
|
+
}
|
13025
|
+
|
13026
|
+
closeCombinationDimensions() {
|
13027
|
+
// Hide editor, and then update the experiment manager to reflect changes
|
13028
|
+
this.combination_modal.hide();
|
13029
|
+
this.updateDialog();
|
13030
|
+
}
|
13031
|
+
|
13032
|
+
editCombinationSelector(selnr) {
|
13033
|
+
const x = this.selected_experiment;
|
13034
|
+
if(!x) return;
|
13035
|
+
let action = 'Add',
|
13036
|
+
clear = '',
|
13037
|
+
sel = ['', ''];
|
13038
|
+
this.edited_combi_selector_index = selnr;
|
13039
|
+
if(selnr >= 0) {
|
13040
|
+
action = 'Edit';
|
13041
|
+
clear = '(clear to remove)';
|
13042
|
+
sel = x.combination_selectors[selnr].split('|');
|
13043
|
+
}
|
13044
|
+
const md = this.combination_selector_modal;
|
13045
|
+
md.element('action').innerHTML = action;
|
13046
|
+
md.element('clear').innerHTML = clear;
|
13047
|
+
md.element('code').value = sel[0];
|
13048
|
+
md.element('string').value = sel[1];
|
13049
|
+
md.show(sel[0] ? 'string' : 'code');
|
13050
|
+
}
|
13051
|
+
|
13052
|
+
modifyCombinationSelector() {
|
13053
|
+
// Accepts an "orthogonal" set of selectors
|
13054
|
+
let x = this.selected_experiment;
|
13055
|
+
if(x) {
|
13056
|
+
const
|
13057
|
+
md = this.combination_selector_modal,
|
13058
|
+
sc = md.element('code'),
|
13059
|
+
ss = md.element('string'),
|
13060
|
+
// Ignore invalid characters in the combination selector
|
13061
|
+
code = sc.value.replace(/[^\w\+\-\%]/g, ''),
|
13062
|
+
// Reduce comma's, semicolons and multiple spaces in the
|
13063
|
+
// combination string to a single space
|
13064
|
+
value = ss.value.trim().replace(/[\,\;\s]+/g, ' '),
|
13065
|
+
add = this.edited_combi_selector_index < 0;
|
13066
|
+
// Remove selector if either field has been cleared
|
13067
|
+
if(code.length === 0 || value.length === 0) {
|
13068
|
+
if(!add) {
|
13069
|
+
x.combination_selectors.splice(this.edited_combi_selector_index, 1);
|
13070
|
+
}
|
13071
|
+
} else {
|
13072
|
+
let ok = x.allDimensionSelectors.indexOf(code) < 0;
|
13073
|
+
if(ok) {
|
13074
|
+
// Check for uniqueness of code
|
13075
|
+
for(let i = 0; i < x.combination_selectors.length; i++) {
|
13076
|
+
// NOTE: ignore selector being edited, as this selector can be renamed
|
13077
|
+
if(i != this.edited_combi_selector_index &&
|
13078
|
+
x.combination_selectors[i].startsWith(code + '|')) ok = false;
|
13079
|
+
}
|
13080
|
+
}
|
13081
|
+
if(!ok) {
|
13082
|
+
UI.warn(`Combination selector "${code}" already defined`);
|
13083
|
+
sc.focus();
|
13084
|
+
return;
|
13085
|
+
}
|
13086
|
+
// Test for orthogonality (and existence!) of the selectors
|
13087
|
+
if(!x.orthogonalSelectors(value.split(' '))) {
|
13088
|
+
ss.focus();
|
13089
|
+
return;
|
13090
|
+
}
|
13091
|
+
// Combination selector has format code|space-separated selectors
|
13092
|
+
const sel = code + '|' + value;
|
13093
|
+
if(add) {
|
13094
|
+
x.combination_selectors.push(sel);
|
13095
|
+
} else {
|
13096
|
+
// NOTE: rename occurrence of code in dimension (should at most be 1)
|
13097
|
+
const oc = x.combination_selectors[this.edited_combi_selector_index].split('|')[0];
|
13098
|
+
x.combination_selectors[this.edited_combi_selector_index] = sel;
|
13099
|
+
for(let i = 0; i < x.combination_dimensions.length; i++) {
|
13100
|
+
const si = x.combination_dimensions[i].indexOf(oc);
|
13101
|
+
if(si >= 0) x.combination_dimensions[i][si] = code;
|
13102
|
+
}
|
13103
|
+
}
|
13104
|
+
}
|
13105
|
+
md.hide();
|
13106
|
+
}
|
13107
|
+
// Update combination dimensions dialog
|
13108
|
+
this.editCombinationDimensions();
|
13109
|
+
}
|
13110
|
+
|
13111
|
+
editCombinationDimension(dimnr) {
|
13112
|
+
const x = this.selected_experiment;
|
13113
|
+
if(!x) return;
|
13114
|
+
let action = 'Add',
|
13115
|
+
clear = '',
|
13116
|
+
value = '';
|
13117
|
+
this.edited_combi_dimension_index = dimnr;
|
13118
|
+
if(dimnr >= 0) {
|
13119
|
+
action = 'Edit';
|
13120
|
+
clear = '(clear to remove)';
|
13121
|
+
// NOTE: present to modeler as space-separated string
|
13122
|
+
value = x.combination_dimensions[dimnr].join(' ');
|
13123
|
+
}
|
13124
|
+
const md = this.combination_dimension_modal;
|
13125
|
+
md.element('action').innerHTML = action;
|
13126
|
+
md.element('clear').innerHTML = clear;
|
13127
|
+
md.element('string').value = value;
|
13128
|
+
md.show('string');
|
13129
|
+
}
|
13130
|
+
|
13131
|
+
modifyCombinationDimension() {
|
13132
|
+
let x = this.selected_experiment;
|
13133
|
+
if(x) {
|
13134
|
+
const
|
13135
|
+
add = this.edited_combi_dimension_index < 0,
|
13136
|
+
// Trim whitespace and reduce inner spacing to a single space
|
13137
|
+
dimstr = this.combination_dimension_modal.element('string').value.trim();
|
13138
|
+
// Remove dimension if field has been cleared
|
13139
|
+
if(dimstr.length === 0) {
|
13140
|
+
if(!add) {
|
13141
|
+
x.combination_dimensions.splice(this.edited_combi_dimension_index, 1);
|
13142
|
+
}
|
13143
|
+
} else {
|
13144
|
+
// Check for valid selector list
|
13145
|
+
const
|
13146
|
+
dim = dimstr.split(/\s+/g),
|
13147
|
+
ssl = [];
|
13148
|
+
// Get this experiment's combination selector list
|
13149
|
+
for(let i = 0; i < x.combination_selectors.length; i++) {
|
13150
|
+
ssl.push(x.combination_selectors[i].split('|')[0]);
|
13151
|
+
}
|
13152
|
+
// All selectors in string should have been defined
|
13153
|
+
let c = complement(dim, ssl);
|
13154
|
+
if(c.length > 0) {
|
13155
|
+
UI.warn('Combination dimension contains ' +
|
13156
|
+
pluralS(c.length, 'unknown selector') + ': ' + c.join(' '));
|
13157
|
+
return;
|
13158
|
+
}
|
13159
|
+
// All selectors should expand to non-overlapping selector sets
|
13160
|
+
if(!x.orthogonalCombinationDimensions(dim)) return;
|
13161
|
+
// Do not add when a (setwise) identical combination dimension exists
|
13162
|
+
for(let i = 0; i < x.combination_dimensions.length; i++) {
|
13163
|
+
const cd = x.combination_dimensions[i];
|
13164
|
+
if(intersection(dim, cd).length === dim.length) {
|
13165
|
+
UI.notify('Combination already defined: ' + setString(cd));
|
13166
|
+
return;
|
13167
|
+
}
|
13168
|
+
}
|
13169
|
+
// OK? Then add or modify
|
13170
|
+
if(add) {
|
13171
|
+
x.combination_dimensions.push(dim);
|
13172
|
+
} else {
|
13173
|
+
x.combination_dimensions[this.edited_combi_dimension_index] = dim;
|
13174
|
+
}
|
13175
|
+
}
|
13176
|
+
}
|
13177
|
+
this.combination_dimension_modal.hide();
|
13178
|
+
// Update combination dimensions dialog
|
13179
|
+
this.editCombinationDimensions();
|
13180
|
+
}
|
13181
|
+
|
12435
13182
|
editActorDimension() {
|
12436
13183
|
// Open dialog for editing the actor dimension
|
12437
13184
|
const x = this.selected_experiment, rows = [];
|
@@ -12655,22 +13402,10 @@ N = ${rr.N}, vector length = ${rr.vector.length}` : '')].join('');
|
|
12655
13402
|
const ol = [];
|
12656
13403
|
this.parameter_modal.element('type').innerHTML = type;
|
12657
13404
|
if(type === 'dimension') {
|
12658
|
-
|
12659
|
-
|
12660
|
-
|
12661
|
-
|
12662
|
-
dl.push(x.settings_dimensions[i]);
|
12663
|
-
}
|
12664
|
-
for(let i = 0; i < x.actor_dimensions.length; i++) {
|
12665
|
-
dl.push(x.actor_dimensions[i]);
|
12666
|
-
}
|
12667
|
-
for(let i = 0; i < dl.length; i++) {
|
12668
|
-
const d = dl[i];
|
12669
|
-
// NOTE: exclude dimensions already in the selected experiment
|
12670
|
-
if (x.hasDimension(d) < 0) {
|
12671
|
-
const ds = setString(d);
|
12672
|
-
ol.push(`<option value="${ds}">${ds}</option>`);
|
12673
|
-
}
|
13405
|
+
x.inferAvailableDimensions();
|
13406
|
+
for(let i = 0; i < x.available_dimensions.length; i++) {
|
13407
|
+
const ds = setString(x.available_dimensions[i]);
|
13408
|
+
ol.push(`<option value="${ds}">${ds}</option>`);
|
12674
13409
|
}
|
12675
13410
|
} else {
|
12676
13411
|
for(let i = 0; i < this.suitable_charts.length; i++) {
|
@@ -12737,7 +13472,7 @@ N = ${rr.N}, vector length = ${rr.vector.length}` : '')].join('');
|
|
12737
13472
|
if(x) {
|
12738
13473
|
x.excluded_selectors = this.exclude.value.replace(
|
12739
13474
|
/[\;\,]/g, ' ').trim().replace(
|
12740
|
-
/[^a-zA-Z0-9
|
13475
|
+
/[^a-zA-Z0-9\+\-\=\%\_\s]/g, '').split(/\s+/).join(' ');
|
12741
13476
|
this.exclude.value = x.excluded_selectors;
|
12742
13477
|
this.updateParameters();
|
12743
13478
|
}
|
@@ -13384,7 +14119,7 @@ class DocumentationManager {
|
|
13384
14119
|
}
|
13385
14120
|
lis.push(`<li>${dn}</li>`);
|
13386
14121
|
}
|
13387
|
-
lis.sort();
|
14122
|
+
lis.sort(ciCompare);
|
13388
14123
|
this.viewer.innerHTML = `<ul>${lis.join('')}</ul>`;
|
13389
14124
|
}
|
13390
14125
|
}
|
@@ -13413,7 +14148,7 @@ class DocumentationManager {
|
|
13413
14148
|
for(let i = 0; i < iol.length; i++) {
|
13414
14149
|
lis.push(`<li>${iol[i].displayName}</li>`);
|
13415
14150
|
}
|
13416
|
-
lis.sort();
|
14151
|
+
lis.sort(ciCompare);
|
13417
14152
|
this.viewer.innerHTML = `<ul>${lis.join('')}</ul>`;
|
13418
14153
|
}
|
13419
14154
|
}
|
@@ -13593,7 +14328,10 @@ class Finder {
|
|
13593
14328
|
this.copy_btn = document.getElementById('finder-copy-btn');
|
13594
14329
|
this.copy_btn.addEventListener(
|
13595
14330
|
'click', (event) => FINDER.copyAttributesToClipboard(event.shiftKey));
|
13596
|
-
|
14331
|
+
this.entity_table = document.getElementById('finder-table');
|
14332
|
+
this.item_table = document.getElementById('finder-item-table');
|
14333
|
+
this.expression_table = document.getElementById('finder-expression-table');
|
14334
|
+
|
13597
14335
|
// Attribute headers are used by Finder to output entity attribute values
|
13598
14336
|
this.attribute_headers = {
|
13599
14337
|
A: 'ACTORS:\tWeight\tCash IN\tCash OUT\tCash FLOW',
|
@@ -13625,6 +14363,47 @@ class Finder {
|
|
13625
14363
|
this.product_cluster_index = 0;
|
13626
14364
|
}
|
13627
14365
|
|
14366
|
+
doubleClicked(obj) {
|
14367
|
+
const
|
14368
|
+
now = Date.now(),
|
14369
|
+
dt = now - this.last_time_clicked;
|
14370
|
+
this.last_time_clicked = now;
|
14371
|
+
if(obj === this.clicked_object) {
|
14372
|
+
// Consider click to be "double" if it occurred less than 300 ms ago
|
14373
|
+
if(dt < 300) {
|
14374
|
+
this.last_time_clicked = 0;
|
14375
|
+
return true;
|
14376
|
+
}
|
14377
|
+
}
|
14378
|
+
this.clicked_object = obj;
|
14379
|
+
return false;
|
14380
|
+
}
|
14381
|
+
|
14382
|
+
enterKey() {
|
14383
|
+
// Open "edit properties" dialog for the selected entity
|
14384
|
+
const srl = this.entity_table.getElementsByClassName('sel-set');
|
14385
|
+
if(srl.length > 0) {
|
14386
|
+
const r = this.entity_table.rows[srl[0].rowIndex];
|
14387
|
+
if(r) {
|
14388
|
+
const e = new Event('click');
|
14389
|
+
e.altKey = true;
|
14390
|
+
r.dispatchEvent(e);
|
14391
|
+
}
|
14392
|
+
}
|
14393
|
+
}
|
14394
|
+
|
14395
|
+
upDownKey(dir) {
|
14396
|
+
// Select row above or below the selected one (if possible)
|
14397
|
+
const srl = this.entity_table.getElementsByClassName('sel-set');
|
14398
|
+
if(srl.length > 0) {
|
14399
|
+
const r = this.entity_table.rows[srl[0].rowIndex + dir];
|
14400
|
+
if(r) {
|
14401
|
+
UI.scrollIntoView(r);
|
14402
|
+
r.dispatchEvent(new Event('click'));
|
14403
|
+
}
|
14404
|
+
}
|
14405
|
+
}
|
14406
|
+
|
13628
14407
|
updateDialog() {
|
13629
14408
|
const
|
13630
14409
|
el = [],
|
@@ -13731,7 +14510,7 @@ class Finder {
|
|
13731
14510
|
}
|
13732
14511
|
}
|
13733
14512
|
}
|
13734
|
-
enl.sort();
|
14513
|
+
enl.sort(ciCompare);
|
13735
14514
|
}
|
13736
14515
|
document.getElementById('finder-entity-imgs').innerHTML = imgs;
|
13737
14516
|
let seid = 'etr';
|
@@ -13740,7 +14519,7 @@ class Finder {
|
|
13740
14519
|
if(e === se) seid += i;
|
13741
14520
|
el.push(['<tr id="etr', i, '" class="dataset',
|
13742
14521
|
(e === se ? ' sel-set' : ''), '" onclick="FINDER.selectEntity(\'',
|
13743
|
-
enl[i], '\');" onmouseover="FINDER.showInfo(\'', enl[i],
|
14522
|
+
enl[i], '\', event.altKey);" onmouseover="FINDER.showInfo(\'', enl[i],
|
13744
14523
|
'\', event.shiftKey);"><td draggable="true" ',
|
13745
14524
|
'ondragstart="FINDER.drag(event);"><img class="finder" src="images/',
|
13746
14525
|
e.type.toLowerCase(), '.png">', e.displayName,
|
@@ -13748,7 +14527,7 @@ class Finder {
|
|
13748
14527
|
}
|
13749
14528
|
// NOTE: reset `selected_entity` if not in the new list
|
13750
14529
|
if(seid === 'etr') this.selected_entity = null;
|
13751
|
-
|
14530
|
+
this.entity_table.innerHTML = el.join('');
|
13752
14531
|
UI.scrollIntoView(document.getElementById(seid));
|
13753
14532
|
document.getElementById('finder-count').innerHTML = pluralS(
|
13754
14533
|
el.length, 'entity', 'entities');
|
@@ -13903,7 +14682,7 @@ class Finder {
|
|
13903
14682
|
e.type.toLowerCase(), '.png">', e.displayName,
|
13904
14683
|
'</td></tr>'].join(''));
|
13905
14684
|
}
|
13906
|
-
|
14685
|
+
this.item_table.innerHTML = el.join('');
|
13907
14686
|
// Clear the table row list
|
13908
14687
|
el.length = 0;
|
13909
14688
|
// Now fill it with entity+attribute having a matching expression
|
@@ -13931,7 +14710,7 @@ class Finder {
|
|
13931
14710
|
'<img class="finder" src="images/', img, '.png">', td, '</td></tr>'
|
13932
14711
|
].join(''));
|
13933
14712
|
}
|
13934
|
-
|
14713
|
+
this.expression_table.innerHTML = el.join('');
|
13935
14714
|
document.getElementById('finder-expression-hdr').innerHTML =
|
13936
14715
|
pluralS(el.length, 'expression');
|
13937
14716
|
}
|
@@ -13966,10 +14745,37 @@ class Finder {
|
|
13966
14745
|
if(e) DOCUMENTATION_MANAGER.update(e, shift);
|
13967
14746
|
}
|
13968
14747
|
|
13969
|
-
selectEntity(id) {
|
13970
|
-
// Looks up entity, selects it in the left pane, and updates the
|
13971
|
-
|
14748
|
+
selectEntity(id, alt=false) {
|
14749
|
+
// Looks up entity, selects it in the left pane, and updates the
|
14750
|
+
// right pane; opens the "edit properties" modal dialog on double-click
|
14751
|
+
// and Alt-click if the entity is editable
|
14752
|
+
const obj = MODEL.objectByID(id);
|
14753
|
+
this.selected_entity = obj;
|
13972
14754
|
this.updateDialog();
|
14755
|
+
if(!obj) return;
|
14756
|
+
if(alt || this.doubleClicked(obj)) {
|
14757
|
+
if(obj instanceof Process) {
|
14758
|
+
UI.showProcessPropertiesDialog(obj);
|
14759
|
+
} else if(obj instanceof Product) {
|
14760
|
+
UI.showProductPropertiesDialog(obj);
|
14761
|
+
} else if(obj instanceof Link) {
|
14762
|
+
UI.showLinkPropertiesDialog(obj);
|
14763
|
+
} else if(obj instanceof Note) {
|
14764
|
+
obj.showNotePropertiesDialog();
|
14765
|
+
} else if(obj instanceof Dataset) {
|
14766
|
+
if(UI.hidden('dataset-dlg')) {
|
14767
|
+
UI.buttons.dataset.dispatchEvent(new Event('click'));
|
14768
|
+
}
|
14769
|
+
DATASET_MANAGER.selected_dataset = obj;
|
14770
|
+
DATASET_MANAGER.updateDialog();
|
14771
|
+
} else if(obj instanceof DatasetModifier) {
|
14772
|
+
if(UI.hidden('equation-dlg')) {
|
14773
|
+
UI.buttons.equation.dispatchEvent(new Event('click'));
|
14774
|
+
}
|
14775
|
+
EQUATION_MANAGER.selected_modifier = obj;
|
14776
|
+
EQUATION_MANAGER.updateDialog();
|
14777
|
+
}
|
14778
|
+
}
|
13973
14779
|
}
|
13974
14780
|
|
13975
14781
|
reveal(id) {
|
@@ -14020,22 +14826,12 @@ class Finder {
|
|
14020
14826
|
// NOTE: return the object to save a second lookup by revealExpression
|
14021
14827
|
return obj;
|
14022
14828
|
}
|
14023
|
-
|
14829
|
+
|
14024
14830
|
revealExpression(id, attr, shift=false, alt=false) {
|
14025
|
-
const
|
14026
|
-
|
14027
|
-
|
14028
|
-
|
14029
|
-
this.last_time_clicked = now;
|
14030
|
-
if(obj === this.clicked_object) {
|
14031
|
-
// Consider click to be "double" if it occurred less than 300 ms ago
|
14032
|
-
if(dt < 300) {
|
14033
|
-
this.last_time_clicked = 0;
|
14034
|
-
shift = true;
|
14035
|
-
}
|
14036
|
-
}
|
14037
|
-
this.clicked_object = obj;
|
14038
|
-
if(obj && attr && (shift || alt)) {
|
14831
|
+
const obj = this.reveal(id);
|
14832
|
+
if(!obj) return;
|
14833
|
+
shift = shift || this.doubleClicked(obj);
|
14834
|
+
if(attr && (shift || alt)) {
|
14039
14835
|
if(obj instanceof Process) {
|
14040
14836
|
// NOTE: the second argument makes the dialog focus on the specified
|
14041
14837
|
// attribute input field; the third makes it open the expression editor
|
@@ -14584,6 +15380,9 @@ class UndoStack {
|
|
14584
15380
|
this.undoables.push(ue);
|
14585
15381
|
// Update the GUI buttons
|
14586
15382
|
UI.updateButtons();
|
15383
|
+
// NOTE: update the Finder only if needed, and with a delay because
|
15384
|
+
// the "prepare for undo" is performed before the actual change
|
15385
|
+
if(action !== 'move') setTimeout(() => { FINDER.updateDialog(); }, 5);
|
14587
15386
|
//console.log('push ' + action);
|
14588
15387
|
//console.log(UNDO_STACK);
|
14589
15388
|
}
|
@@ -14880,6 +15679,8 @@ if (MODEL.focal_cluster === fc) {
|
|
14880
15679
|
MODEL.focal_cluster.clearAllProcesses();
|
14881
15680
|
UI.drawDiagram(MODEL);
|
14882
15681
|
UI.updateButtons();
|
15682
|
+
// Update the Finder if needed
|
15683
|
+
if(ue.action !== 'move') FINDER.updateDialog();
|
14883
15684
|
}
|
14884
15685
|
//console.log('undo');
|
14885
15686
|
//console.log(UNDO_STACK);
|
@@ -14931,6 +15732,7 @@ if (MODEL.focal_cluster === fc) {
|
|
14931
15732
|
MODEL.focal_cluster.clearAllProcesses();
|
14932
15733
|
UI.drawDiagram(MODEL);
|
14933
15734
|
UI.updateButtons();
|
15735
|
+
if(re.action !== 'move') FINDER.updateDialog();
|
14934
15736
|
}
|
14935
15737
|
}
|
14936
15738
|
} // END of class UndoStack
|