linny-r 1.3.3 → 1.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -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),
@@ -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 || '';
@@ -5273,21 +5297,52 @@ class GUIController extends Controller {
5273
5297
  md.element('actor').value = mapping.actor || '';
5274
5298
  md.element('prefix').value = mapping.prefix || '';
5275
5299
  const
5276
- ft = Object.keys(mapping.from_to).sort(ciCompare),
5300
+ tc = (mapping.top_clusters ?
5301
+ Object.keys(mapping.top_clusters).sort(ciCompare) : []),
5302
+ ft = (mapping.from_to ?
5303
+ Object.keys(mapping.from_to).sort(ciCompare) : []),
5277
5304
  sl = [];
5305
+ if(tc.length) {
5306
+ sl.push('<div style="font-weight: bold; margin:4px 2px 2px 2px">',
5307
+ 'Names for top-level clusters:</div>');
5308
+ // Add text inputs for selected cluster nodes
5309
+ for(let i = 0; i < tc.length; i++) {
5310
+ const
5311
+ ti = mapping.top_clusters[tc[i]],
5312
+ state = (ti === tc[i] ? 'color: #e09000; ' :
5313
+ this.validName(ti) ? 'color: #0000c0; ' :
5314
+ 'font-style: italic; color: red; ');
5315
+ sl.push('<div class="paste-option"><span>', tc[i], '</span> ',
5316
+ '<div class="paste-select"><input id="paste-selc-', i,
5317
+ '" type="text" style="', state, 'font-size: 12px" value="',
5318
+ ti, '"></div></div>');
5319
+ }
5320
+ }
5278
5321
  if(ft.length) {
5322
+ sl.push('<div style="font-weight: bold; margin:4px 2px 2px 2px">',
5323
+ 'Mapping of nodes to link from/to:</div>');
5279
5324
  // Add selectors for unresolved FROM/TO nodes
5280
5325
  for(let i = 0; i < ft.length; i++) {
5281
5326
  const ti = mapping.from_to[ft[i]];
5282
5327
  if(ft[i] === ti) {
5283
- sl.push('<div class="paste-option"><span>', ft[i], '</span> ',
5284
- '<div class="paste-select"><select id="paste-ft-', i,
5285
- '" style="font-size: 12px"><option value="', ti, '">', ti,
5286
- '</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>');
5287
5342
  }
5288
5343
  }
5289
- md.element('scroll-area').innerHTML = sl.join('');
5290
5344
  }
5345
+ md.element('scroll-area').innerHTML = sl.join('');
5291
5346
  // Open dialog, which will call pasteSelection(...) on OK
5292
5347
  this.paste_modal.show();
5293
5348
  }
@@ -5297,10 +5352,24 @@ class GUIController extends Controller {
5297
5352
  // proceeds to paste
5298
5353
  const
5299
5354
  md = this.paste_modal,
5300
- mapping = Object.assign(md.mapping, {});
5355
+ mapping = Object.assign(md.mapping, {}),
5356
+ tc = (mapping.top_clusters ?
5357
+ Object.keys(mapping.top_clusters).sort(ciCompare) : []),
5358
+ ft = (mapping.from_to ?
5359
+ Object.keys(mapping.from_to).sort(ciCompare) : []);
5301
5360
  mapping.actor = md.element('actor').value;
5302
5361
  mapping.prefix = md.element('prefix').value.trim();
5303
5362
  mapping.increment = true;
5363
+ for(let i = 0; i < tc.length; i++) {
5364
+ const cn = md.element('selc-' + i).value.trim();
5365
+ if(this.validName(cn)) mapping.top_clusters[tc[i]] = cn;
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
+ }
5304
5373
  this.pasteSelection(mapping);
5305
5374
  }
5306
5375
 
@@ -5309,7 +5378,6 @@ class GUIController extends Controller {
5309
5378
  // see whether PASTE would result in name conflicts, and if so,
5310
5379
  // open the name conflict resolution window
5311
5380
  let xml = window.localStorage.getItem('Linny-R-selection-XML');
5312
- console.log('HERE xml', xml);
5313
5381
  try {
5314
5382
  xml = parseXML(xml);
5315
5383
  } catch(e) {
@@ -5318,13 +5386,11 @@ console.log('HERE xml', xml);
5318
5386
  return;
5319
5387
  }
5320
5388
 
5321
- // For now, while still implementing:
5322
- this.notify('Paste not fully implemented yet -- WORK IN PROGRESS!');
5323
-
5324
5389
  const
5325
5390
  entities_node = childNodeByTag(xml, 'entities'),
5326
5391
  from_tos_node = childNodeByTag(xml, 'from-tos'),
5327
5392
  extras_node = childNodeByTag(xml, 'extras'),
5393
+ selc_node = childNodeByTag(xml, 'selected-clusters'),
5328
5394
  selection_node = childNodeByTag(xml, 'selection'),
5329
5395
  actor_names = [],
5330
5396
  new_entities = [],
@@ -5335,7 +5401,7 @@ console.log('HERE xml', xml);
5335
5401
 
5336
5402
  function fullName(node) {
5337
5403
  // Returns full entity name inferred from XML node data
5338
- if(node.nodeName === 'from-to') {
5404
+ if(node.nodeName === 'from-to' || node.nodeName === 'selc') {
5339
5405
  const
5340
5406
  n = xmlDecoded(nodeParameterValue(node, 'name')),
5341
5407
  an = xmlDecoded(nodeParameterValue(node, 'actor-name'));
@@ -5424,6 +5490,12 @@ console.log('HERE xml', xml);
5424
5490
  if(mapping.increment && nr) {
5425
5491
  return n.replace(new RegExp(nr + '$'), parseInt(nr) + 1);
5426
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
+ }
5427
5499
  // No mapping => return original name
5428
5500
  return n;
5429
5501
  }
@@ -5525,42 +5597,58 @@ console.log('HERE xml', xml);
5525
5597
  if(parseInt(mts) === MODEL.time_created.getTime() &&
5526
5598
  ca === fca && mapping.from_prefix === mapping.to_prefix &&
5527
5599
  !(mapping.prefix || mapping.actor || mapping.increment)) {
5600
+ // Prompt for names of selected cluster nodes
5601
+ if(selc_node.childNodes.length && !mapping.prefix) {
5602
+ mapping.top_clusters = {};
5603
+ for(let i = 0; i < selc_node.childNodes.length; i++) {
5604
+ const
5605
+ c = selc_node.childNodes[i],
5606
+ fn = fullName(c),
5607
+ mn = mappedName(fn);
5608
+ mapping.top_clusters[fn] = mn;
5609
+ }
5610
+ }
5528
5611
  this.promptForMapping(mapping);
5529
5612
  return;
5530
5613
  }
5531
5614
  // Also prompt if FROM and/or TO nodes are not selected, and map to
5532
5615
  // existing entities
5533
5616
  if(from_tos_node.childNodes.length && !mapping.from_to) {
5534
- const ft_map = {};
5617
+ const
5618
+ ft_map = {},
5619
+ ft_type = {};
5535
5620
  for(let i = 0; i < from_tos_node.childNodes.length; i++) {
5536
5621
  const
5537
5622
  c = from_tos_node.childNodes[i],
5538
5623
  fn = fullName(c),
5539
5624
  mn = mappedName(fn);
5540
- 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
+ }
5541
5630
  }
5542
5631
  // Prompt only for FROM/TO nodes that map to existing nodes
5543
5632
  if(Object.keys(ft_map).length) {
5544
5633
  mapping.from_to = ft_map;
5634
+ mapping.from_to_type = ft_type;
5545
5635
  this.promptForMapping(mapping);
5546
5636
  return;
5547
5637
  }
5548
5638
  }
5549
5639
 
5550
- // 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
5551
5641
  // used if they exist, or should be created when copying to a different
5552
5642
  // model
5553
5643
  name_map.length = 0;
5554
5644
  nameConflicts(entities_node);
5555
- nameConflicts(from_tos_node);
5556
5645
  if(name_conflicts.length) {
5557
- UI.notify(pluralS(name_conflicts.length, 'name conflict'));
5558
- 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);
5559
5648
  return;
5560
5649
  }
5561
5650
 
5562
5651
  // No conflicts => add all
5563
- console.log('HERE name map', name_map);
5564
5652
  for(let i = 0; i < extras_node.childNodes.length; i++) {
5565
5653
  addEntityFromNode(extras_node.childNodes[i]);
5566
5654
  }
@@ -5586,6 +5674,7 @@ console.log('HERE name map', name_map);
5586
5674
  // products are displayed as arrows instead of block arrows
5587
5675
  fc.clearAllProcesses();
5588
5676
  UI.drawDiagram(MODEL);
5677
+ this.paste_modal.hide();
5589
5678
  }
5590
5679
 
5591
5680
  //
@@ -5725,6 +5814,8 @@ console.log('HERE name map', name_map);
5725
5814
  const md = this.modals.note;
5726
5815
  if(n) {
5727
5816
  md.element('action').innerHTML = 'Edit';
5817
+ const nr = n.number;
5818
+ md.element('number').innerHTML = (nr ? '#' + nr : '');
5728
5819
  md.element('text').value = n.contents;
5729
5820
  md.element('C').value = n.color.text;
5730
5821
  } else {
@@ -5746,6 +5837,10 @@ console.log('HERE name map', name_map);
5746
5837
  } else {
5747
5838
  md.element('actor').value = '';
5748
5839
  }
5840
+ const sim = p.similarNumberedEntities;
5841
+ if(sim.length) {
5842
+ console.log('HERE!', sim);
5843
+ }
5749
5844
  md.element('LB').value = p.lower_bound.text;
5750
5845
  md.element('UB').value = p.upper_bound.text;
5751
5846
  this.setEqualBounds('process', p.equal_bounds);
@@ -5976,7 +6071,7 @@ console.log('HERE name map', name_map);
5976
6071
  const
5977
6072
  from_process = l.from_node instanceof Process,
5978
6073
  to_process = l.to_node instanceof Process,
5979
- md = this.modals.link;
6074
+ md = this.modals.link;
5980
6075
  md.show();
5981
6076
  md.element('from-name').innerHTML = l.from_node.displayName;
5982
6077
  md.element('to-name').innerHTML = l.to_node.displayName;
@@ -6023,6 +6118,7 @@ console.log('HERE name map', name_map);
6023
6118
  md.element('output-soc').style.display = 'none';
6024
6119
  }
6025
6120
  }
6121
+ this.edited_object = l;
6026
6122
  if(alt) md.element(attr + '-x').dispatchEvent(new Event('click'));
6027
6123
  }
6028
6124
 
@@ -6067,7 +6163,7 @@ console.log('HERE name map', name_map);
6067
6163
  // @@TO DO: prepare for undo
6068
6164
  const
6069
6165
  md = this.modals.link,
6070
- l = this.on_link;
6166
+ l = this.edited_object;
6071
6167
  // Check whether all input fields are valid
6072
6168
  if(!this.updateExpressionInput('link-R', 'rate', l.relative_rate)) {
6073
6169
  return false;
@@ -6374,7 +6470,8 @@ class GUIMonitor {
6374
6470
  if(this.call_stack_shown) return;
6375
6471
  const
6376
6472
  csl = VM.call_stack.length,
6377
- err = VM.call_stack[csl - 1].vector[t],
6473
+ top = VM.call_stack[csl - 1],
6474
+ err = top.vector[t],
6378
6475
  // Make separate lists of variable names and their expressions
6379
6476
  vlist = [],
6380
6477
  xlist = [];
@@ -6439,6 +6536,9 @@ class GUIMonitor {
6439
6536
  tbl.push('<div style="color: gray; margin-top: 8px; font-size: 10px">',
6440
6537
  VM.out_of_bounds_msg.replace(VM.out_of_bounds_array, anc), '</div>');
6441
6538
  }
6539
+ // Dump the code for the last expression to the console
6540
+ console.log('Code for', top.text, top.code);
6541
+ // Show the call stack dialog
6442
6542
  document.getElementById('call-stack-table').innerHTML = tbl.join('');
6443
6543
  document.getElementById('call-stack-modal').style.display = 'block';
6444
6544
  this.call_stack_shown = true;
@@ -6525,11 +6625,12 @@ class GUIMonitor {
6525
6625
  })
6526
6626
  .then((data) => {
6527
6627
  try {
6528
- const jsr = JSON.parse(data);
6529
- if(jsr.solver !== VM.solver_name) {
6530
- UI.notify(`Solver on ${jsr.server} is ${jsr.solver}`);
6531
- }
6628
+ const
6629
+ jsr = JSON.parse(data),
6630
+ svr = `Solver on ${jsr.server} is ${jsr.solver}`;
6631
+ if(jsr.solver !== VM.solver_name) UI.notify(svr);
6532
6632
  VM.solver_name = jsr.solver;
6633
+ document.getElementById('host-logo').title = svr;
6533
6634
  } catch(err) {
6534
6635
  console.log(err, data);
6535
6636
  UI.alert('ERROR: Unexpected data from server: ' +
@@ -7202,10 +7303,12 @@ NOTE: Grouping groups results in a single group, e.g., (1;2);(3;4;5) evaluates a
7202
7303
  if(this.edited_input_id) {
7203
7304
  document.getElementById(this.edited_input_id).value = xp.expr;
7204
7305
  // NOTE: entity properties must be exogenous parameters
7205
- if(UI.edited_object && xp.is_level_based) {
7206
- UI.warn(['Expression for ', this.property.innerHTML,
7207
- 'of <strong>', UI.edited_object.displayName,
7208
- '</strong> contains a solution-dependent variable'].join(''));
7306
+ const eo = UI.edited_object;
7307
+ if(eo && xp.is_level_based &&
7308
+ !(eo instanceof Dataset || eo instanceof Note)) {
7309
+ UI.warn(['Expression for', this.property.innerHTML,
7310
+ 'of<strong>', eo.displayName,
7311
+ '</strong>contains a solution-dependent variable'].join(' '));
7209
7312
  }
7210
7313
  this.edited_input_id = '';
7211
7314
  } else if(DATASET_MANAGER.edited_expression) {
@@ -8701,6 +8804,7 @@ class ConstraintEditor {
8701
8804
  // NOTE: this could be expanded to apply to the selected BL only
8702
8805
  UI.setBox('ce-no-slack', this.constraint.no_slack);
8703
8806
  // NOTE: share of cost can only be transferred between two processes
8807
+ // @@TO DO: CHECK WHETHER THIS LIMITATION IS VALID -- for now, allow both
8704
8808
  if(true||this.from_node instanceof Process && this.from_node instanceof Process) {
8705
8809
  this.soc_direct.value = this.constraint.soc_direction;
8706
8810
  // NOTE: share of cost is input as a percentage
@@ -9852,6 +9956,7 @@ class GUIDatasetManager extends DatasetManager {
9852
9956
  ps = pid.split(UI.PREFIXER),
9853
9957
  pps = prev_id.split(UI.PREFIXER),
9854
9958
  pn = pref_names[pid],
9959
+ pns = pn.join(UI.PREFIXER),
9855
9960
  lpl = [];
9856
9961
  let lindent = 0;
9857
9962
  // Ignore identical leading prefixes
@@ -9866,8 +9971,9 @@ class GUIDatasetManager extends DatasetManager {
9866
9971
  lpl.push(ps.shift());
9867
9972
  lindent++;
9868
9973
  const lpid = lpl.join(UI.PREFIXER);
9869
- dl.push(['<tr data-prefix="', lpid, '" class="dataset',
9870
- '" onclick="DATASET_MANAGER.selectPrefixRow(event);"><td>',
9974
+ dl.push(['<tr data-prefix="', lpid,
9975
+ '" data-prefix-name="', pns, '" class="dataset"',
9976
+ 'onclick="DATASET_MANAGER.selectPrefixRow(event);"><td>',
9871
9977
  // NOTE: data-prefix="x" signals that this is an extra row
9872
9978
  (lindent > 0 ?
9873
9979
  '<div data-prefix="x" style="width: ' + lindent * indent_px +
@@ -9980,6 +10086,7 @@ class GUIDatasetManager extends DatasetManager {
9980
10086
  for(let i = 0; i < msl.length; i++) {
9981
10087
  const
9982
10088
  m = sd.modifiers[UI.nameToID(msl[i])],
10089
+ wild = m.hasWildcards,
9983
10090
  defsel = (m.selector === sd.default_selector),
9984
10091
  clk = '" onclick="DATASET_MANAGER.selectModifier(event, \'' +
9985
10092
  m.selector + '\'';
@@ -9987,13 +10094,14 @@ class GUIDatasetManager extends DatasetManager {
9987
10094
  ml.push(['<tr id="dsmtr', i, '" class="dataset-modif',
9988
10095
  (m === sm ? ' sel-set' : ''),
9989
10096
  '"><td class="dataset-selector',
9990
- (m.hasWildcards ? ' wildcard' : ''),
10097
+ (wild ? ' wildcard' : ''),
9991
10098
  '" title="Shift-click to ', (defsel ? 'clear' : 'set as'),
9992
10099
  ' default modifier',
9993
10100
  clk, ', false);">',
9994
10101
  (defsel ? '<img src="images/solve.png" style="height: 14px;' +
9995
10102
  ' width: 14px; margin: 0 1px -3px -1px;">' : ''),
9996
- m.selector, '</td><td class="dataset-expression',
10103
+ (wild ? wildcardFormat(m.selector, true) : m.selector),
10104
+ '</td><td class="dataset-expression',
9997
10105
  clk, ');">', m.expression.text, '</td></tr>'].join(''));
9998
10106
  }
9999
10107
  this.modifier_table.innerHTML = ml.join('');
@@ -10096,24 +10204,15 @@ class GUIDatasetManager extends DatasetManager {
10096
10204
  }
10097
10205
  } else {
10098
10206
  this.selected_modifier = null;
10099
- }
10207
+ }
10100
10208
  this.updateModifiers();
10101
10209
  }
10102
10210
 
10103
10211
  get selectedPrefix() {
10104
10212
  // Returns the selected prefix (with its trailing colon-space)
10105
- let prefix = '',
10106
- tr = this.selected_prefix_row;
10107
- while(tr) {
10108
- const td = tr.firstElementChild;
10109
- if(td && td.firstElementChild.dataset.prefix === 'x') {
10110
- prefix = td.lastChild.textContent + UI.PREFIXER + prefix;
10111
- tr = tr.previousSibling;
10112
- } else {
10113
- tr = null;
10114
- }
10115
- }
10116
- return prefix;
10213
+ const tr = this.selected_prefix_row;
10214
+ if(tr && tr.dataset.prefixName) return tr.dataset.prefixName + UI.PREFIXER;
10215
+ return '';
10117
10216
  }
10118
10217
 
10119
10218
  promptForDataset(shift=false) {
@@ -10122,9 +10221,7 @@ class GUIDatasetManager extends DatasetManager {
10122
10221
  let prefix = '';
10123
10222
  if(shift) {
10124
10223
  if(this.selected_dataset) {
10125
- const p = UI.prefixesAndName(this.selected_dataset.name);
10126
- p[p.length - 1] = '';
10127
- prefix = p.join(UI.PREFIXER);
10224
+ prefix = UI.completePrefix(this.selected_dataset.name);
10128
10225
  } else if(this.selected_prefix) {
10129
10226
  prefix = this.selectedPrefix;
10130
10227
  }
@@ -10139,6 +10236,7 @@ class GUIDatasetManager extends DatasetManager {
10139
10236
  if(d) {
10140
10237
  this.new_modal.hide();
10141
10238
  this.selected_dataset = d;
10239
+ this.focal_table = this.dataset_table;
10142
10240
  this.updateDialog();
10143
10241
  }
10144
10242
  }
@@ -10178,12 +10276,14 @@ class GUIDatasetManager extends DatasetManager {
10178
10276
  let e = this.rename_modal.element('name'),
10179
10277
  prefix = e.value.trim();
10180
10278
  e.focus();
10279
+ // Trim trailing colon if user entered it
10181
10280
  while(prefix.endsWith(':')) prefix = prefix.slice(0, -1);
10182
10281
  // NOTE: prefix may be empty string, but otherwise should be a valid name
10183
10282
  if(prefix && !UI.validName(prefix)) {
10184
10283
  UI.warn('Invalid prefix');
10185
10284
  return;
10186
10285
  }
10286
+ // Now add the colon-plus-space prefix separator
10187
10287
  prefix += UI.PREFIXER;
10188
10288
  const
10189
10289
  oldpref = this.selectedPrefix,
@@ -10203,7 +10303,7 @@ class GUIDatasetManager extends DatasetManager {
10203
10303
  if(MODEL.datasets[nk]) nc++;
10204
10304
  }
10205
10305
  if(nc) {
10206
- UI.warn('Renaming ' + pluralS(dsl.length, 'dataset') +
10306
+ UI.warn('Renaming ' + pluralS(dsl.length, 'dataset').toLowerCase() +
10207
10307
  ' would cause ' + pluralS(nc, 'name conflict'));
10208
10308
  return;
10209
10309
  }
@@ -10216,7 +10316,7 @@ class GUIDatasetManager extends DatasetManager {
10216
10316
  const d = MODEL.datasets[dsl[i]];
10217
10317
  d.rename(d.displayName.replace(oldpref, prefix), false);
10218
10318
  }
10219
- let msg = 'Renamed ' + pluralS(dsl.length, 'dataset');
10319
+ let msg = 'Renamed ' + pluralS(dsl.length, 'dataset').toLowerCase();
10220
10320
  if(MODEL.variable_count) msg += ', and updated ' +
10221
10321
  pluralS(MODEL.variable_count, 'variable') + ' in ' +
10222
10322
  pluralS(MODEL.expression_count, 'expression');
@@ -10337,14 +10437,14 @@ class GUIDatasetManager extends DatasetManager {
10337
10437
  renameModifier() {
10338
10438
  if(!this.selected_modifier) return;
10339
10439
  const
10340
- hw = this.selected_modifier.hasWildcards,
10440
+ wild = this.selected_modifier.hasWildcards,
10341
10441
  sel = this.rename_selector_modal.element('name').value,
10342
10442
  // NOTE: normal dataset selector, so remove all invalid characters
10343
- clean_sel = sel.replace(/[^a-zA-z0-9\%\+\-]/g, ''),
10443
+ clean_sel = sel.replace(/[^a-zA-z0-9\%\+\-\?\*]/g, ''),
10344
10444
  // Keep track of old name
10345
10445
  oldm = this.selected_modifier,
10346
10446
  // NOTE: addModifier returns existing one if selector not changed
10347
- m = this.selected_dataset.addModifier(sel);
10447
+ m = this.selected_dataset.addModifier(clean_sel);
10348
10448
  // NULL can result when new name is invalid
10349
10449
  if(!m) return;
10350
10450
  // If selected modifier was the dataset default selector, update it
@@ -10356,11 +10456,12 @@ class GUIDatasetManager extends DatasetManager {
10356
10456
  if(m === oldm) {
10357
10457
  m.selector = clean_sel;
10358
10458
  this.updateDialog();
10459
+ this.rename_selector_modal.hide();
10359
10460
  return;
10360
10461
  }
10361
10462
  // Rest is needed only when a new modifier has been added
10362
10463
  m.expression = oldm.expression;
10363
- if(hw) {
10464
+ if(wild) {
10364
10465
  // Wildcard selector means: recompile the modifier expression
10365
10466
  m.expression.attribute = m.selector;
10366
10467
  m.expression.compile();
@@ -10368,11 +10469,10 @@ class GUIDatasetManager extends DatasetManager {
10368
10469
  this.deleteModifier();
10369
10470
  this.selected_modifier = m;
10370
10471
  // Update all chartvariables referencing this dataset + old selector
10371
- const vl = MODEL.datasetChartVariables;
10472
+ const vl = MODEL.datasetVariables;
10372
10473
  let cv_cnt = 0;
10373
10474
  for(let i = 0; i < vl.length; i++) {
10374
- if(v.object === this.selected_dataset &&
10375
- v.attribute === oldm.selector) {
10475
+ if(v.object === this.selected_dataset && v.attribute === oldm.selector) {
10376
10476
  v.attribute = m.selector;
10377
10477
  cv_cnt++;
10378
10478
  }
@@ -10776,6 +10876,7 @@ class EquationManager {
10776
10876
  for(let i = 0; i < msl.length; i++) {
10777
10877
  const
10778
10878
  m = ed.modifiers[UI.nameToID(msl[i])],
10879
+ wild = (m.selector.indexOf('??') >= 0),
10779
10880
  clk = '" onclick="EQUATION_MANAGER.selectModifier(event, \'' +
10780
10881
  m.selector + '\'';
10781
10882
  if(m === sm) smid += i;
@@ -10783,8 +10884,9 @@ class EquationManager {
10783
10884
  (m === sm ? ' sel-set' : ''),
10784
10885
  '"><td class="equation-selector',
10785
10886
  (m.expression.isStatic ? '' : ' it'),
10786
- clk, ', false);">',
10787
- m.selector, '</td><td class="equation-expression',
10887
+ (wild ? ' wildcard' : ''), clk, ', false);">',
10888
+ (wild ? wildcardFormat(m.selector) : m.selector),
10889
+ '</td><td class="equation-expression',
10788
10890
  clk, ');">', m.expression.text, '</td></tr>'].join(''));
10789
10891
  }
10790
10892
  this.table.innerHTML = ml.join('');
@@ -10914,8 +11016,7 @@ class EquationManager {
10914
11016
  const c = MODEL.charts[i];
10915
11017
  for(let j = 0; j < c.variables.length; j++) {
10916
11018
  const v = c.variables[j];
10917
- if(v.object === MODEL.equations_dataset &&
10918
- v.attribute === olds) {
11019
+ if(v.object === MODEL.equations_dataset && v.attribute === olds) {
10919
11020
  v.attribute = m.selector;
10920
11021
  cv_cnt++;
10921
11022
  }