linny-r 1.2.1 → 1.3.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.
@@ -12,7 +12,7 @@ dialogs, the main drawing canvas, and event handler functions.
12
12
  */
13
13
 
14
14
  /*
15
- Copyright (c) 2017-2022 Delft University of Technology
15
+ Copyright (c) 2017-2023 Delft University of Technology
16
16
 
17
17
  Permission is hereby granted, free of charge, to any person obtaining a copy
18
18
  of this software and associated documentation files (the "Software"), to deal
@@ -2877,7 +2877,7 @@ class GUIController extends Controller {
2877
2877
  this.shortcuts = {
2878
2878
  'A': 'actors',
2879
2879
  'B': 'repository', // B for "Browse"
2880
- 'C': 'clone',
2880
+ 'C': 'clone', // button and Ctrl-C now copies; Alt-C clones
2881
2881
  'D': 'dataset',
2882
2882
  'E': 'equation',
2883
2883
  'F': 'finder',
@@ -2887,7 +2887,7 @@ class GUIController extends Controller {
2887
2887
  'J': 'sensitivity', // J for "Jitter"
2888
2888
  'K': 'reset', // reset model and clear results from graph
2889
2889
  'L': 'load',
2890
- 'M': 'monitor',
2890
+ 'M': 'monitor', // Alt-M will open the model settings dialog
2891
2891
  // Ctrl-N will still open a new browser window
2892
2892
  'O': 'chart', // O for "Output", as it can be charts as wel as data
2893
2893
  'P': 'diagram', // P for PNG (Portable Network Graphics image)
@@ -2896,7 +2896,7 @@ class GUIController extends Controller {
2896
2896
  'S': 'save',
2897
2897
  // Ctrl-T will still open a new browser tab
2898
2898
  'U': 'parent', // U for "move UP in cluster hierarchy"
2899
- 'V': 'settings',
2899
+ 'V': 'paste',
2900
2900
  // Ctrl-W will still close the browser window
2901
2901
  'X': 'experiment',
2902
2902
  'Y': 'redo',
@@ -2906,7 +2906,7 @@ class GUIController extends Controller {
2906
2906
  // Initialize controller buttons
2907
2907
  this.node_btns = ['process', 'product', 'link', 'constraint',
2908
2908
  'cluster', 'module', 'note'];
2909
- this.edit_btns = ['clone', 'delete', 'undo', 'redo'];
2909
+ this.edit_btns = ['clone', 'paste', 'delete', 'undo', 'redo'];
2910
2910
  this.model_btns = ['settings', 'save', 'repository', 'actors', 'dataset',
2911
2911
  'equation', 'chart', 'sensitivity', 'experiment', 'diagram',
2912
2912
  'savediagram', 'finder', 'monitor', 'solve'];
@@ -3026,7 +3026,15 @@ class GUIController extends Controller {
3026
3026
  }
3027
3027
  // Vertical tool bar buttons
3028
3028
  this.buttons.clone.addEventListener('click',
3029
- () => UI.promptForCloning());
3029
+ (event) => {
3030
+ if(event.altKey) {
3031
+ UI.promptForCloning();
3032
+ } else {
3033
+ UI.copySelection();
3034
+ }
3035
+ });
3036
+ this.buttons.paste.addEventListener('click',
3037
+ () => UI.pasteSelection());
3030
3038
  this.buttons['delete'].addEventListener('click',
3031
3039
  () => {
3032
3040
  UNDO_STACK.push('delete');
@@ -3057,6 +3065,12 @@ class GUIController extends Controller {
3057
3065
  (event) => UI.stepBack(event));
3058
3066
  this.buttons.stepforward.addEventListener('click',
3059
3067
  (event) => UI.stepForward(event));
3068
+ document.getElementById('prev-issue').addEventListener('click',
3069
+ () => UI.updateIssuePanel(-1));
3070
+ document.getElementById('issue-nr').addEventListener('click',
3071
+ () => UI.jumpToIssue());
3072
+ document.getElementById('next-issue').addEventListener('click',
3073
+ () => UI.updateIssuePanel(1));
3060
3074
  this.buttons.recall.addEventListener('click',
3061
3075
  // Recall button toggles the documentation dialog
3062
3076
  () => UI.buttons.documentation.dispatchEvent(new Event('click')));
@@ -3448,6 +3462,62 @@ class GUIController extends Controller {
3448
3462
  this.start_sel_y = -1;
3449
3463
  }
3450
3464
 
3465
+ updateIssuePanel(change=0) {
3466
+ const
3467
+ count = VM.issue_list.length,
3468
+ panel = document.getElementById('issue-panel');
3469
+ if(count > 0) {
3470
+ const
3471
+ prev = document.getElementById('prev-issue'),
3472
+ next = document.getElementById('next-issue'),
3473
+ nr = document.getElementById('issue-nr');
3474
+ panel.title = pluralS(count, 'issue') +
3475
+ ' occurred - click on number, \u25C1 or \u25B7 to view what and when';
3476
+ if(VM.issue_index === -1) {
3477
+ VM.issue_index = 0;
3478
+ } else if(change) {
3479
+ VM.issue_index += change;
3480
+ setTimeout(() => UI.jumpToIssue(), 10);
3481
+ }
3482
+ nr.innerText = VM.issue_index + 1;
3483
+ if(VM.issue_index <= 0) {
3484
+ prev.classList.add('disab');
3485
+ } else {
3486
+ prev.classList.remove('disab');
3487
+ }
3488
+ if(this.issue_index >= count - 1) {
3489
+ next.classList.add('disab');
3490
+ } else {
3491
+ next.classList.remove('disab');
3492
+ }
3493
+ panel.style.display = 'table-cell';
3494
+ } else {
3495
+ panel.style.display = 'none';
3496
+ VM.issue_index = -1;
3497
+ }
3498
+ }
3499
+
3500
+ jumpToIssue() {
3501
+ // Set time step to the one of the warning message for the issue
3502
+ // index, redraw the diagram if needed, and display the message
3503
+ // on the infoline
3504
+ if(VM.issue_index >= 0) {
3505
+ const
3506
+ issue = VM.issue_list[VM.issue_index],
3507
+ po = issue.indexOf('(t='),
3508
+ pc = issue.indexOf(')', po),
3509
+ t = parseInt(issue.substring(po + 3, pc - 1));
3510
+ if(MODEL.t !== t) {
3511
+ MODEL.t = t;
3512
+ this.updateTimeStep();
3513
+ this.drawDiagram(MODEL);
3514
+ }
3515
+ this.info_line.classList.remove('error', 'notification');
3516
+ this.info_line.classList.add('warning');
3517
+ this.info_line.innerHTML = issue.substring(pc + 2);
3518
+ }
3519
+ }
3520
+
3451
3521
  get doubleClicked() {
3452
3522
  // Return TRUE when a "double-click" occurred
3453
3523
  const
@@ -3722,7 +3792,7 @@ class GUIController extends Controller {
3722
3792
  // Updates the buttons on the main GUI toolbars
3723
3793
  const
3724
3794
  node_btns = 'process product link constraint cluster note ',
3725
- edit_btns = 'clone delete undo redo ',
3795
+ edit_btns = 'clone paste delete undo redo ',
3726
3796
  model_btns = 'settings save actors dataset equation chart ' +
3727
3797
  'diagram savediagram finder monitor solve';
3728
3798
  if(MODEL === null) {
@@ -3749,6 +3819,7 @@ class GUIController extends Controller {
3749
3819
  this.active_button = this.stayActiveButton;
3750
3820
  this.disableButtons(edit_btns);
3751
3821
  if(MODEL.selection.length > 0) this.enableButtons('clone delete');
3822
+ if(this.canPaste) this.enableButtons('paste');
3752
3823
  // Only allow target seeking when some target or process constraint is defined
3753
3824
  if(MODEL.hasTargets) this.enableButtons('solve');
3754
3825
  var u = UNDO_STACK.canUndo;
@@ -4424,6 +4495,15 @@ class GUIController extends Controller {
4424
4495
  this.stepBack(e);
4425
4496
  } else if(e.keyCode === 39) {
4426
4497
  this.stepForward(e);
4498
+ } else if(e.altKey && [67, 77].indexOf(e.keyCode) >= 0) {
4499
+ // Special shortcut keys for "clone selection" and "model settings"
4500
+ const be = new Event('click');
4501
+ be.altKey = true;
4502
+ if(e.keyCode === 67) {
4503
+ this.buttons.clone.dispatchEvent(be);
4504
+ } else {
4505
+ this.buttons.settings.dispatchEvent(be);
4506
+ }
4427
4507
  } else if(!e.shiftKey && !e.altKey &&
4428
4508
  (!topmod || [65, 67, 86].indexOf(e.keyCode) < 0)) {
4429
4509
  // Interpret special keys as shortcuts unless a modal dialog is open
@@ -5137,7 +5217,42 @@ class GUIController extends Controller {
5137
5217
  cancelCloneSelection() {
5138
5218
  this.modals.clone.hide();
5139
5219
  this.updateButtons();
5140
- }
5220
+ }
5221
+
5222
+ copySelection() {
5223
+ // Save selection as XML in local storage of the browser
5224
+ const xml = MODEL.selectionAsXML;
5225
+ //console.log('HERE copy xml', xml);
5226
+ if(xml) {
5227
+ window.localStorage.setItem('Linny-R-selection-XML', xml);
5228
+ this.updateButtons();
5229
+ this.notify('Selection copied, but cannot be pasted yet -- Use Alt-C to clone');
5230
+ }
5231
+ }
5232
+
5233
+ get canPaste() {
5234
+ const xml = window.localStorage.getItem('Linny-R-selection-XML');
5235
+ if(xml) {
5236
+ const timestamp = xml.match(/<copy timestamp="(\d+)"/);
5237
+ if(timestamp) {
5238
+ if(Date.now() - parseInt(timestamp[1]) < 8*3600000) return true;
5239
+ }
5240
+ // Remove XML from local storage if older than 8 hours
5241
+ window.localStorage.removeItem('Linny-R-selection-XML');
5242
+ }
5243
+ return false;
5244
+ }
5245
+
5246
+ pasteSelection() {
5247
+ // If selection has been saved as XML in local storage, test to
5248
+ // see whether PASTE would result in name conflicts, and if so,
5249
+ // open the name conflict resolution window
5250
+ const xml = window.localStorage.getItem('Linny-R-selection-XML');
5251
+ if(xml) {
5252
+ // @@ TO DO!
5253
+ this.notify('Paste not implemented yet -- WORK IN PROGRESS!');
5254
+ }
5255
+ }
5141
5256
 
5142
5257
  //
5143
5258
  // Interaction with modal dialogs to modify model or entity properties
@@ -5346,17 +5461,20 @@ class GUIController extends Controller {
5346
5461
  if(!this.updateExpressionInput(
5347
5462
  'process-IL', 'initial level', p.initial_level)) return false;
5348
5463
  // Store original expression string
5349
- const pxt = p.pace_expression.text;
5464
+ const
5465
+ px = p.pace_expression,
5466
+ pxt = p.pace_expression.text;
5350
5467
  // Validate expression
5351
5468
  if(!this.updateExpressionInput('process-pace', 'level change frequency',
5352
- p.pace_expression)) return false;
5469
+ px)) return false;
5353
5470
  // NOTE: pace expression must be *static* and >= 1
5354
- n = p.pace_expression.result(1);
5355
- if(!p.pace_expression.isStatic || n < 1) {
5471
+ n = px.result(1);
5472
+ if(!px.isStatic || n < 1) {
5356
5473
  md.element('pace').focus();
5357
5474
  this.warn('Level change frequency must be static and &ge; 1');
5358
5475
  // Restore original expression string
5359
- p.pace_expression.text = pxt;
5476
+ px.text = pxt;
5477
+ px.code = null;
5360
5478
  return false;
5361
5479
  }
5362
5480
  // Ignore fraction if a real number was entered.
@@ -6466,7 +6584,7 @@ class GUIFileManager {
6466
6584
  }
6467
6585
 
6468
6586
  renderDiagramAsPNG() {
6469
- localStorage.removeItem('png-url');
6587
+ window.localStorage.removeItem('png-url');
6470
6588
  UI.paper.fitToSize();
6471
6589
  MODEL.alignToGrid();
6472
6590
  this.renderSVGAsPNG(UI.paper.svg.outerHTML);
@@ -6492,7 +6610,7 @@ class GUIFileManager {
6492
6610
  })
6493
6611
  .then((data) => {
6494
6612
  // Pass URL of image to the newly opened browser window
6495
- localStorage.setItem('png-url', data);
6613
+ window.localStorage.setItem('png-url', data);
6496
6614
  })
6497
6615
  .catch((err) => UI.warn(UI.WARNING.NO_CONNECTION, err));
6498
6616
  }
@@ -9080,7 +9198,9 @@ class GUIDatasetManager extends DatasetManager {
9080
9198
  this.close_btn.addEventListener(
9081
9199
  'click', (event) => UI.toggleDialog(event));
9082
9200
  document.getElementById('ds-new-btn').addEventListener(
9083
- 'click', () => DATASET_MANAGER.promptForDataset());
9201
+ // Shift-click on New button => add prefix of selected dataset
9202
+ // (if any) to the name field of the dialog
9203
+ 'click', () => DATASET_MANAGER.promptForDataset(event.shiftKey));
9084
9204
  document.getElementById('ds-data-btn').addEventListener(
9085
9205
  'click', () => DATASET_MANAGER.editData());
9086
9206
  document.getElementById('ds-rename-btn').addEventListener(
@@ -9117,6 +9237,8 @@ class GUIDatasetManager extends DatasetManager {
9117
9237
  'click', () => DATASET_MANAGER.editExpression());
9118
9238
  document.getElementById('ds-delete-modif-btn').addEventListener(
9119
9239
  'click', () => DATASET_MANAGER.deleteModifier());
9240
+ document.getElementById('ds-convert-modif-btn').addEventListener(
9241
+ 'click', () => DATASET_MANAGER.promptToConvertModifiers());
9120
9242
  // Modifier table
9121
9243
  this.modifier_table = document.getElementById('dataset-modif-table');
9122
9244
  // Modal dialogs
@@ -9130,6 +9252,11 @@ class GUIDatasetManager extends DatasetManager {
9130
9252
  'click', () => DATASET_MANAGER.renameDataset());
9131
9253
  this.rename_modal.cancel.addEventListener(
9132
9254
  'click', () => DATASET_MANAGER.rename_modal.hide());
9255
+ this.conversion_modal = new ModalDialog('convert-modifiers');
9256
+ this.conversion_modal.ok.addEventListener(
9257
+ 'click', () => DATASET_MANAGER.convertModifiers());
9258
+ this.conversion_modal.cancel.addEventListener(
9259
+ 'click', () => DATASET_MANAGER.conversion_modal.hide());
9133
9260
  this.new_selector_modal = new ModalDialog('new-selector');
9134
9261
  this.new_selector_modal.ok.addEventListener(
9135
9262
  'click', () => DATASET_MANAGER.newModifier());
@@ -9164,12 +9291,14 @@ class GUIDatasetManager extends DatasetManager {
9164
9291
 
9165
9292
  reset() {
9166
9293
  super.reset();
9294
+ this.selected_prefix_row = null;
9167
9295
  this.selected_modifier = null;
9168
9296
  this.edited_expression = null;
9169
9297
  this.filter_pattern = null;
9170
9298
  this.clicked_object = null;
9171
9299
  this.last_time_clicked = 0;
9172
9300
  this.focal_table = null;
9301
+ this.expanded_rows = [];
9173
9302
  }
9174
9303
 
9175
9304
  doubleClicked(obj) {
@@ -9213,6 +9342,9 @@ class GUIDatasetManager extends DatasetManager {
9213
9342
  const srl = this.focal_table.getElementsByClassName('sel-set');
9214
9343
  if(srl.length > 0) {
9215
9344
  let r = this.focal_table.rows[srl[0].rowIndex + dir];
9345
+ while(r && r.style.display === 'none') {
9346
+ r = (dir > 0 ? r.nextSibling : r.previousSibling);
9347
+ }
9216
9348
  if(r) {
9217
9349
  UI.scrollIntoView(r);
9218
9350
  // NOTE: cell, not row, listens for onclick event
@@ -9222,14 +9354,118 @@ class GUIDatasetManager extends DatasetManager {
9222
9354
  }
9223
9355
  }
9224
9356
 
9357
+ hideCollapsedRows() {
9358
+ // Hides all rows except top level and immediate children of expanded
9359
+ for(let i = 0; i < this.dataset_table.rows.length; i++) {
9360
+ const
9361
+ row = this.dataset_table.rows[i],
9362
+ // Get the first DIV in the first TD of this row
9363
+ first_div = row.firstChild.firstElementChild,
9364
+ btn = first_div.dataset.prefix === 'x';
9365
+ let p = row.dataset.prefix,
9366
+ x = this.expanded_rows.indexOf(p) >= 0,
9367
+ show = !p || x;
9368
+ if(btn) {
9369
+ const btn_div = row.getElementsByClassName('tree-btn')[0];
9370
+ // Special expand/collapse row
9371
+ if(show) {
9372
+ // Set triangle to point down
9373
+ btn_div.innerText = '\u25BC';
9374
+ } else {
9375
+ // Set triangle to point right
9376
+ btn_div.innerText = '\u25BA';
9377
+ // See whether "parent prefix" is expanded
9378
+ p = p.split(UI.PREFIXER);
9379
+ p.pop();
9380
+ p = p.join(UI.PREFIXER);
9381
+ // If so, then also show the row
9382
+ show = (!p || this.expanded_rows.indexOf(p) >= 0);
9383
+ }
9384
+ }
9385
+ row.style.display = (show ? 'block' : 'none');
9386
+ }
9387
+ }
9388
+
9389
+ togglePrefixRow(e) {
9390
+ // Shows list items of the next prefix level
9391
+ let r = e.target;
9392
+ while(r.tagName !== 'TR') r = r.parentNode;
9393
+ const
9394
+ p = r.dataset.prefix,
9395
+ i = this.expanded_rows.indexOf(p);
9396
+ if(i >= 0) {
9397
+ this.expanded_rows.splice(i, 1);
9398
+ // Also remove all prefixes that have `p` as prefix
9399
+ for(let j = this.expanded_rows.length - 1; j >= 0; j--) {
9400
+ if(this.expanded_rows[j].startsWith(p + UI.PREFIXER)) {
9401
+ this.expanded_rows.splice(j, 1);
9402
+ }
9403
+ }
9404
+ } else {
9405
+ addDistinct(p, this.expanded_rows);
9406
+ }
9407
+ this.hideCollapsedRows();
9408
+ }
9409
+
9410
+ rowByPrefix(prefix) {
9411
+ // Returns first table row with the specified prefix
9412
+ if(!prefix) return null;
9413
+ let lcp = prefix.toLowerCase(),
9414
+ pl = lcp.split(': ');
9415
+ // Remove trailing ': '
9416
+ if(lcp.endsWith(': ')) {
9417
+ pl.pop();
9418
+ lcp = pl.join(': ');
9419
+ }
9420
+ while(pl.length > 0) {
9421
+ addDistinct(pl.join(': '), this.expanded_rows);
9422
+ pl.pop();
9423
+ }
9424
+ this.hideCollapsedRows();
9425
+ for(let i = 0; i < this.dataset_table.rows.length; i++) {
9426
+ const r = this.dataset_table.rows[i];
9427
+ if(r.dataset.prefix === lcp) return r;
9428
+ }
9429
+ return null;
9430
+ }
9431
+
9432
+ selectPrefixRow(e) {
9433
+ // Selects expand/collapse prefix row
9434
+ this.focal_table = this.dataset_table;
9435
+ // NOTE: `e` can also be a string specifying the prefix to select
9436
+ let r = e.target || this.rowByPrefix(e);
9437
+ if(!r) return;
9438
+ // Modeler may have clicked on the expand/collapse triangle;
9439
+ const toggle = r.classList.contains('tree-btn');
9440
+ while(r.tagName !== 'TR') r = r.parentNode;
9441
+ this.selected_prefix_row = r;
9442
+ const sel = this.dataset_table.getElementsByClassName('sel-set');
9443
+ this.selected_dataset = null;
9444
+ if(sel.length > 0) {
9445
+ sel[0].classList.remove('sel-set');
9446
+ this.updatePanes();
9447
+ }
9448
+ r.classList.add('sel-set');
9449
+ if(!e.target) r.scrollIntoView({block: 'center'});
9450
+ if(toggle || e.altKey || this.doubleClicked(r)) this.togglePrefixRow(e);
9451
+ UI.enableButtons('ds-rename');
9452
+ }
9453
+
9225
9454
  updateDialog() {
9226
9455
  const
9456
+ indent_px = 14,
9227
9457
  dl = [],
9228
9458
  dnl = [],
9229
9459
  sd = this.selected_dataset,
9230
- ioclass = ['', 'import', 'export'];
9460
+ ioclass = ['', 'import', 'export'],
9461
+ ciPrefixCompare = (a, b) => {
9462
+ const
9463
+ pa = a.split(':_').join(' '),
9464
+ pb = b.split(':_').join(' ');
9465
+ return ciCompare(pa, pb);
9466
+ };
9231
9467
  for(let d in MODEL.datasets) if(MODEL.datasets.hasOwnProperty(d) &&
9232
- // NOTE: do not list "black-boxed" entities
9468
+ // NOTE: do not list "black-boxed" entities
9233
9469
  !d.startsWith(UI.BLACK_BOX) &&
9234
9470
  // NOTE: do not list the equations dataset
9235
9471
  MODEL.datasets[d] !== MODEL.equations_dataset) {
@@ -9238,10 +9474,76 @@ class GUIDatasetManager extends DatasetManager {
9238
9474
  dnl.push(d);
9239
9475
  }
9240
9476
  }
9241
- dnl.sort(ciCompare);
9242
- let sdid = 'dstr';
9477
+ dnl.sort(ciPrefixCompare);
9478
+ // First determine indentation levels, prefixes and names
9479
+ const
9480
+ indent = [],
9481
+ pref_ids = [],
9482
+ names = [],
9483
+ pref_names = {},
9484
+ xids = [];
9243
9485
  for(let i = 0; i < dnl.length; i++) {
9244
- const d = MODEL.datasets[dnl[i]];
9486
+ const pref = UI.prefixesAndName(MODEL.datasets[dnl[i]].name);
9487
+ // NOTE: only the name part (so no prefixes at all) will be shown
9488
+ names.push(pref.pop());
9489
+ indent.push(pref.length);
9490
+ // NOTE: ignore case but join again with ": " because prefixes
9491
+ // can contain any character; only the prefixer is "reserved"
9492
+ const pref_id = pref.join(UI.PREFIXER).toLowerCase();
9493
+ pref_ids.push(pref_id);
9494
+ pref_names[pref_id] = pref;
9495
+ }
9496
+ let sdid = 'dstr',
9497
+ prev_id = '',
9498
+ ind_div = '';
9499
+ for(let i = 0; i < dnl.length; i++) {
9500
+ const
9501
+ d = MODEL.datasets[dnl[i]],
9502
+ pid = pref_ids[i];
9503
+ if(indent[i]) {
9504
+ ind_div = '<div class="ds-indent" style="width: ' +
9505
+ indent[i] * indent_px + 'px">\u25B9</div>';
9506
+ } else {
9507
+ ind_div = '';
9508
+ }
9509
+ // NOTE: empty string should not add a collapse/expand row
9510
+ if(pid && pid != prev_id && xids.indexOf(pid) < 0) {
9511
+ // NOTE: XX: aa may be followed by XX: YY: ZZ: bb, which requires
9512
+ // *two* collapsable lines: XX: YY and XX: YY: ZZ: before adding
9513
+ // XX: YY: ZZ: bb
9514
+ const
9515
+ ps = pid.split(UI.PREFIXER),
9516
+ pps = prev_id.split(UI.PREFIXER),
9517
+ pn = pref_names[pid],
9518
+ lpl = [];
9519
+ let lindent = 0;
9520
+ // Ignore identical leading prefixes
9521
+ while(ps.length > 0 && pps.length > 0 && ps[0] === pps[0]) {
9522
+ lpl.push(ps.shift());
9523
+ pps.shift();
9524
+ pn.shift();
9525
+ lindent++;
9526
+ }
9527
+ // Add a "collapse" row for each new prefix
9528
+ while(ps.length > 0) {
9529
+ lpl.push(ps.shift());
9530
+ lindent++;
9531
+ const lpid = lpl.join(UI.PREFIXER);
9532
+ dl.push(['<tr data-prefix="', lpid, '" class="dataset',
9533
+ '" onclick="DATASET_MANAGER.selectPrefixRow(event);"><td>',
9534
+ // NOTE: data-prefix="x" signals that this is an extra row
9535
+ (lindent > 0 ?
9536
+ '<div data-prefix="x" style="width: ' + lindent * indent_px +
9537
+ 'px"></div>' :
9538
+ ''),
9539
+ '<div data-prefix="x" class="tree-btn">',
9540
+ (this.expanded_rows.indexOf(lpid) >= 0 ? '\u25BC' : '\u25BA'),
9541
+ '</div>', pn.shift(), '</td></tr>'].join(''));
9542
+ // Add to the list to prevent multiple c/x-rows for the same prefix
9543
+ xids.push(lpid);
9544
+ }
9545
+ }
9546
+ prev_id = pid;
9245
9547
  let cls = ioclass[MODEL.ioType(d)];
9246
9548
  if(d.outcome) {
9247
9549
  cls += ' outcome';
@@ -9253,20 +9555,29 @@ class GUIDatasetManager extends DatasetManager {
9253
9555
  if(Object.keys(d.modifiers).length > 0) cls += ' modif';
9254
9556
  if(d.black_box) cls += ' blackbox';
9255
9557
  cls = cls.trim();
9256
- if(cls) cls = ' class="'+ cls + '"';
9558
+ if(cls) cls = ' class="' + cls + '"';
9257
9559
  if(d === sd) sdid += i;
9258
9560
  dl.push(['<tr id="dstr', i, '" class="dataset',
9259
9561
  (d === sd ? ' sel-set' : ''),
9260
9562
  (d.default_selector ? ' def-sel' : ''),
9563
+ '" data-prefix="', pid,
9261
9564
  '" onclick="DATASET_MANAGER.selectDataset(event, \'',
9262
9565
  dnl[i], '\');" onmouseover="DATASET_MANAGER.showInfo(\'', dnl[i],
9263
- '\', event.shiftKey);"><td', cls, '>', d.displayName,
9264
- '</td></tr>'].join(''));
9566
+ '\', event.shiftKey);"><td>', ind_div, '<div', cls, '>',
9567
+ names[i], '</td></tr>'].join(''));
9265
9568
  }
9266
9569
  this.dataset_table.innerHTML = dl.join('');
9267
- const btns = 'ds-data ds-rename ds-clone ds-delete';
9570
+ this.hideCollapsedRows();
9571
+ const e = document.getElementById(sdid);
9572
+ if(e) UI.scrollIntoView(e);
9573
+ this.updatePanes();
9574
+ }
9575
+
9576
+ updatePanes() {
9577
+ const
9578
+ sd = this.selected_dataset,
9579
+ btns = 'ds-data ds-clone ds-delete ds-rename';
9268
9580
  if(sd) {
9269
- this.dataset_table.innerHTML = dl.join('');
9270
9581
  this.properties.style.display = 'block';
9271
9582
  document.getElementById('dataset-default').innerHTML =
9272
9583
  VM.sig4Dig(sd.default_value) +
@@ -9296,12 +9607,11 @@ class GUIDatasetManager extends DatasetManager {
9296
9607
  this.outcome.classList.add('not-selected');
9297
9608
  }
9298
9609
  UI.setImportExportBox('dataset', MODEL.ioType(sd));
9299
- const e = document.getElementById(sdid);
9300
- UI.scrollIntoView(e);
9301
9610
  UI.enableButtons(btns);
9302
9611
  } else {
9303
9612
  this.properties.style.display = 'none';
9304
9613
  UI.disableButtons(btns);
9614
+ if(this.selected_prefix_row) UI.enableButtons('ds-rename');
9305
9615
  }
9306
9616
  this.updateModifiers();
9307
9617
  }
@@ -9360,6 +9670,17 @@ class GUIDatasetManager extends DatasetManager {
9360
9670
  } else {
9361
9671
  UI.disableButtons(btns);
9362
9672
  }
9673
+ // Check if dataset appears to "misuse" dataset modifiers
9674
+ const
9675
+ pml = sd.inferPrefixableModifiers,
9676
+ e = document.getElementById('ds-convert-modif-btn');
9677
+ if(pml.length > 0) {
9678
+ e.style.display = 'inline-block';
9679
+ e.title = 'Convert '+ pluralS(pml.length, 'modifier') +
9680
+ ' to prefixed dataset(s)';
9681
+ } else {
9682
+ e.style.display = 'none';
9683
+ }
9363
9684
  }
9364
9685
 
9365
9686
  showInfo(id, shift) {
@@ -9442,8 +9763,36 @@ class GUIDatasetManager extends DatasetManager {
9442
9763
  this.updateModifiers();
9443
9764
  }
9444
9765
 
9445
- promptForDataset() {
9446
- this.new_modal.element('name').value = '';
9766
+ get selectedPrefix() {
9767
+ // Returns the selected prefix (with its trailing colon-space)
9768
+ let prefix = '',
9769
+ tr = this.selected_prefix_row;
9770
+ while(tr) {
9771
+ const td = tr.firstElementChild;
9772
+ if(td && td.firstElementChild.dataset.prefix === 'x') {
9773
+ prefix = td.lastChild.textContent + UI.PREFIXER + prefix;
9774
+ tr = tr.previousSibling;
9775
+ } else {
9776
+ tr = null;
9777
+ }
9778
+ }
9779
+ return prefix;
9780
+ }
9781
+
9782
+ promptForDataset(shift=false) {
9783
+ // Shift signifies: add prefix of selected dataset (if any) to
9784
+ // the name field of the dialog
9785
+ let prefix = '';
9786
+ if(shift) {
9787
+ if(this.selected_dataset) {
9788
+ const p = UI.prefixesAndName(this.selected_dataset.name);
9789
+ p[p.length - 1] = '';
9790
+ prefix = p.join(UI.PREFIXER);
9791
+ } else if(this.selected_prefix) {
9792
+ prefix = this.selectedPrefix;
9793
+ }
9794
+ }
9795
+ this.new_modal.element('name').value = prefix;
9447
9796
  this.new_modal.show('name');
9448
9797
  }
9449
9798
 
@@ -9460,9 +9809,14 @@ class GUIDatasetManager extends DatasetManager {
9460
9809
  promptForName() {
9461
9810
  // Prompts the modeler for a new name for the selected dataset (if any)
9462
9811
  if(this.selected_dataset) {
9812
+ this.rename_modal.element('title').innerText = 'Rename dataset';
9463
9813
  this.rename_modal.element('name').value =
9464
9814
  this.selected_dataset.displayName;
9465
9815
  this.rename_modal.show('name');
9816
+ } else if(this.selected_prefix_row) {
9817
+ this.rename_modal.element('title').innerText = 'Rename datasets by prefix';
9818
+ this.rename_modal.element('name').value = this.selectedPrefix.slice(0, -2);
9819
+ this.rename_modal.show('name');
9466
9820
  }
9467
9821
  }
9468
9822
 
@@ -9477,16 +9831,67 @@ class GUIDatasetManager extends DatasetManager {
9477
9831
  // Then try to rename -- this may generate a warning
9478
9832
  if(this.selected_dataset.rename(n)) {
9479
9833
  this.rename_modal.hide();
9480
- this.updateDialog();
9481
- // Also update Chart manager and Experiment viewer, as these may
9482
- // display a variable name for this dataset
9483
- CHART_MANAGER.updateDialog();
9484
9834
  if(EXPERIMENT_MANAGER.selected_experiment) {
9485
9835
  EXPERIMENT_MANAGER.selected_experiment.inferVariables();
9486
9836
  }
9487
- EXPERIMENT_MANAGER.updateDialog();
9837
+ UI.updateControllerDialogs('CDEFJX');
9838
+ }
9839
+ } else if(this.selected_prefix_row) {
9840
+ // Create a list of datasets to be renamed
9841
+ let e = this.rename_modal.element('name'),
9842
+ prefix = e.value.trim();
9843
+ e.focus();
9844
+ while(prefix.endsWith(':')) prefix = prefix.slice(0, -1);
9845
+ // NOTE: prefix may be empty string, but otherwise should be a valid name
9846
+ if(prefix && !UI.validName(prefix)) {
9847
+ UI.warn('Invalid prefix');
9848
+ return;
9849
+ }
9850
+ prefix += UI.PREFIXER;
9851
+ const
9852
+ oldpref = this.selectedPrefix,
9853
+ key = oldpref.toLowerCase().split(UI.PREFIXER).join(':_'),
9854
+ newkey = prefix.toLowerCase().split(UI.PREFIXER).join(':_'),
9855
+ dsl = [];
9856
+ // No change if new prefix is identical to old prefix
9857
+ if(oldpref !== prefix) {
9858
+ for(let k in MODEL.datasets) if(MODEL.datasets.hasOwnProperty(k)) {
9859
+ if(k.startsWith(key)) dsl.push(k);
9860
+ }
9861
+ // NOTE: no check needed for mere upper/lower case changes
9862
+ if(newkey !== key) {
9863
+ let nc = 0;
9864
+ for(let i = 0; i < dsl.length; i++) {
9865
+ let nk = newkey + dsl[i].substring(key.length);
9866
+ if(MODEL.datasets[nk]) nc++;
9867
+ }
9868
+ if(nc) {
9869
+ UI.warn('Renaming ' + pluralS(dsl.length, 'dataset') +
9870
+ ' would cause ' + pluralS(nc, 'name conflict'));
9871
+ return;
9872
+ }
9873
+ }
9874
+ // Reset counts of effects of a rename operation
9875
+ this.entity_count = 0;
9876
+ this.expression_count = 0;
9877
+ // Rename datasets one by one, suppressing notifications
9878
+ for(let i = 0; i < dsl.length; i++) {
9879
+ const d = MODEL.datasets[dsl[i]];
9880
+ d.rename(d.displayName.replace(oldpref, prefix), false);
9881
+ }
9882
+ let msg = 'Renamed ' + pluralS(dsl.length, 'dataset');
9883
+ if(MODEL.variable_count) msg += ', and updated ' +
9884
+ pluralS(MODEL.variable_count, 'variable') + ' in ' +
9885
+ pluralS(MODEL.expression_count, 'expression');
9886
+ UI.notify(msg);
9887
+ if(EXPERIMENT_MANAGER.selected_experiment) {
9888
+ EXPERIMENT_MANAGER.selected_experiment.inferVariables();
9889
+ }
9890
+ UI.updateControllerDialogs('CDEFJX');
9891
+ this.selectPrefixRow(prefix);
9488
9892
  }
9489
9893
  }
9894
+ this.rename_modal.hide();
9490
9895
  }
9491
9896
 
9492
9897
  cloneDataset() {
@@ -9626,16 +10031,13 @@ class GUIDatasetManager extends DatasetManager {
9626
10031
  this.deleteModifier();
9627
10032
  this.selected_modifier = m;
9628
10033
  // Update all chartvariables referencing this dataset + old selector
10034
+ const vl = MODEL.datasetChartVariables;
9629
10035
  let cv_cnt = 0;
9630
- for(let i = 0; i < MODEL.charts.length; i++) {
9631
- const c = MODEL.charts[i];
9632
- for(let j = 0; j < c.variables.length; j++) {
9633
- const v = c.variables[j];
9634
- if(v.object === this.selected_dataset &&
9635
- v.attribute === oldm.selector) {
9636
- v.attribute = m.selector;
9637
- cv_cnt++;
9638
- }
10036
+ for(let i = 0; i < vl.length; i++) {
10037
+ if(v.object === this.selected_dataset &&
10038
+ v.attribute === oldm.selector) {
10039
+ v.attribute = m.selector;
10040
+ cv_cnt++;
9639
10041
  }
9640
10042
  }
9641
10043
  // Also replace old selector in all expressions (count these as well)
@@ -9649,7 +10051,7 @@ class GUIDatasetManager extends DatasetManager {
9649
10051
  UI.notify('Updated ' + msg.join(' and '));
9650
10052
  // Also update these stay-on-top dialogs, as they may display a
9651
10053
  // variable name for this dataset + modifier
9652
- UI.updateControllerDialogs('CDEFX');
10054
+ UI.updateControllerDialogs('CDEFJX');
9653
10055
  }
9654
10056
  // NOTE: update dimensions only if dataset now has 2 or more modifiers
9655
10057
  // (ignoring those with wildcards)
@@ -9701,6 +10103,138 @@ class GUIDatasetManager extends DatasetManager {
9701
10103
  }
9702
10104
  }
9703
10105
 
10106
+ promptToConvertModifiers() {
10107
+ // Convert modifiers of selected dataset to new prefixed datasets
10108
+ const
10109
+ ds = this.selected_dataset,
10110
+ md = this.conversion_modal;
10111
+ if(ds) {
10112
+ md.element('prefix').value = ds.displayName;
10113
+ md.show('prefix');
10114
+ }
10115
+ }
10116
+
10117
+ convertModifiers() {
10118
+ // Convert modifiers of selected dataset to new prefixed datasets
10119
+ if(!this.selected_dataset) return;
10120
+ const
10121
+ ds = this.selected_dataset,
10122
+ md = this.conversion_modal,
10123
+ e = md.element('prefix');
10124
+ let prefix = e.value.trim(),
10125
+ vcount = 0;
10126
+ e.focus();
10127
+ while(prefix.endsWith(':')) prefix = prefix.slice(0, -1);
10128
+ // NOTE: prefix may be empty string, but otherwise should be a valid name
10129
+ if(!UI.validName(prefix)) {
10130
+ UI.warn('Invalid prefix');
10131
+ return;
10132
+ }
10133
+ prefix += UI.PREFIXER;
10134
+ const
10135
+ dsn = ds.displayName,
10136
+ pml = ds.inferPrefixableModifiers,
10137
+ xl = MODEL.allExpressions,
10138
+ vl = MODEL.datasetVariables,
10139
+ nl = MODEL.notesWithTags;
10140
+ for(let i = 0; i < pml.length; i++) {
10141
+ // Create prefixed dataset with correct default value
10142
+ const
10143
+ m = pml[i],
10144
+ sel = m.selector,
10145
+ newds = MODEL.addDataset(prefix + sel);
10146
+ if(newds) {
10147
+ // Retain properties of the "parent" dataset
10148
+ newds.scale_unit = ds.scale_unit;
10149
+ newds.time_scale = ds.time_scale;
10150
+ newds.time_unit = ds.time_unit;
10151
+ // Set modifier's expression result as default value
10152
+ newds.default_value = m.expression.result(1);
10153
+ // Remove the modifier from the dataset
10154
+ delete ds.modifiers[UI.nameToID(sel)];
10155
+ // If it was the dataset default modifier, clear this default
10156
+ if(sel === ds.default_selector) ds.default_selector = '';
10157
+ // Rename variable in charts
10158
+ const
10159
+ from = dsn + UI.OA_SEPARATOR + sel,
10160
+ to = newds.displayName;
10161
+ for(let j = 0; j < vl.length; j++) {
10162
+ const v = vl[j];
10163
+ // NOTE: variable should match original dataset + selector
10164
+ if(v.displayName === from) {
10165
+ // Change to new dataset WITHOUT selector
10166
+ v.object = newds;
10167
+ v.attribute = '';
10168
+ vcount++;
10169
+ }
10170
+ }
10171
+ // Rename variable in the Sensitivity Analysis
10172
+ for(let j = 0; j < MODEL.sensitivity_parameters.length; j++) {
10173
+ if(MODEL.sensitivity_parameters[j] === from) {
10174
+ MODEL.sensitivity_parameters[j] = to;
10175
+ vcount++;
10176
+ }
10177
+ }
10178
+ for(let j = 0; j < MODEL.sensitivity_outcomes.length; j++) {
10179
+ if(MODEL.sensitivity_outcomes[j] === from) {
10180
+ MODEL.sensitivity_outcomes[j] = to;
10181
+ vcount++;
10182
+ }
10183
+ }
10184
+ // Rename variable in expressions and notes
10185
+ const re = new RegExp(
10186
+ // Handle multiple spaces between words
10187
+ '\\[\\s*' + escapeRegex(from).replace(/\s+/g, '\\s+')
10188
+ // Handle spaces around the separator |
10189
+ .replace('\\|', '\\s*\\|\\s*') +
10190
+ // Pattern ends at any character that is invalid for a
10191
+ // dataset modifier selector (unlike equation names)
10192
+ '\\s*[^a-zA-Z0-9\\+\\-\\%\\_]', 'gi');
10193
+ for(let j = 0; j < xl.length; j++) {
10194
+ const
10195
+ x = xl[j],
10196
+ matches = x.text.match(re);
10197
+ if(matches) {
10198
+ for(let k = 0; k < matches.length; k++) {
10199
+ // NOTE: each match will start with the opening bracket,
10200
+ // but end with the first "non-selector" character, which
10201
+ // will typically be ']', but may also be '@' (and now that
10202
+ // units can be converted, also the '>' of the arrow '->')
10203
+ x.text = x.text.replace(matches[k], '[' + to + matches[k].slice(-1));
10204
+ vcount ++;
10205
+ }
10206
+ // Force recompilation
10207
+ x.code = null;
10208
+ }
10209
+ }
10210
+ for(let j = 0; j < nl.length; j++) {
10211
+ const
10212
+ n = nl[j],
10213
+ matches = n.contents.match(re);
10214
+ if(matches) {
10215
+ for(let k = 0; k < matches.length; k++) {
10216
+ // See NOTE above for the use of `slice` here
10217
+ n.contents = n.contents.replace(matches[k], '[' + to + matches[k].slice(-1));
10218
+ vcount ++;
10219
+ }
10220
+ // Note fields must be parsed again
10221
+ n.parsed = false;
10222
+ }
10223
+ }
10224
+ }
10225
+ }
10226
+ if(vcount) UI.notify('Renamed ' + pluralS(vcount, 'variable') +
10227
+ ' throughout the model');
10228
+ // Delete the original dataset unless it has series data
10229
+ if(ds.data.length === 0) this.deleteDataset();
10230
+ MODEL.updateDimensions();
10231
+ this.selected_dataset = null;
10232
+ this.selected_prefix_row = null;
10233
+ this.updateDialog();
10234
+ md.hide();
10235
+ this.selectPrefixRow(prefix);
10236
+ }
10237
+
9704
10238
  updateLine() {
9705
10239
  const
9706
10240
  ln = document.getElementById('series-line-number'),
@@ -10061,7 +10595,7 @@ class EquationManager {
10061
10595
  UI.notify('Updated ' + msg.join(' and '));
10062
10596
  // Also update these stay-on-top dialogs, as they may display a
10063
10597
  // variable name for this dataset + modifier
10064
- UI.updateControllerDialogs('CDEFX');
10598
+ UI.updateControllerDialogs('CDEFJX');
10065
10599
  }
10066
10600
  // Always close the name prompt dialog, and update the equation manager
10067
10601
  this.rename_modal.hide();
@@ -10182,6 +10716,12 @@ class GUIChartManager extends ChartManager {
10182
10716
  'click', () => CHART_MANAGER.renameEquation());
10183
10717
  document.getElementById('chart-edit-equation-btn').addEventListener(
10184
10718
  'click', () => CHART_MANAGER.editEquation());
10719
+ document.getElementById('variable-color').addEventListener(
10720
+ 'mouseenter', () => CHART_MANAGER.showPasteColor());
10721
+ document.getElementById('variable-color').addEventListener(
10722
+ 'mouseleave', () => CHART_MANAGER.hidePasteColor());
10723
+ document.getElementById('variable-color').addEventListener(
10724
+ 'click', (event) => CHART_MANAGER.copyPasteColor(event));
10185
10725
  // NOTE: uses the color picker developed by James Daniel
10186
10726
  this.color_picker = new iro.ColorPicker("#color-picker", {
10187
10727
  width: 92,
@@ -10229,6 +10769,7 @@ class GUIChartManager extends ChartManager {
10229
10769
  this.options_shown = true;
10230
10770
  this.setRunsChart(false);
10231
10771
  this.last_time_selected = 0;
10772
+ this.paste_color = '';
10232
10773
  }
10233
10774
 
10234
10775
  enterKey() {
@@ -10283,14 +10824,13 @@ class GUIChartManager extends ChartManager {
10283
10824
  const
10284
10825
  n = ev.dataTransfer.getData('text'),
10285
10826
  obj = MODEL.objectByID(n);
10827
+ ev.preventDefault();
10286
10828
  if(!obj) {
10287
10829
  UI.alert(`Unknown entity ID "${n}"`);
10288
10830
  } else if(this.chart_index >= 0) {
10289
- // Only accept when all conditions are met
10290
- ev.preventDefault();
10291
10831
  if(obj instanceof DatasetModifier) {
10292
10832
  // Equations can be added directly as chart variable
10293
- this.addVariable(obj.name);
10833
+ this.addVariable(obj.selector);
10294
10834
  return;
10295
10835
  }
10296
10836
  // For other entities, the attribute must be specified
@@ -10670,7 +11210,16 @@ class GUIChartManager extends ChartManager {
10670
11210
  this.variable_index = vi;
10671
11211
  this.updateDialog();
10672
11212
  }
10673
-
11213
+
11214
+ setColorPicker(color) {
11215
+ // Robust way to set iro color picker color
11216
+ try {
11217
+ this.color_picker.color.hexString = color;
11218
+ } catch(e) {
11219
+ this.color_picker.color.rgbString = color;
11220
+ }
11221
+ }
11222
+
10674
11223
  editVariable() {
10675
11224
  // Shows the edit (or rather: format) variable dialog
10676
11225
  if(this.chart_index >= 0 && this.variable_index >= 0) {
@@ -10680,11 +11229,7 @@ class GUIChartManager extends ChartManager {
10680
11229
  this.variable_modal.element('scale').value = VM.sig4Dig(cv.scale_factor);
10681
11230
  this.variable_modal.element('width').value = VM.sig4Dig(cv.line_width);
10682
11231
  this.variable_modal.element('color').style.backgroundColor = cv.color;
10683
- try {
10684
- this.color_picker.color.hexString = cv.color;
10685
- } catch(e) {
10686
- this.color_picker.color.rgbString = cv.color;
10687
- }
11232
+ this.setColorPicker(cv.color);
10688
11233
  // Show change equation buttons only for equation variables
10689
11234
  if(cv.object === MODEL.equations_dataset) {
10690
11235
  this.change_equation_btns.style.display = 'block';
@@ -10695,6 +11240,34 @@ class GUIChartManager extends ChartManager {
10695
11240
  }
10696
11241
  }
10697
11242
 
11243
+ showPasteColor() {
11244
+ // Show last copied color (if any) as smaller square next to color box
11245
+ if(this.paste_color) {
11246
+ const pc = this.variable_modal.element('paste-color');
11247
+ pc.style.backgroundColor = this.paste_color;
11248
+ pc.style.display = 'inline-block';
11249
+ }
11250
+ }
11251
+
11252
+ hidePasteColor() {
11253
+ // Hide paste color box
11254
+ this.variable_modal.element('paste-color').style.display = 'none';
11255
+ }
11256
+
11257
+ copyPasteColor(event) {
11258
+ // Store the current color as past color, or set it to the current
11259
+ // paste color if this is defined and the Shift key was pressed
11260
+ event.stopPropagation();
11261
+ const cbox = this.variable_modal.element('color');
11262
+ if(event.shiftKey && this.paste_color) {
11263
+ cbox.style.backgroundColor = this.paste_color;
11264
+ this.setColorPicker(this.paste_color);
11265
+ } else {
11266
+ this.paste_color = cbox.style.backgroundColor;
11267
+ this.showPasteColor();
11268
+ }
11269
+ }
11270
+
10698
11271
  toggleVariable(vi) {
10699
11272
  window.event.stopPropagation();
10700
11273
  if(vi >= 0 && this.chart_index >= 0) {
@@ -10925,7 +11498,7 @@ class GUIChartManager extends ChartManager {
10925
11498
  }
10926
11499
 
10927
11500
  renderChartAsPNG() {
10928
- localStorage.removeItem('png-url');
11501
+ window.localStorage.removeItem('png-url');
10929
11502
  FILE_MANAGER.renderSVGAsPNG(MODEL.charts[this.chart_index].svg);
10930
11503
  }
10931
11504
 
@@ -12050,7 +12623,8 @@ class GUIExperimentManager extends ExperimentManager {
12050
12623
  x.charts[i].title, '</td></tr>'].join(''));
12051
12624
  }
12052
12625
  this.chart_table.innerHTML = tr.join('');
12053
- if(x.charts.length === 0) canview = false;
12626
+ // Do not show viewer unless at least 1 dependent variable has been defined
12627
+ if(x.charts.length === 0 && MODEL.outcomeNames.length === 0) canview = false;
12054
12628
  if(tr.length >= this.suitable_charts.length) {
12055
12629
  document.getElementById('xp-c-add-btn').classList.add('v-disab');
12056
12630
  } else {
@@ -12070,7 +12644,7 @@ class GUIExperimentManager extends ExperimentManager {
12070
12644
  dbtn.classList.add('v-disab');
12071
12645
  cbtn.classList.add('v-disab');
12072
12646
  }
12073
- // Enable viewing only if > 1 dimensions and > 1 charts
12647
+ // Enable viewing only if > 1 dimensions and > 1 outcome variables
12074
12648
  if(canview) {
12075
12649
  UI.enableButtons('xp-view');
12076
12650
  } else {
@@ -12155,14 +12729,11 @@ class GUIExperimentManager extends ExperimentManager {
12155
12729
  const x = this.selected_experiment;
12156
12730
  if(x) {
12157
12731
  x.inferVariables();
12158
- if(x.selected_variable === '') {
12159
- x.selected_variable = x.variables[0].displayName;
12160
- }
12161
12732
  const
12162
12733
  ol = [],
12163
12734
  vl = MODEL.outcomeNames;
12164
12735
  for(let i = 0; i < x.variables.length; i++) {
12165
- vl.push(x.variables[i].displayName);
12736
+ addDistinct(x.variables[i].displayName, vl);
12166
12737
  }
12167
12738
  vl.sort(ciCompare);
12168
12739
  for(let i = 0; i < vl.length; i++) {
@@ -12171,6 +12742,9 @@ class GUIExperimentManager extends ExperimentManager {
12171
12742
  '>', vl[i], '</option>'].join(''));
12172
12743
  }
12173
12744
  document.getElementById('viewer-variable').innerHTML = ol.join('');
12745
+ if(x.selected_variable === '') {
12746
+ x.selected_variable = vl[0];
12747
+ }
12174
12748
  }
12175
12749
  }
12176
12750
 
@@ -14592,7 +15166,7 @@ class Finder {
14592
15166
  const
14593
15167
  raw = escapeRegex(se.displayName),
14594
15168
  re = new RegExp(
14595
- '\\[\\s*' + raw.replace(/\s+/g, '\\s+') + '\\s*[\\|\\@\\]]');
15169
+ '\\[\\s*!?' + raw.replace(/\s+/g, '\\s+') + '\\s*[\\|\\@\\]]');
14596
15170
  // Check actor weight expressions
14597
15171
  for(let k in MODEL.actors) if(MODEL.actors.hasOwnProperty(k)) {
14598
15172
  const a = MODEL.actors[k];