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.
- package/README.md +6 -6
- package/console.js +2 -2
- package/package.json +2 -2
- package/static/images/paste.png +0 -0
- package/static/index.html +35 -6
- package/static/linny-r.css +88 -16
- package/static/scripts/linny-r-ctrl.js +22 -4
- package/static/scripts/linny-r-gui.js +637 -63
- package/static/scripts/linny-r-model.js +185 -29
- package/static/scripts/linny-r-utils.js +49 -11
- package/static/scripts/linny-r-vm.js +29 -18
@@ -12,7 +12,7 @@ dialogs, the main drawing canvas, and event handler functions.
|
|
12
12
|
*/
|
13
13
|
|
14
14
|
/*
|
15
|
-
Copyright (c) 2017-
|
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': '
|
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
|
-
() =>
|
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
|
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
|
-
|
5469
|
+
px)) return false;
|
5353
5470
|
// NOTE: pace expression must be *static* and >= 1
|
5354
|
-
n =
|
5355
|
-
if(!
|
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 ≥ 1');
|
5358
5475
|
// Restore original expression string
|
5359
|
-
|
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
|
-
|
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
|
-
|
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(
|
9242
|
-
|
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
|
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, '>',
|
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
|
-
|
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
|
-
|
9446
|
-
|
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
|
-
|
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 <
|
9631
|
-
|
9632
|
-
|
9633
|
-
|
9634
|
-
|
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('
|
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('
|
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.
|
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
|
-
|
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
|
-
|
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
|
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
|
-
|
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
|
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];
|