linny-r 1.3.4 → 1.4.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.
@@ -916,6 +916,8 @@ class Paper {
916
916
  for(let i = 0; i < fc.sub_clusters.length; i++) {
917
917
  fc.sub_clusters[i].clearHiddenIO();
918
918
  }
919
+ // NOTE: also ensure that notes will update their fields
920
+ fc.resetNoteFields();
919
921
  // Draw link arrows and constraints first, as all other entities are
920
922
  // slightly transparent so they cannot completely hide these lines
921
923
  for(let i = 0; i < fc.arrows.length; i++) {
@@ -953,6 +955,7 @@ class Paper {
953
955
  // Links and constraints are drawn separately, so do not draw those
954
956
  // contained in the selection
955
957
  if(!(obj instanceof Link || obj instanceof Constraint)) {
958
+ if(obj instanceof Note) obj.parsed = false;
956
959
  UI.drawObject(obj, dx, dy);
957
960
  }
958
961
  }
@@ -1519,6 +1522,7 @@ class Paper {
1519
1522
  } else if(mf[0] > 1) {
1520
1523
  // Multi-flow arrow with flow data computed
1521
1524
  let clr = this.palette.active_process;
1525
+ // Highlight if related process(es) are at upper bound
1522
1526
  if(mf[3]) ffill.fill = this.palette.at_process_ub_bar;
1523
1527
  s = VM.sig4Dig(mf[1]);
1524
1528
  bb = this.numberSize(s, 10, 700);
@@ -2828,6 +2832,21 @@ class GUIController extends Controller {
2828
2832
  constructor() {
2829
2833
  super();
2830
2834
  this.console = false;
2835
+ const
2836
+ ua = window.navigator.userAgent.toLowerCase(),
2837
+ browsers = [
2838
+ ['edg', 'Edge'],
2839
+ ['opr', 'Opera'],
2840
+ ['chrome', 'Chrome'],
2841
+ ['firefox', 'Firefox'],
2842
+ ['safari', 'Safari']];
2843
+ for(let i = 0; i < browsers.length; i++) {
2844
+ const b = browsers[i];
2845
+ if(ua.indexOf(b[0]) >= 0) {
2846
+ this.browser_name = b[1];
2847
+ break;
2848
+ }
2849
+ }
2831
2850
  // Display version number as clickable link (just below the Linny-R logo)
2832
2851
  this.version_number = LINNY_R_VERSION;
2833
2852
  this.version_div = document.getElementById('linny-r-version-number');
@@ -3484,8 +3503,7 @@ class GUIController extends Controller {
3484
3503
  if(VM.issue_index === -1) {
3485
3504
  VM.issue_index = 0;
3486
3505
  } else if(change) {
3487
- VM.issue_index += change;
3488
- setTimeout(() => UI.jumpToIssue(), 10);
3506
+ VM.issue_index = Math.min(VM.issue_index + change, count - 1);
3489
3507
  }
3490
3508
  nr.innerText = VM.issue_index + 1;
3491
3509
  if(VM.issue_index <= 0) {
@@ -3493,12 +3511,13 @@ class GUIController extends Controller {
3493
3511
  } else {
3494
3512
  prev.classList.remove('disab');
3495
3513
  }
3496
- if(this.issue_index >= count - 1) {
3514
+ if(VM.issue_index >= count - 1) {
3497
3515
  next.classList.add('disab');
3498
3516
  } else {
3499
3517
  next.classList.remove('disab');
3500
3518
  }
3501
3519
  panel.style.display = 'table-cell';
3520
+ if(change) UI.jumpToIssue();
3502
3521
  } else {
3503
3522
  panel.style.display = 'none';
3504
3523
  VM.issue_index = -1;
@@ -4770,7 +4789,7 @@ class GUIController extends Controller {
4770
4789
 
4771
4790
  updateExpressionInput(id, name, x) {
4772
4791
  // Updates expression object `x` if input field identified by `id`
4773
- // contains a well-formed expression; if error, focuses on the field
4792
+ // contains a well-formed expression. If error, focuses on the field
4774
4793
  // and shows the error while specifying the name of the field.
4775
4794
  const
4776
4795
  inp = document.getElementById(id),
@@ -4882,7 +4901,7 @@ class GUIController extends Controller {
4882
4901
  // Logs MB's of used heap memory to console (to detect memory leaks)
4883
4902
  // NOTE: this feature is supported only by Chrome
4884
4903
  if(msg) msg += ' -- ';
4885
- if(typeof performance.memory !== 'undefined') {
4904
+ if(performance.memory !== undefined) {
4886
4905
  console.log(msg + 'Allocated memory: ' + Math.round(
4887
4906
  performance.memory.usedJSHeapSize/1048576.0).toFixed(1) + ' MB');
4888
4907
  }
@@ -5006,14 +5025,19 @@ class GUIController extends Controller {
5006
5025
  an,
5007
5026
  md;
5008
5027
  if(type === 'note') {
5028
+
5009
5029
  md = this.modals.note;
5010
- const cx = new Expression(null, '', '');
5030
+ n = this.dbl_clicked_node;
5031
+ const
5032
+ editing = md.element('action').innerHTML === 'Edit',
5033
+ cx = new Expression(editing ? n : null, '', 'C');
5011
5034
  if(this.updateExpressionInput('note-C', 'note color', cx)) {
5012
- if(md.element('action').innerHTML === 'Edit') {
5035
+ if(editing) {
5013
5036
  n = this.dbl_clicked_node;
5014
5037
  this.dbl_clicked_node = null;
5015
5038
  UNDO_STACK.push('modify', n);
5016
5039
  n.contents = md.element('text').value;
5040
+ n.color.owner = n;
5017
5041
  n.color.text = md.element('C').value;
5018
5042
  n.color.compile();
5019
5043
  n.parsed = false;
@@ -5024,10 +5048,10 @@ class GUIController extends Controller {
5024
5048
  n.y = this.add_y;
5025
5049
  n.contents = md.element('text').value;
5026
5050
  n.color.text = md.element('C').value;
5027
- n.color.compile();
5028
5051
  n.parsed = false;
5029
5052
  n.resize();
5030
- UNDO_STACK.push('add', n);
5053
+ n.color.compile();
5054
+ UNDO_STACK.push('add', n);
5031
5055
  }
5032
5056
  }
5033
5057
  } else if(type === 'cluster') {
@@ -5242,7 +5266,8 @@ class GUIController extends Controller {
5242
5266
  if(xml) {
5243
5267
  window.localStorage.setItem('Linny-R-selection-XML', xml);
5244
5268
  this.updateButtons();
5245
- this.notify('Selection copied, but cannot be pasted yet -- Use Alt-C to clone');
5269
+ const bn = (this.browser_name ? ` of ${this.browser_name}` : '');
5270
+ this.notify('Selection copied to local storage' + bn);
5246
5271
  }
5247
5272
  }
5248
5273
 
@@ -5261,7 +5286,6 @@ class GUIController extends Controller {
5261
5286
 
5262
5287
  promptForMapping(mapping) {
5263
5288
  // Prompt user to specify name conflict resolution strategy
5264
- console.log('HERE prompt for mapping', mapping);
5265
5289
  const md = this.paste_modal;
5266
5290
  md.mapping = mapping;
5267
5291
  md.element('from-prefix').innerText = mapping.from_prefix || '';
@@ -5301,10 +5325,20 @@ class GUIController extends Controller {
5301
5325
  for(let i = 0; i < ft.length; i++) {
5302
5326
  const ti = mapping.from_to[ft[i]];
5303
5327
  if(ft[i] === ti) {
5304
- sl.push('<div class="paste-option"><span>', ft[i], '</span> ',
5305
- '<div class="paste-select"><select id="paste-ft-', i,
5306
- '" style="font-size: 12px"><option value="', ti, '">', ti,
5307
- '</option></select></div></div>');
5328
+ const elig = MODEL.eligibleFromToNodes(mapping.from_to_type[ti]);
5329
+ sl.push('<div class="paste-option"><span>', ft[i], '</span> ');
5330
+ if(elig.length) {
5331
+ sl.push('<div class="paste-select"><select id="paste-ft-', i,
5332
+ '" style="font-size: 12px">');
5333
+ for(let j = 0; j < elig.length; j++) {
5334
+ const dn = elig[j].displayName;
5335
+ sl.push('<option value="', dn, '">', dn, '</option>');
5336
+ }
5337
+ sl.push('</select></div>');
5338
+ } else {
5339
+ sl.push('<span><em>(no eligible node)</em></span');
5340
+ }
5341
+ sl.push('</div>');
5308
5342
  }
5309
5343
  }
5310
5344
  }
@@ -5330,6 +5364,12 @@ class GUIController extends Controller {
5330
5364
  const cn = md.element('selc-' + i).value.trim();
5331
5365
  if(this.validName(cn)) mapping.top_clusters[tc[i]] = cn;
5332
5366
  }
5367
+ for(let i = 0; i < ft.length; i++) if(mapping.from_to[ft[i]] === ft[i]) {
5368
+ const
5369
+ ftn = md.element('ft-' + i).value,
5370
+ fto = MODEL.objectByName(ftn);
5371
+ if(fto) mapping.from_to[ft[i]] = ftn;
5372
+ }
5333
5373
  this.pasteSelection(mapping);
5334
5374
  }
5335
5375
 
@@ -5338,7 +5378,6 @@ class GUIController extends Controller {
5338
5378
  // see whether PASTE would result in name conflicts, and if so,
5339
5379
  // open the name conflict resolution window
5340
5380
  let xml = window.localStorage.getItem('Linny-R-selection-XML');
5341
- console.log('HERE xml', xml);
5342
5381
  try {
5343
5382
  xml = parseXML(xml);
5344
5383
  } catch(e) {
@@ -5347,9 +5386,6 @@ console.log('HERE xml', xml);
5347
5386
  return;
5348
5387
  }
5349
5388
 
5350
- // For now, while still implementing:
5351
- this.notify('Paste not fully implemented yet -- WORK IN PROGRESS!');
5352
-
5353
5389
  const
5354
5390
  entities_node = childNodeByTag(xml, 'entities'),
5355
5391
  from_tos_node = childNodeByTag(xml, 'from-tos'),
@@ -5454,6 +5490,12 @@ console.log('HERE xml', xml);
5454
5490
  if(mapping.increment && nr) {
5455
5491
  return n.replace(new RegExp(nr + '$'), parseInt(nr) + 1);
5456
5492
  }
5493
+ if(mapping.top_clusters && mapping.top_clusters[n]) {
5494
+ return mapping.top_clusters[n];
5495
+ }
5496
+ if(mapping.from_to && mapping.from_to[n]) {
5497
+ return mapping.from_to[n];
5498
+ }
5457
5499
  // No mapping => return original name
5458
5500
  return n;
5459
5501
  }
@@ -5572,36 +5614,41 @@ console.log('HERE xml', xml);
5572
5614
  // Also prompt if FROM and/or TO nodes are not selected, and map to
5573
5615
  // existing entities
5574
5616
  if(from_tos_node.childNodes.length && !mapping.from_to) {
5575
- const ft_map = {};
5617
+ const
5618
+ ft_map = {},
5619
+ ft_type = {};
5576
5620
  for(let i = 0; i < from_tos_node.childNodes.length; i++) {
5577
5621
  const
5578
5622
  c = from_tos_node.childNodes[i],
5579
5623
  fn = fullName(c),
5580
5624
  mn = mappedName(fn);
5581
- if(MODEL.objectByName(mn)) ft_map[fn] = mn;
5625
+ if(MODEL.objectByName(mn)) {
5626
+ ft_map[fn] = mn;
5627
+ ft_type[fn] = (nodeParameterValue(c, 'is-data') === '1' ?
5628
+ 'Data' : nodeParameterValue(c, 'type'));
5629
+ }
5582
5630
  }
5583
5631
  // Prompt only for FROM/TO nodes that map to existing nodes
5584
5632
  if(Object.keys(ft_map).length) {
5585
5633
  mapping.from_to = ft_map;
5634
+ mapping.from_to_type = ft_type;
5586
5635
  this.promptForMapping(mapping);
5587
5636
  return;
5588
5637
  }
5589
5638
  }
5590
5639
 
5591
- // Only check for selected entities and from-to's; extra's should be
5640
+ // Only check for selected entities; from-to's and extra's should be
5592
5641
  // used if they exist, or should be created when copying to a different
5593
5642
  // model
5594
5643
  name_map.length = 0;
5595
5644
  nameConflicts(entities_node);
5596
- nameConflicts(from_tos_node);
5597
5645
  if(name_conflicts.length) {
5598
- UI.notify(pluralS(name_conflicts.length, 'name conflict'));
5599
- console.log('HERE name conflicts', name_conflicts);
5646
+ UI.warn(pluralS(name_conflicts.length, 'name conflict'));
5647
+ console.log('HERE name conflicts', name_conflicts, mapping);
5600
5648
  return;
5601
5649
  }
5602
5650
 
5603
5651
  // No conflicts => add all
5604
- console.log('HERE name map', name_map);
5605
5652
  for(let i = 0; i < extras_node.childNodes.length; i++) {
5606
5653
  addEntityFromNode(extras_node.childNodes[i]);
5607
5654
  }
@@ -5627,6 +5674,7 @@ console.log('HERE name map', name_map);
5627
5674
  // products are displayed as arrows instead of block arrows
5628
5675
  fc.clearAllProcesses();
5629
5676
  UI.drawDiagram(MODEL);
5677
+ this.paste_modal.hide();
5630
5678
  }
5631
5679
 
5632
5680
  //
@@ -5653,6 +5701,7 @@ console.log('HERE name map', name_map);
5653
5701
  this.setBox('settings-decimal-comma', model.decimal_comma);
5654
5702
  this.setBox('settings-align-to-grid', model.align_to_grid);
5655
5703
  this.setBox('settings-cost-prices', model.infer_cost_prices);
5704
+ this.setBox('settings-report-results', model.report_results);
5656
5705
  this.setBox('settings-block-arrows', model.show_block_arrows);
5657
5706
  md.show('name');
5658
5707
  }
@@ -5705,6 +5754,7 @@ console.log('HERE name map', name_map);
5705
5754
  if(!model.scale_units.hasOwnProperty(dsu)) model.addScaleUnit(dsu);
5706
5755
  model.default_unit = dsu;
5707
5756
  model.currency_unit = md.element('currency-unit').value.trim();
5757
+ model.report_results = UI.boxChecked('settings-report-results');
5708
5758
  model.encrypt = UI.boxChecked('settings-encrypt');
5709
5759
  model.decimal_comma = UI.boxChecked('settings-decimal-comma');
5710
5760
  // Some changes may necessitate redrawing the diagram
@@ -5766,6 +5816,8 @@ console.log('HERE name map', name_map);
5766
5816
  const md = this.modals.note;
5767
5817
  if(n) {
5768
5818
  md.element('action').innerHTML = 'Edit';
5819
+ const nr = n.number;
5820
+ md.element('number').innerHTML = (nr ? '#' + nr : '');
5769
5821
  md.element('text').value = n.contents;
5770
5822
  md.element('C').value = n.color.text;
5771
5823
  } else {
@@ -5787,6 +5839,10 @@ console.log('HERE name map', name_map);
5787
5839
  } else {
5788
5840
  md.element('actor').value = '';
5789
5841
  }
5842
+ const sim = p.similarNumberedEntities;
5843
+ if(sim.length) {
5844
+ console.log('HERE!', sim);
5845
+ }
5790
5846
  md.element('LB').value = p.lower_bound.text;
5791
5847
  md.element('UB').value = p.upper_bound.text;
5792
5848
  this.setEqualBounds('process', p.equal_bounds);
@@ -6017,7 +6073,7 @@ console.log('HERE name map', name_map);
6017
6073
  const
6018
6074
  from_process = l.from_node instanceof Process,
6019
6075
  to_process = l.to_node instanceof Process,
6020
- md = this.modals.link;
6076
+ md = this.modals.link;
6021
6077
  md.show();
6022
6078
  md.element('from-name').innerHTML = l.from_node.displayName;
6023
6079
  md.element('to-name').innerHTML = l.to_node.displayName;
@@ -6064,6 +6120,7 @@ console.log('HERE name map', name_map);
6064
6120
  md.element('output-soc').style.display = 'none';
6065
6121
  }
6066
6122
  }
6123
+ this.edited_object = l;
6067
6124
  if(alt) md.element(attr + '-x').dispatchEvent(new Event('click'));
6068
6125
  }
6069
6126
 
@@ -6108,7 +6165,7 @@ console.log('HERE name map', name_map);
6108
6165
  // @@TO DO: prepare for undo
6109
6166
  const
6110
6167
  md = this.modals.link,
6111
- l = this.on_link;
6168
+ l = this.edited_object;
6112
6169
  // Check whether all input fields are valid
6113
6170
  if(!this.updateExpressionInput('link-R', 'rate', l.relative_rate)) {
6114
6171
  return false;
@@ -6415,7 +6472,8 @@ class GUIMonitor {
6415
6472
  if(this.call_stack_shown) return;
6416
6473
  const
6417
6474
  csl = VM.call_stack.length,
6418
- err = VM.call_stack[csl - 1].vector[t],
6475
+ top = VM.call_stack[csl - 1],
6476
+ err = top.vector[t],
6419
6477
  // Make separate lists of variable names and their expressions
6420
6478
  vlist = [],
6421
6479
  xlist = [];
@@ -6480,6 +6538,9 @@ class GUIMonitor {
6480
6538
  tbl.push('<div style="color: gray; margin-top: 8px; font-size: 10px">',
6481
6539
  VM.out_of_bounds_msg.replace(VM.out_of_bounds_array, anc), '</div>');
6482
6540
  }
6541
+ // Dump the code for the last expression to the console
6542
+ console.log('Code for', top.text, top.code);
6543
+ // Show the call stack dialog
6483
6544
  document.getElementById('call-stack-table').innerHTML = tbl.join('');
6484
6545
  document.getElementById('call-stack-modal').style.display = 'block';
6485
6546
  this.call_stack_shown = true;
@@ -7244,10 +7305,12 @@ NOTE: Grouping groups results in a single group, e.g., (1;2);(3;4;5) evaluates a
7244
7305
  if(this.edited_input_id) {
7245
7306
  document.getElementById(this.edited_input_id).value = xp.expr;
7246
7307
  // NOTE: entity properties must be exogenous parameters
7247
- if(UI.edited_object && xp.is_level_based) {
7248
- UI.warn(['Expression for ', this.property.innerHTML,
7249
- 'of <strong>', UI.edited_object.displayName,
7250
- '</strong> contains a solution-dependent variable'].join(''));
7308
+ const eo = UI.edited_object;
7309
+ if(eo && xp.is_level_based &&
7310
+ !(eo instanceof Dataset || eo instanceof Note)) {
7311
+ UI.warn(['Expression for', this.property.innerHTML,
7312
+ 'of<strong>', eo.displayName,
7313
+ '</strong>contains a solution-dependent variable'].join(' '));
7251
7314
  }
7252
7315
  this.edited_input_id = '';
7253
7316
  } else if(DATASET_MANAGER.edited_expression) {
@@ -8743,6 +8806,7 @@ class ConstraintEditor {
8743
8806
  // NOTE: this could be expanded to apply to the selected BL only
8744
8807
  UI.setBox('ce-no-slack', this.constraint.no_slack);
8745
8808
  // NOTE: share of cost can only be transferred between two processes
8809
+ // @@TO DO: CHECK WHETHER THIS LIMITATION IS VALID -- for now, allow both
8746
8810
  if(true||this.from_node instanceof Process && this.from_node instanceof Process) {
8747
8811
  this.soc_direct.value = this.constraint.soc_direction;
8748
8812
  // NOTE: share of cost is input as a percentage
@@ -9894,6 +9958,7 @@ class GUIDatasetManager extends DatasetManager {
9894
9958
  ps = pid.split(UI.PREFIXER),
9895
9959
  pps = prev_id.split(UI.PREFIXER),
9896
9960
  pn = pref_names[pid],
9961
+ pns = pn.join(UI.PREFIXER),
9897
9962
  lpl = [];
9898
9963
  let lindent = 0;
9899
9964
  // Ignore identical leading prefixes
@@ -9908,8 +9973,9 @@ class GUIDatasetManager extends DatasetManager {
9908
9973
  lpl.push(ps.shift());
9909
9974
  lindent++;
9910
9975
  const lpid = lpl.join(UI.PREFIXER);
9911
- dl.push(['<tr data-prefix="', lpid, '" class="dataset',
9912
- '" onclick="DATASET_MANAGER.selectPrefixRow(event);"><td>',
9976
+ dl.push(['<tr data-prefix="', lpid,
9977
+ '" data-prefix-name="', pns, '" class="dataset"',
9978
+ 'onclick="DATASET_MANAGER.selectPrefixRow(event);"><td>',
9913
9979
  // NOTE: data-prefix="x" signals that this is an extra row
9914
9980
  (lindent > 0 ?
9915
9981
  '<div data-prefix="x" style="width: ' + lindent * indent_px +
@@ -10022,6 +10088,7 @@ class GUIDatasetManager extends DatasetManager {
10022
10088
  for(let i = 0; i < msl.length; i++) {
10023
10089
  const
10024
10090
  m = sd.modifiers[UI.nameToID(msl[i])],
10091
+ wild = m.hasWildcards,
10025
10092
  defsel = (m.selector === sd.default_selector),
10026
10093
  clk = '" onclick="DATASET_MANAGER.selectModifier(event, \'' +
10027
10094
  m.selector + '\'';
@@ -10029,13 +10096,14 @@ class GUIDatasetManager extends DatasetManager {
10029
10096
  ml.push(['<tr id="dsmtr', i, '" class="dataset-modif',
10030
10097
  (m === sm ? ' sel-set' : ''),
10031
10098
  '"><td class="dataset-selector',
10032
- (m.hasWildcards ? ' wildcard' : ''),
10099
+ (wild ? ' wildcard' : ''),
10033
10100
  '" title="Shift-click to ', (defsel ? 'clear' : 'set as'),
10034
10101
  ' default modifier',
10035
10102
  clk, ', false);">',
10036
10103
  (defsel ? '<img src="images/solve.png" style="height: 14px;' +
10037
10104
  ' width: 14px; margin: 0 1px -3px -1px;">' : ''),
10038
- m.selector, '</td><td class="dataset-expression',
10105
+ (wild ? wildcardFormat(m.selector, true) : m.selector),
10106
+ '</td><td class="dataset-expression',
10039
10107
  clk, ');">', m.expression.text, '</td></tr>'].join(''));
10040
10108
  }
10041
10109
  this.modifier_table.innerHTML = ml.join('');
@@ -10138,24 +10206,15 @@ class GUIDatasetManager extends DatasetManager {
10138
10206
  }
10139
10207
  } else {
10140
10208
  this.selected_modifier = null;
10141
- }
10209
+ }
10142
10210
  this.updateModifiers();
10143
10211
  }
10144
10212
 
10145
10213
  get selectedPrefix() {
10146
10214
  // Returns the selected prefix (with its trailing colon-space)
10147
- let prefix = '',
10148
- tr = this.selected_prefix_row;
10149
- while(tr) {
10150
- const td = tr.firstElementChild;
10151
- if(td && td.firstElementChild.dataset.prefix === 'x') {
10152
- prefix = td.lastChild.textContent + UI.PREFIXER + prefix;
10153
- tr = tr.previousSibling;
10154
- } else {
10155
- tr = null;
10156
- }
10157
- }
10158
- return prefix;
10215
+ const tr = this.selected_prefix_row;
10216
+ if(tr && tr.dataset.prefixName) return tr.dataset.prefixName + UI.PREFIXER;
10217
+ return '';
10159
10218
  }
10160
10219
 
10161
10220
  promptForDataset(shift=false) {
@@ -10164,9 +10223,7 @@ class GUIDatasetManager extends DatasetManager {
10164
10223
  let prefix = '';
10165
10224
  if(shift) {
10166
10225
  if(this.selected_dataset) {
10167
- const p = UI.prefixesAndName(this.selected_dataset.name);
10168
- p[p.length - 1] = '';
10169
- prefix = p.join(UI.PREFIXER);
10226
+ prefix = UI.completePrefix(this.selected_dataset.name);
10170
10227
  } else if(this.selected_prefix) {
10171
10228
  prefix = this.selectedPrefix;
10172
10229
  }
@@ -10181,6 +10238,7 @@ class GUIDatasetManager extends DatasetManager {
10181
10238
  if(d) {
10182
10239
  this.new_modal.hide();
10183
10240
  this.selected_dataset = d;
10241
+ this.focal_table = this.dataset_table;
10184
10242
  this.updateDialog();
10185
10243
  }
10186
10244
  }
@@ -10220,12 +10278,14 @@ class GUIDatasetManager extends DatasetManager {
10220
10278
  let e = this.rename_modal.element('name'),
10221
10279
  prefix = e.value.trim();
10222
10280
  e.focus();
10281
+ // Trim trailing colon if user entered it
10223
10282
  while(prefix.endsWith(':')) prefix = prefix.slice(0, -1);
10224
10283
  // NOTE: prefix may be empty string, but otherwise should be a valid name
10225
10284
  if(prefix && !UI.validName(prefix)) {
10226
10285
  UI.warn('Invalid prefix');
10227
10286
  return;
10228
10287
  }
10288
+ // Now add the colon-plus-space prefix separator
10229
10289
  prefix += UI.PREFIXER;
10230
10290
  const
10231
10291
  oldpref = this.selectedPrefix,
@@ -10245,7 +10305,7 @@ class GUIDatasetManager extends DatasetManager {
10245
10305
  if(MODEL.datasets[nk]) nc++;
10246
10306
  }
10247
10307
  if(nc) {
10248
- UI.warn('Renaming ' + pluralS(dsl.length, 'dataset') +
10308
+ UI.warn('Renaming ' + pluralS(dsl.length, 'dataset').toLowerCase() +
10249
10309
  ' would cause ' + pluralS(nc, 'name conflict'));
10250
10310
  return;
10251
10311
  }
@@ -10258,7 +10318,7 @@ class GUIDatasetManager extends DatasetManager {
10258
10318
  const d = MODEL.datasets[dsl[i]];
10259
10319
  d.rename(d.displayName.replace(oldpref, prefix), false);
10260
10320
  }
10261
- let msg = 'Renamed ' + pluralS(dsl.length, 'dataset');
10321
+ let msg = 'Renamed ' + pluralS(dsl.length, 'dataset').toLowerCase();
10262
10322
  if(MODEL.variable_count) msg += ', and updated ' +
10263
10323
  pluralS(MODEL.variable_count, 'variable') + ' in ' +
10264
10324
  pluralS(MODEL.expression_count, 'expression');
@@ -10379,14 +10439,14 @@ class GUIDatasetManager extends DatasetManager {
10379
10439
  renameModifier() {
10380
10440
  if(!this.selected_modifier) return;
10381
10441
  const
10382
- hw = this.selected_modifier.hasWildcards,
10442
+ wild = this.selected_modifier.hasWildcards,
10383
10443
  sel = this.rename_selector_modal.element('name').value,
10384
10444
  // NOTE: normal dataset selector, so remove all invalid characters
10385
- clean_sel = sel.replace(/[^a-zA-z0-9\%\+\-]/g, ''),
10445
+ clean_sel = sel.replace(/[^a-zA-z0-9\%\+\-\?\*]/g, ''),
10386
10446
  // Keep track of old name
10387
10447
  oldm = this.selected_modifier,
10388
10448
  // NOTE: addModifier returns existing one if selector not changed
10389
- m = this.selected_dataset.addModifier(sel);
10449
+ m = this.selected_dataset.addModifier(clean_sel);
10390
10450
  // NULL can result when new name is invalid
10391
10451
  if(!m) return;
10392
10452
  // If selected modifier was the dataset default selector, update it
@@ -10398,11 +10458,12 @@ class GUIDatasetManager extends DatasetManager {
10398
10458
  if(m === oldm) {
10399
10459
  m.selector = clean_sel;
10400
10460
  this.updateDialog();
10461
+ this.rename_selector_modal.hide();
10401
10462
  return;
10402
10463
  }
10403
10464
  // Rest is needed only when a new modifier has been added
10404
10465
  m.expression = oldm.expression;
10405
- if(hw) {
10466
+ if(wild) {
10406
10467
  // Wildcard selector means: recompile the modifier expression
10407
10468
  m.expression.attribute = m.selector;
10408
10469
  m.expression.compile();
@@ -10410,11 +10471,10 @@ class GUIDatasetManager extends DatasetManager {
10410
10471
  this.deleteModifier();
10411
10472
  this.selected_modifier = m;
10412
10473
  // Update all chartvariables referencing this dataset + old selector
10413
- const vl = MODEL.datasetChartVariables;
10474
+ const vl = MODEL.datasetVariables;
10414
10475
  let cv_cnt = 0;
10415
10476
  for(let i = 0; i < vl.length; i++) {
10416
- if(v.object === this.selected_dataset &&
10417
- v.attribute === oldm.selector) {
10477
+ if(v.object === this.selected_dataset && v.attribute === oldm.selector) {
10418
10478
  v.attribute = m.selector;
10419
10479
  cv_cnt++;
10420
10480
  }
@@ -10818,6 +10878,7 @@ class EquationManager {
10818
10878
  for(let i = 0; i < msl.length; i++) {
10819
10879
  const
10820
10880
  m = ed.modifiers[UI.nameToID(msl[i])],
10881
+ wild = (m.selector.indexOf('??') >= 0),
10821
10882
  clk = '" onclick="EQUATION_MANAGER.selectModifier(event, \'' +
10822
10883
  m.selector + '\'';
10823
10884
  if(m === sm) smid += i;
@@ -10825,8 +10886,9 @@ class EquationManager {
10825
10886
  (m === sm ? ' sel-set' : ''),
10826
10887
  '"><td class="equation-selector',
10827
10888
  (m.expression.isStatic ? '' : ' it'),
10828
- clk, ', false);">',
10829
- m.selector, '</td><td class="equation-expression',
10889
+ (wild ? ' wildcard' : ''), clk, ', false);">',
10890
+ (wild ? wildcardFormat(m.selector) : m.selector),
10891
+ '</td><td class="equation-expression',
10830
10892
  clk, ');">', m.expression.text, '</td></tr>'].join(''));
10831
10893
  }
10832
10894
  this.table.innerHTML = ml.join('');
@@ -10956,8 +11018,7 @@ class EquationManager {
10956
11018
  const c = MODEL.charts[i];
10957
11019
  for(let j = 0; j < c.variables.length; j++) {
10958
11020
  const v = c.variables[j];
10959
- if(v.object === MODEL.equations_dataset &&
10960
- v.attribute === olds) {
11021
+ if(v.object === MODEL.equations_dataset && v.attribute === olds) {
10961
11022
  v.attribute = m.selector;
10962
11023
  cv_cnt++;
10963
11024
  }
@@ -15932,8 +15993,8 @@ class GUIReceiver {
15932
15993
  }
15933
15994
 
15934
15995
  log(msg) {
15935
- // Logs a message displayed on the status line while solving
15936
- if(this.active) {
15996
+ // Logs a message displayed on the status line while solving.
15997
+ if(this.active || MODEL.report_results) {
15937
15998
  if(!msg.startsWith('[')) {
15938
15999
  const
15939
16000
  d = new Date(),
@@ -16076,21 +16137,34 @@ class GUIReceiver {
16076
16137
  report() {
16077
16138
  // Posts the run results to the local server, or signals an error
16078
16139
  let form,
16079
- run = '';
16140
+ run = '',
16141
+ path = this.channel,
16142
+ file = this.file_name;
16080
16143
  // NOTE: Always set `solving` to FALSE
16081
16144
  this.solving = false;
16082
- if(this.experiment){
16145
+ // NOTE: When reporting receiver while is not active, report the
16146
+ // results of the running experiment.
16147
+ if(this.experiment || !this.active) {
16083
16148
  if(MODEL.running_experiment) {
16084
16149
  run = MODEL.running_experiment.active_combination_index;
16085
16150
  this.log(`Reporting: ${this.file_name} (run #${run})`);
16086
16151
  }
16087
16152
  }
16153
+ // NOTE: If receiver is not active, path and file must be set.
16154
+ if(!this.active) {
16155
+ path = 'user/reports';
16156
+ // NOTE: The @ will be replaced by the run number, so that that
16157
+ // number precedes the clock time. The @ will be unique because
16158
+ // `asFileName()` replaces special characters by underscores.
16159
+ file = REPOSITORY_BROWSER.asFileName(MODEL.name || 'model') +
16160
+ '@-' + compactClockTime();
16161
+ }
16088
16162
  if(MODEL.solved && !VM.halted) {
16089
16163
  // Normal execution termination => report results
16090
16164
  const od = MODEL.outputData;
16091
16165
  form = {
16092
- path: this.channel,
16093
- file: this.file_name,
16166
+ path: path,
16167
+ file: file,
16094
16168
  action: 'report',
16095
16169
  run: run,
16096
16170
  data: od[0],
@@ -16119,10 +16193,11 @@ class GUIReceiver {
16119
16193
  .then((data) => {
16120
16194
  // For experiments, only display server response if warning or error
16121
16195
  UI.postResponseOK(data, !RECEIVER.experiment);
16122
- // If execution completed, perform the call-back action
16196
+ // If execution completed, perform the call-back action if the
16197
+ // receiver is active (so not when auto-reporting a run).
16123
16198
  // NOTE: for experiments, call-back is performed upon completion by
16124
- // the Experiment Manager
16125
- if(!RECEIVER.experiment) RECEIVER.callBack();
16199
+ // the Experiment Manager.
16200
+ if(RECEIVER.active && !RECEIVER.experiment) RECEIVER.callBack();
16126
16201
  })
16127
16202
  .catch(() => UI.warn(UI.WARNING.NO_CONNECTION, err));
16128
16203
  }