linny-r 1.1.23 → 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
  }
@@ -10710,12 +11004,12 @@ class GUISensitivityAnalysis extends SensitivityAnalysis {
10710
11004
  this.showBaseCaseInfo();
10711
11005
  return;
10712
11006
  }
10713
- // Otherwise, display list of all database selectors in docu-viewer
11007
+ // Otherwise, display list of all dataset selectors in docu-viewer
10714
11008
  if(DOCUMENTATION_MANAGER.visible) {
10715
11009
  const
10716
11010
  ds_dict = MODEL.listOfAllSelectors,
10717
11011
  html = [],
10718
- sl = Object.keys(ds_dict).sort();
11012
+ sl = Object.keys(ds_dict).sort(ciCompare);
10719
11013
  for(let i = 0; i < sl.length; i++) {
10720
11014
  const
10721
11015
  s = sl[i],
@@ -11131,7 +11425,7 @@ class GUISensitivityAnalysis extends SensitivityAnalysis {
11131
11425
  }
11132
11426
  this.table.innerHTML = html.join('');
11133
11427
  if(this.selected_run >= 0) document.getElementById(
11134
- `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');
11135
11429
  this.updateData();
11136
11430
  }
11137
11431
 
@@ -11194,7 +11488,7 @@ class GUISensitivityAnalysis extends SensitivityAnalysis {
11194
11488
  } else if(n < MODEL.sensitivity_runs.length) {
11195
11489
  this.selected_run = n;
11196
11490
  if(n >= 0) document.getElementById(
11197
- `sa-r${n}c0`).parent().classList.add('sa-p-sel');
11491
+ `sa-r${n}c0`).parentNode.classList.add('sa-p-sel');
11198
11492
  }
11199
11493
  VM.setRunMessages(this.selected_run);
11200
11494
  }
@@ -11294,6 +11588,10 @@ class GUIExperimentManager extends ExperimentManager {
11294
11588
  'click', () => EXPERIMENT_MANAGER.moveDimension(1));
11295
11589
  document.getElementById('xp-d-settings-btn').addEventListener(
11296
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());
11297
11595
  document.getElementById('xp-d-actor-btn').addEventListener(
11298
11596
  'click', () => EXPERIMENT_MANAGER.editActorDimension());
11299
11597
  document.getElementById('xp-d-delete-btn').addEventListener(
@@ -11354,6 +11652,12 @@ class GUIExperimentManager extends ExperimentManager {
11354
11652
  this.parameter_modal.cancel.addEventListener(
11355
11653
  'click', () => EXPERIMENT_MANAGER.parameter_modal.hide());
11356
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
+
11357
11661
  this.settings_modal = new ModalDialog('xp-settings');
11358
11662
  this.settings_modal.close.addEventListener(
11359
11663
  'click', () => EXPERIMENT_MANAGER.closeSettingsDimensions());
@@ -11374,6 +11678,26 @@ class GUIExperimentManager extends ExperimentManager {
11374
11678
  this.settings_dimension_modal.cancel.addEventListener(
11375
11679
  'click', () => EXPERIMENT_MANAGER.settings_dimension_modal.hide());
11376
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
+
11377
11701
  this.actor_dimension_modal = new ModalDialog('xp-actor-dimension');
11378
11702
  this.actor_dimension_modal.close.addEventListener(
11379
11703
  'click', () => EXPERIMENT_MANAGER.closeActorDimension());
@@ -11423,6 +11747,7 @@ class GUIExperimentManager extends ExperimentManager {
11423
11747
  this.selected_parameter = '';
11424
11748
  this.edited_selector_index = -1;
11425
11749
  this.edited_dimension_index = -1;
11750
+ this.edited_combi_selector_index = -1;
11426
11751
  this.color_scale = new ColorScale('no');
11427
11752
  this.designMode();
11428
11753
  }
@@ -11448,7 +11773,7 @@ class GUIExperimentManager extends ExperimentManager {
11448
11773
  for(let i = 0; i < MODEL.experiments.length; i++) {
11449
11774
  xtl.push(MODEL.experiments[i].title);
11450
11775
  }
11451
- xtl.sort();
11776
+ xtl.sort(ciCompare);
11452
11777
  for(let i = 0; i < xtl.length; i++) {
11453
11778
  const
11454
11779
  xi = MODEL.indexOfExperiment(xtl[i]),
@@ -11490,24 +11815,25 @@ class GUIExperimentManager extends ExperimentManager {
11490
11815
 
11491
11816
  updateParameters() {
11492
11817
  MODEL.inferDimensions();
11493
- let n = MODEL.dimensions.length,
11494
- canview = true;
11818
+ let canview = true;
11495
11819
  const
11496
11820
  dim_count = document.getElementById('experiment-dim-count'),
11497
11821
  combi_count = document.getElementById('experiment-combi-count'),
11498
11822
  header = document.getElementById('experiment-params-header'),
11499
11823
  x = this.selected_experiment;
11500
11824
  if(!x) {
11501
- dim_count.innerHTML = pluralS(n, ' data dimension') + ' in model';
11825
+ dim_count.innerHTML = pluralS(
11826
+ MODEL.dimensions.length, ' data dimension') + ' in model';
11502
11827
  combi_count.innerHTML = '';
11503
11828
  header.innerHTML = '(no experiment selected)';
11504
11829
  this.params_div.style.display = 'none';
11505
11830
  return;
11506
11831
  }
11507
11832
  x.updateActorDimension();
11508
- n += x.settings_dimensions.length +
11509
- x.actor_dimensions.length - x.dimensions.length;
11510
- 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');
11511
11837
  x.inferActualDimensions();
11512
11838
  x.inferCombinations();
11513
11839
  combi_count.innerHTML = pluralS(x.combinations.length, 'combination');
@@ -11525,11 +11851,10 @@ class GUIExperimentManager extends ExperimentManager {
11525
11851
  }
11526
11852
  document.getElementById('experiment-dim-table').innerHTML = tr.join('');
11527
11853
  // Add button must be enabled only if there still are unused dimensions
11528
- if(x.dimensions.length >= MODEL.dimensions.length +
11529
- x.settings_dimensions.length + x.actor_dimensions.length) {
11530
- document.getElementById('xp-d-add-btn').classList.add('v-disab');
11531
- } else {
11854
+ if(x.available_dimensions.length > 0) {
11532
11855
  document.getElementById('xp-d-add-btn').classList.remove('v-disab');
11856
+ } else {
11857
+ document.getElementById('xp-d-add-btn').classList.add('v-disab');
11533
11858
  }
11534
11859
  this.updateUpDownButtons();
11535
11860
  tr.length = 0;
@@ -11655,7 +11980,7 @@ class GUIExperimentManager extends ExperimentManager {
11655
11980
  for(let i = 0; i < x.variables.length; i++) {
11656
11981
  vl.push(x.variables[i].displayName);
11657
11982
  }
11658
- vl.sort();
11983
+ vl.sort(ciCompare);
11659
11984
  for(let i = 0; i < vl.length; i++) {
11660
11985
  ol.push(['<option value="', vl[i], '"',
11661
11986
  (vl[i] == x.selected_variable ? ' selected="selected"' : ''),
@@ -12250,6 +12575,61 @@ N = ${rr.N}, vector length = ${rr.vector.length}` : '')].join('');
12250
12575
  }
12251
12576
  }
12252
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
+
12253
12633
  editSettingsDimensions() {
12254
12634
  // Open dialog for editing model settings dimensions
12255
12635
  const x = this.selected_experiment, rows = [];
@@ -12299,7 +12679,7 @@ N = ${rr.N}, vector length = ${rr.vector.length}` : '')].join('');
12299
12679
  md.element('clear').innerHTML = clear;
12300
12680
  md.element('code').value = sel[0];
12301
12681
  md.element('string').value = sel[1];
12302
- md.show('string');
12682
+ md.show(sel[0] ? 'string' : 'code');
12303
12683
  }
12304
12684
 
12305
12685
  modifySettingsSelector() {
@@ -12350,10 +12730,7 @@ N = ${rr.N}, vector length = ${rr.vector.length}` : '')].join('');
12350
12730
  // NOTE: rename occurrence of code in dimension (should at most be 1)
12351
12731
  const oc = x.settings_selectors[this.edited_selector_index].split('|')[0];
12352
12732
  x.settings_selectors[this.edited_selector_index] = sel;
12353
- for(let i = 0; i < x.settings_dimensions.length; i++) {
12354
- const si = x.settings_dimensions[i].indexOf(oc);
12355
- if(si >= 0) x.settings_dimensions[i][si] = code;
12356
- }
12733
+ x.renameSelectorInDimensions(oc, code);
12357
12734
  }
12358
12735
  }
12359
12736
  md.hide();
@@ -12432,6 +12809,190 @@ N = ${rr.N}, vector length = ${rr.vector.length}` : '')].join('');
12432
12809
  this.editSettingsDimensions();
12433
12810
  }
12434
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
+
12435
12996
  editActorDimension() {
12436
12997
  // Open dialog for editing the actor dimension
12437
12998
  const x = this.selected_experiment, rows = [];
@@ -12655,22 +13216,10 @@ N = ${rr.N}, vector length = ${rr.vector.length}` : '')].join('');
12655
13216
  const ol = [];
12656
13217
  this.parameter_modal.element('type').innerHTML = type;
12657
13218
  if(type === 'dimension') {
12658
- // Compile a list of data dimensions and settings dimensions
12659
- // NOTE: slice to avoid adding settings dimensions to the data dimensions
12660
- const dl = MODEL.dimensions.slice();
12661
- for(let i = 0; i < x.settings_dimensions.length; i++) {
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
- }
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>`);
12674
13223
  }
12675
13224
  } else {
12676
13225
  for(let i = 0; i < this.suitable_charts.length; i++) {
@@ -12737,7 +13286,7 @@ N = ${rr.N}, vector length = ${rr.vector.length}` : '')].join('');
12737
13286
  if(x) {
12738
13287
  x.excluded_selectors = this.exclude.value.replace(
12739
13288
  /[\;\,]/g, ' ').trim().replace(
12740
- /[^a-zA-Z0-9\+\-\%\_\s]/g, '').split(/\s+/).join(' ');
13289
+ /[^a-zA-Z0-9\+\-\=\%\_\s]/g, '').split(/\s+/).join(' ');
12741
13290
  this.exclude.value = x.excluded_selectors;
12742
13291
  this.updateParameters();
12743
13292
  }
@@ -13384,7 +13933,7 @@ class DocumentationManager {
13384
13933
  }
13385
13934
  lis.push(`<li>${dn}</li>`);
13386
13935
  }
13387
- lis.sort();
13936
+ lis.sort(ciCompare);
13388
13937
  this.viewer.innerHTML = `<ul>${lis.join('')}</ul>`;
13389
13938
  }
13390
13939
  }
@@ -13413,7 +13962,7 @@ class DocumentationManager {
13413
13962
  for(let i = 0; i < iol.length; i++) {
13414
13963
  lis.push(`<li>${iol[i].displayName}</li>`);
13415
13964
  }
13416
- lis.sort();
13965
+ lis.sort(ciCompare);
13417
13966
  this.viewer.innerHTML = `<ul>${lis.join('')}</ul>`;
13418
13967
  }
13419
13968
  }
@@ -13731,7 +14280,7 @@ class Finder {
13731
14280
  }
13732
14281
  }
13733
14282
  }
13734
- enl.sort();
14283
+ enl.sort(ciCompare);
13735
14284
  }
13736
14285
  document.getElementById('finder-entity-imgs').innerHTML = imgs;
13737
14286
  let seid = 'etr';
@@ -14584,6 +15133,9 @@ class UndoStack {
14584
15133
  this.undoables.push(ue);
14585
15134
  // Update the GUI buttons
14586
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);
14587
15139
  //console.log('push ' + action);
14588
15140
  //console.log(UNDO_STACK);
14589
15141
  }
@@ -14880,6 +15432,8 @@ if (MODEL.focal_cluster === fc) {
14880
15432
  MODEL.focal_cluster.clearAllProcesses();
14881
15433
  UI.drawDiagram(MODEL);
14882
15434
  UI.updateButtons();
15435
+ // Update the Finder if needed
15436
+ if(ue.action !== 'move') FINDER.updateDialog();
14883
15437
  }
14884
15438
  //console.log('undo');
14885
15439
  //console.log(UNDO_STACK);
@@ -14931,6 +15485,7 @@ if (MODEL.focal_cluster === fc) {
14931
15485
  MODEL.focal_cluster.clearAllProcesses();
14932
15486
  UI.drawDiagram(MODEL);
14933
15487
  UI.updateButtons();
15488
+ if(re.action !== 'move') FINDER.updateDialog();
14934
15489
  }
14935
15490
  }
14936
15491
  } // END of class UndoStack