linny-r 1.3.4 → 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 || '';
@@ -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
  //
@@ -5766,6 +5814,8 @@ console.log('HERE name map', name_map);
5766
5814
  const md = this.modals.note;
5767
5815
  if(n) {
5768
5816
  md.element('action').innerHTML = 'Edit';
5817
+ const nr = n.number;
5818
+ md.element('number').innerHTML = (nr ? '#' + nr : '');
5769
5819
  md.element('text').value = n.contents;
5770
5820
  md.element('C').value = n.color.text;
5771
5821
  } else {
@@ -5787,6 +5837,10 @@ console.log('HERE name map', name_map);
5787
5837
  } else {
5788
5838
  md.element('actor').value = '';
5789
5839
  }
5840
+ const sim = p.similarNumberedEntities;
5841
+ if(sim.length) {
5842
+ console.log('HERE!', sim);
5843
+ }
5790
5844
  md.element('LB').value = p.lower_bound.text;
5791
5845
  md.element('UB').value = p.upper_bound.text;
5792
5846
  this.setEqualBounds('process', p.equal_bounds);
@@ -6017,7 +6071,7 @@ console.log('HERE name map', name_map);
6017
6071
  const
6018
6072
  from_process = l.from_node instanceof Process,
6019
6073
  to_process = l.to_node instanceof Process,
6020
- md = this.modals.link;
6074
+ md = this.modals.link;
6021
6075
  md.show();
6022
6076
  md.element('from-name').innerHTML = l.from_node.displayName;
6023
6077
  md.element('to-name').innerHTML = l.to_node.displayName;
@@ -6064,6 +6118,7 @@ console.log('HERE name map', name_map);
6064
6118
  md.element('output-soc').style.display = 'none';
6065
6119
  }
6066
6120
  }
6121
+ this.edited_object = l;
6067
6122
  if(alt) md.element(attr + '-x').dispatchEvent(new Event('click'));
6068
6123
  }
6069
6124
 
@@ -6108,7 +6163,7 @@ console.log('HERE name map', name_map);
6108
6163
  // @@TO DO: prepare for undo
6109
6164
  const
6110
6165
  md = this.modals.link,
6111
- l = this.on_link;
6166
+ l = this.edited_object;
6112
6167
  // Check whether all input fields are valid
6113
6168
  if(!this.updateExpressionInput('link-R', 'rate', l.relative_rate)) {
6114
6169
  return false;
@@ -6415,7 +6470,8 @@ class GUIMonitor {
6415
6470
  if(this.call_stack_shown) return;
6416
6471
  const
6417
6472
  csl = VM.call_stack.length,
6418
- err = VM.call_stack[csl - 1].vector[t],
6473
+ top = VM.call_stack[csl - 1],
6474
+ err = top.vector[t],
6419
6475
  // Make separate lists of variable names and their expressions
6420
6476
  vlist = [],
6421
6477
  xlist = [];
@@ -6480,6 +6536,9 @@ class GUIMonitor {
6480
6536
  tbl.push('<div style="color: gray; margin-top: 8px; font-size: 10px">',
6481
6537
  VM.out_of_bounds_msg.replace(VM.out_of_bounds_array, anc), '</div>');
6482
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
6483
6542
  document.getElementById('call-stack-table').innerHTML = tbl.join('');
6484
6543
  document.getElementById('call-stack-modal').style.display = 'block';
6485
6544
  this.call_stack_shown = true;
@@ -7244,10 +7303,12 @@ NOTE: Grouping groups results in a single group, e.g., (1;2);(3;4;5) evaluates a
7244
7303
  if(this.edited_input_id) {
7245
7304
  document.getElementById(this.edited_input_id).value = xp.expr;
7246
7305
  // 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(''));
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(' '));
7251
7312
  }
7252
7313
  this.edited_input_id = '';
7253
7314
  } else if(DATASET_MANAGER.edited_expression) {
@@ -8743,6 +8804,7 @@ class ConstraintEditor {
8743
8804
  // NOTE: this could be expanded to apply to the selected BL only
8744
8805
  UI.setBox('ce-no-slack', this.constraint.no_slack);
8745
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
8746
8808
  if(true||this.from_node instanceof Process && this.from_node instanceof Process) {
8747
8809
  this.soc_direct.value = this.constraint.soc_direction;
8748
8810
  // NOTE: share of cost is input as a percentage
@@ -9894,6 +9956,7 @@ class GUIDatasetManager extends DatasetManager {
9894
9956
  ps = pid.split(UI.PREFIXER),
9895
9957
  pps = prev_id.split(UI.PREFIXER),
9896
9958
  pn = pref_names[pid],
9959
+ pns = pn.join(UI.PREFIXER),
9897
9960
  lpl = [];
9898
9961
  let lindent = 0;
9899
9962
  // Ignore identical leading prefixes
@@ -9908,8 +9971,9 @@ class GUIDatasetManager extends DatasetManager {
9908
9971
  lpl.push(ps.shift());
9909
9972
  lindent++;
9910
9973
  const lpid = lpl.join(UI.PREFIXER);
9911
- dl.push(['<tr data-prefix="', lpid, '" class="dataset',
9912
- '" 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>',
9913
9977
  // NOTE: data-prefix="x" signals that this is an extra row
9914
9978
  (lindent > 0 ?
9915
9979
  '<div data-prefix="x" style="width: ' + lindent * indent_px +
@@ -10022,6 +10086,7 @@ class GUIDatasetManager extends DatasetManager {
10022
10086
  for(let i = 0; i < msl.length; i++) {
10023
10087
  const
10024
10088
  m = sd.modifiers[UI.nameToID(msl[i])],
10089
+ wild = m.hasWildcards,
10025
10090
  defsel = (m.selector === sd.default_selector),
10026
10091
  clk = '" onclick="DATASET_MANAGER.selectModifier(event, \'' +
10027
10092
  m.selector + '\'';
@@ -10029,13 +10094,14 @@ class GUIDatasetManager extends DatasetManager {
10029
10094
  ml.push(['<tr id="dsmtr', i, '" class="dataset-modif',
10030
10095
  (m === sm ? ' sel-set' : ''),
10031
10096
  '"><td class="dataset-selector',
10032
- (m.hasWildcards ? ' wildcard' : ''),
10097
+ (wild ? ' wildcard' : ''),
10033
10098
  '" title="Shift-click to ', (defsel ? 'clear' : 'set as'),
10034
10099
  ' default modifier',
10035
10100
  clk, ', false);">',
10036
10101
  (defsel ? '<img src="images/solve.png" style="height: 14px;' +
10037
10102
  ' width: 14px; margin: 0 1px -3px -1px;">' : ''),
10038
- m.selector, '</td><td class="dataset-expression',
10103
+ (wild ? wildcardFormat(m.selector, true) : m.selector),
10104
+ '</td><td class="dataset-expression',
10039
10105
  clk, ');">', m.expression.text, '</td></tr>'].join(''));
10040
10106
  }
10041
10107
  this.modifier_table.innerHTML = ml.join('');
@@ -10138,24 +10204,15 @@ class GUIDatasetManager extends DatasetManager {
10138
10204
  }
10139
10205
  } else {
10140
10206
  this.selected_modifier = null;
10141
- }
10207
+ }
10142
10208
  this.updateModifiers();
10143
10209
  }
10144
10210
 
10145
10211
  get selectedPrefix() {
10146
10212
  // 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;
10213
+ const tr = this.selected_prefix_row;
10214
+ if(tr && tr.dataset.prefixName) return tr.dataset.prefixName + UI.PREFIXER;
10215
+ return '';
10159
10216
  }
10160
10217
 
10161
10218
  promptForDataset(shift=false) {
@@ -10164,9 +10221,7 @@ class GUIDatasetManager extends DatasetManager {
10164
10221
  let prefix = '';
10165
10222
  if(shift) {
10166
10223
  if(this.selected_dataset) {
10167
- const p = UI.prefixesAndName(this.selected_dataset.name);
10168
- p[p.length - 1] = '';
10169
- prefix = p.join(UI.PREFIXER);
10224
+ prefix = UI.completePrefix(this.selected_dataset.name);
10170
10225
  } else if(this.selected_prefix) {
10171
10226
  prefix = this.selectedPrefix;
10172
10227
  }
@@ -10181,6 +10236,7 @@ class GUIDatasetManager extends DatasetManager {
10181
10236
  if(d) {
10182
10237
  this.new_modal.hide();
10183
10238
  this.selected_dataset = d;
10239
+ this.focal_table = this.dataset_table;
10184
10240
  this.updateDialog();
10185
10241
  }
10186
10242
  }
@@ -10220,12 +10276,14 @@ class GUIDatasetManager extends DatasetManager {
10220
10276
  let e = this.rename_modal.element('name'),
10221
10277
  prefix = e.value.trim();
10222
10278
  e.focus();
10279
+ // Trim trailing colon if user entered it
10223
10280
  while(prefix.endsWith(':')) prefix = prefix.slice(0, -1);
10224
10281
  // NOTE: prefix may be empty string, but otherwise should be a valid name
10225
10282
  if(prefix && !UI.validName(prefix)) {
10226
10283
  UI.warn('Invalid prefix');
10227
10284
  return;
10228
10285
  }
10286
+ // Now add the colon-plus-space prefix separator
10229
10287
  prefix += UI.PREFIXER;
10230
10288
  const
10231
10289
  oldpref = this.selectedPrefix,
@@ -10245,7 +10303,7 @@ class GUIDatasetManager extends DatasetManager {
10245
10303
  if(MODEL.datasets[nk]) nc++;
10246
10304
  }
10247
10305
  if(nc) {
10248
- UI.warn('Renaming ' + pluralS(dsl.length, 'dataset') +
10306
+ UI.warn('Renaming ' + pluralS(dsl.length, 'dataset').toLowerCase() +
10249
10307
  ' would cause ' + pluralS(nc, 'name conflict'));
10250
10308
  return;
10251
10309
  }
@@ -10258,7 +10316,7 @@ class GUIDatasetManager extends DatasetManager {
10258
10316
  const d = MODEL.datasets[dsl[i]];
10259
10317
  d.rename(d.displayName.replace(oldpref, prefix), false);
10260
10318
  }
10261
- let msg = 'Renamed ' + pluralS(dsl.length, 'dataset');
10319
+ let msg = 'Renamed ' + pluralS(dsl.length, 'dataset').toLowerCase();
10262
10320
  if(MODEL.variable_count) msg += ', and updated ' +
10263
10321
  pluralS(MODEL.variable_count, 'variable') + ' in ' +
10264
10322
  pluralS(MODEL.expression_count, 'expression');
@@ -10379,14 +10437,14 @@ class GUIDatasetManager extends DatasetManager {
10379
10437
  renameModifier() {
10380
10438
  if(!this.selected_modifier) return;
10381
10439
  const
10382
- hw = this.selected_modifier.hasWildcards,
10440
+ wild = this.selected_modifier.hasWildcards,
10383
10441
  sel = this.rename_selector_modal.element('name').value,
10384
10442
  // NOTE: normal dataset selector, so remove all invalid characters
10385
- clean_sel = sel.replace(/[^a-zA-z0-9\%\+\-]/g, ''),
10443
+ clean_sel = sel.replace(/[^a-zA-z0-9\%\+\-\?\*]/g, ''),
10386
10444
  // Keep track of old name
10387
10445
  oldm = this.selected_modifier,
10388
10446
  // NOTE: addModifier returns existing one if selector not changed
10389
- m = this.selected_dataset.addModifier(sel);
10447
+ m = this.selected_dataset.addModifier(clean_sel);
10390
10448
  // NULL can result when new name is invalid
10391
10449
  if(!m) return;
10392
10450
  // If selected modifier was the dataset default selector, update it
@@ -10398,11 +10456,12 @@ class GUIDatasetManager extends DatasetManager {
10398
10456
  if(m === oldm) {
10399
10457
  m.selector = clean_sel;
10400
10458
  this.updateDialog();
10459
+ this.rename_selector_modal.hide();
10401
10460
  return;
10402
10461
  }
10403
10462
  // Rest is needed only when a new modifier has been added
10404
10463
  m.expression = oldm.expression;
10405
- if(hw) {
10464
+ if(wild) {
10406
10465
  // Wildcard selector means: recompile the modifier expression
10407
10466
  m.expression.attribute = m.selector;
10408
10467
  m.expression.compile();
@@ -10410,11 +10469,10 @@ class GUIDatasetManager extends DatasetManager {
10410
10469
  this.deleteModifier();
10411
10470
  this.selected_modifier = m;
10412
10471
  // Update all chartvariables referencing this dataset + old selector
10413
- const vl = MODEL.datasetChartVariables;
10472
+ const vl = MODEL.datasetVariables;
10414
10473
  let cv_cnt = 0;
10415
10474
  for(let i = 0; i < vl.length; i++) {
10416
- if(v.object === this.selected_dataset &&
10417
- v.attribute === oldm.selector) {
10475
+ if(v.object === this.selected_dataset && v.attribute === oldm.selector) {
10418
10476
  v.attribute = m.selector;
10419
10477
  cv_cnt++;
10420
10478
  }
@@ -10818,6 +10876,7 @@ class EquationManager {
10818
10876
  for(let i = 0; i < msl.length; i++) {
10819
10877
  const
10820
10878
  m = ed.modifiers[UI.nameToID(msl[i])],
10879
+ wild = (m.selector.indexOf('??') >= 0),
10821
10880
  clk = '" onclick="EQUATION_MANAGER.selectModifier(event, \'' +
10822
10881
  m.selector + '\'';
10823
10882
  if(m === sm) smid += i;
@@ -10825,8 +10884,9 @@ class EquationManager {
10825
10884
  (m === sm ? ' sel-set' : ''),
10826
10885
  '"><td class="equation-selector',
10827
10886
  (m.expression.isStatic ? '' : ' it'),
10828
- clk, ', false);">',
10829
- 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',
10830
10890
  clk, ');">', m.expression.text, '</td></tr>'].join(''));
10831
10891
  }
10832
10892
  this.table.innerHTML = ml.join('');
@@ -10956,8 +11016,7 @@ class EquationManager {
10956
11016
  const c = MODEL.charts[i];
10957
11017
  for(let j = 0; j < c.variables.length; j++) {
10958
11018
  const v = c.variables[j];
10959
- if(v.object === MODEL.equations_dataset &&
10960
- v.attribute === olds) {
11019
+ if(v.object === MODEL.equations_dataset && v.attribute === olds) {
10961
11020
  v.attribute = m.selector;
10962
11021
  cv_cnt++;
10963
11022
  }