linny-r 1.1.23 → 1.2.1

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