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.
@@ -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 numerical
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.changeFilter();
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
- mouseMove(e) {
3803
- // Responds to mouse cursor moving over Linny-R diagram area
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.default_unit = md.element('product-unit').value.trim();
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.scale_unit = md.element('unit').value.trim();
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 x = VM.call_stack[i];
5855
- vlist.push(x.object.displayName + '|' + x.attribute);
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(bcode) {
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
- const bwr = VM.blockWithRound;
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/', postData({
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
- and constants (<code title="Mathematical constant &pi; = ${Math.PI}">pi</code>,
6551
+ <code title="A random number from the uniform distribution U(0, 1)">random</code>),
6552
+ constants (<code title="Mathematical constant &pi; = ${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 &lsquo;unbounded&rsquo; 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, &hellip;, 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 &ldquo;tuple&rdquo; (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> and probabilistic operators)<br>
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.attributeExpression('C');
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 = (cls + ' outcome').trim();
9132
+ cls += ' outcome';
8843
9133
  } else if(d.array) {
8844
- cls = (cls + ' array').trim();
9134
+ cls += ' array';
9135
+ } else if(d.data.length > 0) {
9136
+ cls += ' series';
8845
9137
  }
8846
- if(d.black_box) cls = (cls + ' blackbox').trim();
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' ? '' : '&nbsp;' + 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 = sel.replace(/[^a-zA-z0-9\%\+\-]/g, '');
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
- CHART_MANAGER.updateDialog();
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, mp, '</td><td class="equation-expression',
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
- CHART_MANAGER.updateDialog();
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
- c.variables[this.variable_index].displayName;
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
- EXPERIMENT_MANAGER.updateDialog();
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
- EXPERIMENT_MANAGER.updateDialog();
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('sa-delta');
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 database selectors in docu-viewer
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`).parent().classList.add('sa-p-sel');
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`).parent().classList.add('sa-p-sel');
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 n = MODEL.dimensions.length,
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(n, ' data dimension') + ' in model';
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
- n += x.settings_dimensions.length +
11494
- x.actor_dimensions.length - x.dimensions.length;
11495
- dim_count.innerHTML = pluralS(n, 'more dimension');
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.dimensions.length >= MODEL.dimensions.length +
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
- for(let i = 0; i < x.settings_dimensions.length; i++) {
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
- // Compile a list of data dimensions and settings dimensions
12618
- // NOTE: slice to avoid adding settings dimensions to the data dimensions
12619
- const dl = MODEL.dimensions.slice();
12620
- for(let i = 0; i < x.settings_dimensions.length; i++) {
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\+\-\%\_\s]/g, '').split(/\s+/).join(' ');
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.variables.length;
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