linny-r 1.2.1 → 1.3.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +6 -6
- package/console.js +139 -7
- package/package.json +2 -2
- package/server.js +15 -11
- package/static/images/paperclip.png +0 -0
- package/static/images/paste.png +0 -0
- package/static/index.html +40 -8
- package/static/linny-r.css +97 -19
- package/static/scripts/linny-r-ctrl.js +22 -4
- package/static/scripts/linny-r-gui.js +723 -140
- 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 +32 -20
@@ -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
|
@@ -3485,81 +3555,6 @@ class GUIController extends Controller {
|
|
3485
3555
|
// Methods related to draggable & resizable dialogs
|
3486
3556
|
//
|
3487
3557
|
|
3488
|
-
toggleDialog(e) {
|
3489
|
-
e = e || window.event;
|
3490
|
-
e.preventDefault();
|
3491
|
-
e.stopImmediatePropagation();
|
3492
|
-
// Infer dialog identifier from target element
|
3493
|
-
const
|
3494
|
-
dlg = e.target.id.split('-')[0],
|
3495
|
-
tde = document.getElementById(dlg + '-dlg'),
|
3496
|
-
was_hidden = this.hidden(tde.id);
|
3497
|
-
let mgr = tde.getAttribute('data-manager');
|
3498
|
-
if(mgr) mgr = window[mgr];
|
3499
|
-
// NOTE: prevent modeler from viewing charts while an experiment is running
|
3500
|
-
if(dlg === 'chart' && was_hidden && MODEL.running_experiment) {
|
3501
|
-
UI.notify(UI.NOTICE.NO_CHARTS);
|
3502
|
-
mgr.visible = false;
|
3503
|
-
return;
|
3504
|
-
}
|
3505
|
-
this.toggle(tde.id);
|
3506
|
-
if(mgr) mgr.visible = was_hidden;
|
3507
|
-
// Open at position after last drag (recorded in DOM data attributes)
|
3508
|
-
let t = tde.getAttribute('data-top'),
|
3509
|
-
l = tde.getAttribute('data-left');
|
3510
|
-
// Make dialog appear in screen center the first time it is shown
|
3511
|
-
if(t === null || l === null) {
|
3512
|
-
const cs = window.getComputedStyle(tde);
|
3513
|
-
t = ((window.innerHeight - parseFloat(cs.height)) / 2) + 'px';
|
3514
|
-
l = ((window.innerWidth - parseFloat(cs.width)) / 2) + 'px';
|
3515
|
-
tde.style.top = t;
|
3516
|
-
tde.style.left = l;
|
3517
|
-
}
|
3518
|
-
if(!this.hidden(tde.id)) {
|
3519
|
-
// Add dialog to "showing" list, and adjust z-indices
|
3520
|
-
this.dr_dialog_order.push(tde);
|
3521
|
-
this.reorderDialogs();
|
3522
|
-
// Update the diagram if its manager has been specified
|
3523
|
-
if(mgr) {
|
3524
|
-
mgr.visible = true;
|
3525
|
-
mgr.updateDialog();
|
3526
|
-
if(mgr === DOCUMENTATION_MANAGER) {
|
3527
|
-
if(this.info_line.innerHTML.length === 0) {
|
3528
|
-
mgr.title.innerHTML = 'About Linny-R';
|
3529
|
-
mgr.viewer.innerHTML = mgr.about_linny_r;
|
3530
|
-
mgr.edit_btn.classList.remove('enab');
|
3531
|
-
mgr.edit_btn.classList.add('disab');
|
3532
|
-
}
|
3533
|
-
UI.drawDiagram(MODEL);
|
3534
|
-
}
|
3535
|
-
}
|
3536
|
-
} else {
|
3537
|
-
const doi = this.dr_dialog_order.indexOf(tde);
|
3538
|
-
// NOTE: doi should ALWAYS be >= 0 because dialog WAS showing
|
3539
|
-
if(doi >= 0) {
|
3540
|
-
this.dr_dialog_order.splice(doi, 1);
|
3541
|
-
this.reorderDialogs();
|
3542
|
-
}
|
3543
|
-
if(mgr) {
|
3544
|
-
mgr.visible = true;
|
3545
|
-
if(mgr === DOCUMENTATION_MANAGER) {
|
3546
|
-
mgr.visible = false;
|
3547
|
-
mgr.title.innerHTML = 'Documentation';
|
3548
|
-
UI.drawDiagram(MODEL);
|
3549
|
-
}
|
3550
|
-
}
|
3551
|
-
}
|
3552
|
-
UI.buttons[dlg].classList.toggle('stay-activ');
|
3553
|
-
}
|
3554
|
-
|
3555
|
-
reorderDialogs() {
|
3556
|
-
let z = 10;
|
3557
|
-
for(let i = 0; i < this.dr_dialog_order.length; i++) {
|
3558
|
-
this.dr_dialog_order[i].style.zIndex = z;
|
3559
|
-
z += 5;
|
3560
|
-
}
|
3561
|
-
}
|
3562
|
-
|
3563
3558
|
draggableDialog(d) {
|
3564
3559
|
// Make dialog draggable
|
3565
3560
|
const
|
@@ -3685,7 +3680,7 @@ class GUIController extends Controller {
|
|
3685
3680
|
UI.dr_dialog.style.width = Math.max(minw, w + dw) + 'px';
|
3686
3681
|
UI.dr_dialog.style.height = Math.max(minh, h + dh) + 'px';
|
3687
3682
|
// Update the dialog if its manager has been specified
|
3688
|
-
const mgr = UI.dr_dialog.
|
3683
|
+
const mgr = UI.dr_dialog.dataset.manager;
|
3689
3684
|
if(mgr) window[mgr].updateDialog();
|
3690
3685
|
}
|
3691
3686
|
|
@@ -3696,6 +3691,90 @@ class GUIController extends Controller {
|
|
3696
3691
|
}
|
3697
3692
|
}
|
3698
3693
|
|
3694
|
+
toggleDialog(e) {
|
3695
|
+
// Hide dialog if visible, or show it if not, and update the
|
3696
|
+
// order of appearance so that this dialog appears on top
|
3697
|
+
e = e || window.event;
|
3698
|
+
e.preventDefault();
|
3699
|
+
e.stopImmediatePropagation();
|
3700
|
+
// Infer dialog identifier from target element
|
3701
|
+
const
|
3702
|
+
dlg = e.target.id.split('-')[0],
|
3703
|
+
tde = document.getElementById(dlg + '-dlg');
|
3704
|
+
// NOTE: manager attribute is a string, e.g. 'MONITOR' or 'CHART_MANAGER'
|
3705
|
+
let mgr = tde.dataset.manager,
|
3706
|
+
was_hidden = this.hidden(tde.id);
|
3707
|
+
if(mgr) {
|
3708
|
+
// Dialog has a manager object => let `mgr` point to it
|
3709
|
+
mgr = window[mgr];
|
3710
|
+
// Manager object attributes are more reliable than DOM element
|
3711
|
+
// style attributes, so update the visibility status
|
3712
|
+
was_hidden = !mgr.visible;
|
3713
|
+
}
|
3714
|
+
// NOTE: modeler should not view charts while an experiment is
|
3715
|
+
// running, so do NOT toggle when the Chart Manager is NOT visible
|
3716
|
+
if(dlg === 'chart' && was_hidden && MODEL.running_experiment) {
|
3717
|
+
UI.notify(UI.NOTICE.NO_CHARTS);
|
3718
|
+
return;
|
3719
|
+
}
|
3720
|
+
// Otherwise, toggle the dialog visibility
|
3721
|
+
this.toggle(tde.id);
|
3722
|
+
UI.buttons[dlg].classList.toggle('stay-activ');
|
3723
|
+
if(mgr) mgr.visible = was_hidden;
|
3724
|
+
let t, l;
|
3725
|
+
if(top in tde.dataset && left in tde.dataset) {
|
3726
|
+
// Open at position after last drag (recorded in DOM data attributes)
|
3727
|
+
t = tde.dataset.top;
|
3728
|
+
l = tde.dataset.left;
|
3729
|
+
} else {
|
3730
|
+
// Make dialog appear in screen center the first time it is shown
|
3731
|
+
const cs = window.getComputedStyle(tde);
|
3732
|
+
t = ((window.innerHeight - parseFloat(cs.height)) / 2) + 'px';
|
3733
|
+
l = ((window.innerWidth - parseFloat(cs.width)) / 2) + 'px';
|
3734
|
+
tde.style.top = t;
|
3735
|
+
tde.style.left = l;
|
3736
|
+
}
|
3737
|
+
if(was_hidden) {
|
3738
|
+
// Add activated dialog to "showing" list, and adjust z-indices
|
3739
|
+
this.dr_dialog_order.push(tde);
|
3740
|
+
this.reorderDialogs();
|
3741
|
+
// Update the diagram if its manager has been specified
|
3742
|
+
if(mgr) {
|
3743
|
+
mgr.updateDialog();
|
3744
|
+
if(mgr === DOCUMENTATION_MANAGER) {
|
3745
|
+
if(this.info_line.innerHTML.length === 0) {
|
3746
|
+
mgr.title.innerHTML = 'About Linny-R';
|
3747
|
+
mgr.viewer.innerHTML = mgr.about_linny_r;
|
3748
|
+
mgr.edit_btn.classList.remove('enab');
|
3749
|
+
mgr.edit_btn.classList.add('disab');
|
3750
|
+
}
|
3751
|
+
UI.drawDiagram(MODEL);
|
3752
|
+
}
|
3753
|
+
}
|
3754
|
+
} else {
|
3755
|
+
const doi = this.dr_dialog_order.indexOf(tde);
|
3756
|
+
// NOTE: doi should ALWAYS be >= 0 because dialog WAS showing
|
3757
|
+
if(doi >= 0) {
|
3758
|
+
this.dr_dialog_order.splice(doi, 1);
|
3759
|
+
this.reorderDialogs();
|
3760
|
+
}
|
3761
|
+
if(mgr === DOCUMENTATION_MANAGER) {
|
3762
|
+
mgr.title.innerHTML = 'Documentation';
|
3763
|
+
UI.drawDiagram(MODEL);
|
3764
|
+
}
|
3765
|
+
}
|
3766
|
+
}
|
3767
|
+
|
3768
|
+
reorderDialogs() {
|
3769
|
+
// Set z-index of draggable dialogs according to their order
|
3770
|
+
// (most recently shown or clicked on top)
|
3771
|
+
let z = 10;
|
3772
|
+
for(let i = 0; i < this.dr_dialog_order.length; i++) {
|
3773
|
+
this.dr_dialog_order[i].style.zIndex = z;
|
3774
|
+
z += 5;
|
3775
|
+
}
|
3776
|
+
}
|
3777
|
+
|
3699
3778
|
//
|
3700
3779
|
// Button functionality
|
3701
3780
|
//
|
@@ -3722,7 +3801,7 @@ class GUIController extends Controller {
|
|
3722
3801
|
// Updates the buttons on the main GUI toolbars
|
3723
3802
|
const
|
3724
3803
|
node_btns = 'process product link constraint cluster note ',
|
3725
|
-
edit_btns = 'clone delete undo redo ',
|
3804
|
+
edit_btns = 'clone paste delete undo redo ',
|
3726
3805
|
model_btns = 'settings save actors dataset equation chart ' +
|
3727
3806
|
'diagram savediagram finder monitor solve';
|
3728
3807
|
if(MODEL === null) {
|
@@ -3749,6 +3828,7 @@ class GUIController extends Controller {
|
|
3749
3828
|
this.active_button = this.stayActiveButton;
|
3750
3829
|
this.disableButtons(edit_btns);
|
3751
3830
|
if(MODEL.selection.length > 0) this.enableButtons('clone delete');
|
3831
|
+
if(this.canPaste) this.enableButtons('paste');
|
3752
3832
|
// Only allow target seeking when some target or process constraint is defined
|
3753
3833
|
if(MODEL.hasTargets) this.enableButtons('solve');
|
3754
3834
|
var u = UNDO_STACK.canUndo;
|
@@ -4424,6 +4504,15 @@ class GUIController extends Controller {
|
|
4424
4504
|
this.stepBack(e);
|
4425
4505
|
} else if(e.keyCode === 39) {
|
4426
4506
|
this.stepForward(e);
|
4507
|
+
} else if(e.altKey && [67, 77].indexOf(e.keyCode) >= 0) {
|
4508
|
+
// Special shortcut keys for "clone selection" and "model settings"
|
4509
|
+
const be = new Event('click');
|
4510
|
+
be.altKey = true;
|
4511
|
+
if(e.keyCode === 67) {
|
4512
|
+
this.buttons.clone.dispatchEvent(be);
|
4513
|
+
} else {
|
4514
|
+
this.buttons.settings.dispatchEvent(be);
|
4515
|
+
}
|
4427
4516
|
} else if(!e.shiftKey && !e.altKey &&
|
4428
4517
|
(!topmod || [65, 67, 86].indexOf(e.keyCode) < 0)) {
|
4429
4518
|
// Interpret special keys as shortcuts unless a modal dialog is open
|
@@ -5137,7 +5226,42 @@ class GUIController extends Controller {
|
|
5137
5226
|
cancelCloneSelection() {
|
5138
5227
|
this.modals.clone.hide();
|
5139
5228
|
this.updateButtons();
|
5140
|
-
}
|
5229
|
+
}
|
5230
|
+
|
5231
|
+
copySelection() {
|
5232
|
+
// Save selection as XML in local storage of the browser
|
5233
|
+
const xml = MODEL.selectionAsXML;
|
5234
|
+
//console.log('HERE copy xml', xml);
|
5235
|
+
if(xml) {
|
5236
|
+
window.localStorage.setItem('Linny-R-selection-XML', xml);
|
5237
|
+
this.updateButtons();
|
5238
|
+
this.notify('Selection copied, but cannot be pasted yet -- Use Alt-C to clone');
|
5239
|
+
}
|
5240
|
+
}
|
5241
|
+
|
5242
|
+
get canPaste() {
|
5243
|
+
const xml = window.localStorage.getItem('Linny-R-selection-XML');
|
5244
|
+
if(xml) {
|
5245
|
+
const timestamp = xml.match(/<copy timestamp="(\d+)"/);
|
5246
|
+
if(timestamp) {
|
5247
|
+
if(Date.now() - parseInt(timestamp[1]) < 8*3600000) return true;
|
5248
|
+
}
|
5249
|
+
// Remove XML from local storage if older than 8 hours
|
5250
|
+
window.localStorage.removeItem('Linny-R-selection-XML');
|
5251
|
+
}
|
5252
|
+
return false;
|
5253
|
+
}
|
5254
|
+
|
5255
|
+
pasteSelection() {
|
5256
|
+
// If selection has been saved as XML in local storage, test to
|
5257
|
+
// see whether PASTE would result in name conflicts, and if so,
|
5258
|
+
// open the name conflict resolution window
|
5259
|
+
const xml = window.localStorage.getItem('Linny-R-selection-XML');
|
5260
|
+
if(xml) {
|
5261
|
+
// @@ TO DO!
|
5262
|
+
this.notify('Paste not implemented yet -- WORK IN PROGRESS!');
|
5263
|
+
}
|
5264
|
+
}
|
5141
5265
|
|
5142
5266
|
//
|
5143
5267
|
// Interaction with modal dialogs to modify model or entity properties
|
@@ -5346,17 +5470,20 @@ class GUIController extends Controller {
|
|
5346
5470
|
if(!this.updateExpressionInput(
|
5347
5471
|
'process-IL', 'initial level', p.initial_level)) return false;
|
5348
5472
|
// Store original expression string
|
5349
|
-
const
|
5473
|
+
const
|
5474
|
+
px = p.pace_expression,
|
5475
|
+
pxt = p.pace_expression.text;
|
5350
5476
|
// Validate expression
|
5351
5477
|
if(!this.updateExpressionInput('process-pace', 'level change frequency',
|
5352
|
-
|
5478
|
+
px)) return false;
|
5353
5479
|
// NOTE: pace expression must be *static* and >= 1
|
5354
|
-
n =
|
5355
|
-
if(!
|
5480
|
+
n = px.result(1);
|
5481
|
+
if(!px.isStatic || n < 1) {
|
5356
5482
|
md.element('pace').focus();
|
5357
5483
|
this.warn('Level change frequency must be static and ≥ 1');
|
5358
5484
|
// Restore original expression string
|
5359
|
-
|
5485
|
+
px.text = pxt;
|
5486
|
+
px.code = null;
|
5360
5487
|
return false;
|
5361
5488
|
}
|
5362
5489
|
// Ignore fraction if a real number was entered.
|
@@ -5857,7 +5984,7 @@ class GUIMonitor {
|
|
5857
5984
|
(event) => {
|
5858
5985
|
const el = event.target;
|
5859
5986
|
el.classList.add('sel-pb');
|
5860
|
-
MONITOR.showBlock(el.
|
5987
|
+
MONITOR.showBlock(el.dataset.blk);
|
5861
5988
|
},
|
5862
5989
|
false);
|
5863
5990
|
this.progress_bar.appendChild(n);
|
@@ -6466,7 +6593,7 @@ class GUIFileManager {
|
|
6466
6593
|
}
|
6467
6594
|
|
6468
6595
|
renderDiagramAsPNG() {
|
6469
|
-
localStorage.removeItem('png-url');
|
6596
|
+
window.localStorage.removeItem('png-url');
|
6470
6597
|
UI.paper.fitToSize();
|
6471
6598
|
MODEL.alignToGrid();
|
6472
6599
|
this.renderSVGAsPNG(UI.paper.svg.outerHTML);
|
@@ -6492,7 +6619,7 @@ class GUIFileManager {
|
|
6492
6619
|
})
|
6493
6620
|
.then((data) => {
|
6494
6621
|
// Pass URL of image to the newly opened browser window
|
6495
|
-
localStorage.setItem('png-url', data);
|
6622
|
+
window.localStorage.setItem('png-url', data);
|
6496
6623
|
})
|
6497
6624
|
.catch((err) => UI.warn(UI.WARNING.NO_CONNECTION, err));
|
6498
6625
|
}
|
@@ -9080,7 +9207,9 @@ class GUIDatasetManager extends DatasetManager {
|
|
9080
9207
|
this.close_btn.addEventListener(
|
9081
9208
|
'click', (event) => UI.toggleDialog(event));
|
9082
9209
|
document.getElementById('ds-new-btn').addEventListener(
|
9083
|
-
|
9210
|
+
// Shift-click on New button => add prefix of selected dataset
|
9211
|
+
// (if any) to the name field of the dialog
|
9212
|
+
'click', () => DATASET_MANAGER.promptForDataset(event.shiftKey));
|
9084
9213
|
document.getElementById('ds-data-btn').addEventListener(
|
9085
9214
|
'click', () => DATASET_MANAGER.editData());
|
9086
9215
|
document.getElementById('ds-rename-btn').addEventListener(
|
@@ -9117,6 +9246,8 @@ class GUIDatasetManager extends DatasetManager {
|
|
9117
9246
|
'click', () => DATASET_MANAGER.editExpression());
|
9118
9247
|
document.getElementById('ds-delete-modif-btn').addEventListener(
|
9119
9248
|
'click', () => DATASET_MANAGER.deleteModifier());
|
9249
|
+
document.getElementById('ds-convert-modif-btn').addEventListener(
|
9250
|
+
'click', () => DATASET_MANAGER.promptToConvertModifiers());
|
9120
9251
|
// Modifier table
|
9121
9252
|
this.modifier_table = document.getElementById('dataset-modif-table');
|
9122
9253
|
// Modal dialogs
|
@@ -9130,6 +9261,11 @@ class GUIDatasetManager extends DatasetManager {
|
|
9130
9261
|
'click', () => DATASET_MANAGER.renameDataset());
|
9131
9262
|
this.rename_modal.cancel.addEventListener(
|
9132
9263
|
'click', () => DATASET_MANAGER.rename_modal.hide());
|
9264
|
+
this.conversion_modal = new ModalDialog('convert-modifiers');
|
9265
|
+
this.conversion_modal.ok.addEventListener(
|
9266
|
+
'click', () => DATASET_MANAGER.convertModifiers());
|
9267
|
+
this.conversion_modal.cancel.addEventListener(
|
9268
|
+
'click', () => DATASET_MANAGER.conversion_modal.hide());
|
9133
9269
|
this.new_selector_modal = new ModalDialog('new-selector');
|
9134
9270
|
this.new_selector_modal.ok.addEventListener(
|
9135
9271
|
'click', () => DATASET_MANAGER.newModifier());
|
@@ -9164,12 +9300,14 @@ class GUIDatasetManager extends DatasetManager {
|
|
9164
9300
|
|
9165
9301
|
reset() {
|
9166
9302
|
super.reset();
|
9303
|
+
this.selected_prefix_row = null;
|
9167
9304
|
this.selected_modifier = null;
|
9168
9305
|
this.edited_expression = null;
|
9169
9306
|
this.filter_pattern = null;
|
9170
9307
|
this.clicked_object = null;
|
9171
9308
|
this.last_time_clicked = 0;
|
9172
9309
|
this.focal_table = null;
|
9310
|
+
this.expanded_rows = [];
|
9173
9311
|
}
|
9174
9312
|
|
9175
9313
|
doubleClicked(obj) {
|
@@ -9213,6 +9351,9 @@ class GUIDatasetManager extends DatasetManager {
|
|
9213
9351
|
const srl = this.focal_table.getElementsByClassName('sel-set');
|
9214
9352
|
if(srl.length > 0) {
|
9215
9353
|
let r = this.focal_table.rows[srl[0].rowIndex + dir];
|
9354
|
+
while(r && r.style.display === 'none') {
|
9355
|
+
r = (dir > 0 ? r.nextSibling : r.previousSibling);
|
9356
|
+
}
|
9216
9357
|
if(r) {
|
9217
9358
|
UI.scrollIntoView(r);
|
9218
9359
|
// NOTE: cell, not row, listens for onclick event
|
@@ -9222,14 +9363,118 @@ class GUIDatasetManager extends DatasetManager {
|
|
9222
9363
|
}
|
9223
9364
|
}
|
9224
9365
|
|
9366
|
+
hideCollapsedRows() {
|
9367
|
+
// Hides all rows except top level and immediate children of expanded
|
9368
|
+
for(let i = 0; i < this.dataset_table.rows.length; i++) {
|
9369
|
+
const
|
9370
|
+
row = this.dataset_table.rows[i],
|
9371
|
+
// Get the first DIV in the first TD of this row
|
9372
|
+
first_div = row.firstChild.firstElementChild,
|
9373
|
+
btn = first_div.dataset.prefix === 'x';
|
9374
|
+
let p = row.dataset.prefix,
|
9375
|
+
x = this.expanded_rows.indexOf(p) >= 0,
|
9376
|
+
show = !p || x;
|
9377
|
+
if(btn) {
|
9378
|
+
const btn_div = row.getElementsByClassName('tree-btn')[0];
|
9379
|
+
// Special expand/collapse row
|
9380
|
+
if(show) {
|
9381
|
+
// Set triangle to point down
|
9382
|
+
btn_div.innerText = '\u25BC';
|
9383
|
+
} else {
|
9384
|
+
// Set triangle to point right
|
9385
|
+
btn_div.innerText = '\u25BA';
|
9386
|
+
// See whether "parent prefix" is expanded
|
9387
|
+
p = p.split(UI.PREFIXER);
|
9388
|
+
p.pop();
|
9389
|
+
p = p.join(UI.PREFIXER);
|
9390
|
+
// If so, then also show the row
|
9391
|
+
show = (!p || this.expanded_rows.indexOf(p) >= 0);
|
9392
|
+
}
|
9393
|
+
}
|
9394
|
+
row.style.display = (show ? 'block' : 'none');
|
9395
|
+
}
|
9396
|
+
}
|
9397
|
+
|
9398
|
+
togglePrefixRow(e) {
|
9399
|
+
// Shows list items of the next prefix level
|
9400
|
+
let r = e.target;
|
9401
|
+
while(r.tagName !== 'TR') r = r.parentNode;
|
9402
|
+
const
|
9403
|
+
p = r.dataset.prefix,
|
9404
|
+
i = this.expanded_rows.indexOf(p);
|
9405
|
+
if(i >= 0) {
|
9406
|
+
this.expanded_rows.splice(i, 1);
|
9407
|
+
// Also remove all prefixes that have `p` as prefix
|
9408
|
+
for(let j = this.expanded_rows.length - 1; j >= 0; j--) {
|
9409
|
+
if(this.expanded_rows[j].startsWith(p + UI.PREFIXER)) {
|
9410
|
+
this.expanded_rows.splice(j, 1);
|
9411
|
+
}
|
9412
|
+
}
|
9413
|
+
} else {
|
9414
|
+
addDistinct(p, this.expanded_rows);
|
9415
|
+
}
|
9416
|
+
this.hideCollapsedRows();
|
9417
|
+
}
|
9418
|
+
|
9419
|
+
rowByPrefix(prefix) {
|
9420
|
+
// Returns first table row with the specified prefix
|
9421
|
+
if(!prefix) return null;
|
9422
|
+
let lcp = prefix.toLowerCase(),
|
9423
|
+
pl = lcp.split(': ');
|
9424
|
+
// Remove trailing ': '
|
9425
|
+
if(lcp.endsWith(': ')) {
|
9426
|
+
pl.pop();
|
9427
|
+
lcp = pl.join(': ');
|
9428
|
+
}
|
9429
|
+
while(pl.length > 0) {
|
9430
|
+
addDistinct(pl.join(': '), this.expanded_rows);
|
9431
|
+
pl.pop();
|
9432
|
+
}
|
9433
|
+
this.hideCollapsedRows();
|
9434
|
+
for(let i = 0; i < this.dataset_table.rows.length; i++) {
|
9435
|
+
const r = this.dataset_table.rows[i];
|
9436
|
+
if(r.dataset.prefix === lcp) return r;
|
9437
|
+
}
|
9438
|
+
return null;
|
9439
|
+
}
|
9440
|
+
|
9441
|
+
selectPrefixRow(e) {
|
9442
|
+
// Selects expand/collapse prefix row
|
9443
|
+
this.focal_table = this.dataset_table;
|
9444
|
+
// NOTE: `e` can also be a string specifying the prefix to select
|
9445
|
+
let r = e.target || this.rowByPrefix(e);
|
9446
|
+
if(!r) return;
|
9447
|
+
// Modeler may have clicked on the expand/collapse triangle;
|
9448
|
+
const toggle = r.classList.contains('tree-btn');
|
9449
|
+
while(r.tagName !== 'TR') r = r.parentNode;
|
9450
|
+
this.selected_prefix_row = r;
|
9451
|
+
const sel = this.dataset_table.getElementsByClassName('sel-set');
|
9452
|
+
this.selected_dataset = null;
|
9453
|
+
if(sel.length > 0) {
|
9454
|
+
sel[0].classList.remove('sel-set');
|
9455
|
+
this.updatePanes();
|
9456
|
+
}
|
9457
|
+
r.classList.add('sel-set');
|
9458
|
+
if(!e.target) r.scrollIntoView({block: 'center'});
|
9459
|
+
if(toggle || e.altKey || this.doubleClicked(r)) this.togglePrefixRow(e);
|
9460
|
+
UI.enableButtons('ds-rename');
|
9461
|
+
}
|
9462
|
+
|
9225
9463
|
updateDialog() {
|
9226
9464
|
const
|
9465
|
+
indent_px = 14,
|
9227
9466
|
dl = [],
|
9228
9467
|
dnl = [],
|
9229
9468
|
sd = this.selected_dataset,
|
9230
|
-
ioclass = ['', 'import', 'export']
|
9469
|
+
ioclass = ['', 'import', 'export'],
|
9470
|
+
ciPrefixCompare = (a, b) => {
|
9471
|
+
const
|
9472
|
+
pa = a.split(':_').join(' '),
|
9473
|
+
pb = b.split(':_').join(' ');
|
9474
|
+
return ciCompare(pa, pb);
|
9475
|
+
};
|
9231
9476
|
for(let d in MODEL.datasets) if(MODEL.datasets.hasOwnProperty(d) &&
|
9232
|
-
|
9477
|
+
// NOTE: do not list "black-boxed" entities
|
9233
9478
|
!d.startsWith(UI.BLACK_BOX) &&
|
9234
9479
|
// NOTE: do not list the equations dataset
|
9235
9480
|
MODEL.datasets[d] !== MODEL.equations_dataset) {
|
@@ -9238,10 +9483,76 @@ class GUIDatasetManager extends DatasetManager {
|
|
9238
9483
|
dnl.push(d);
|
9239
9484
|
}
|
9240
9485
|
}
|
9241
|
-
dnl.sort(
|
9242
|
-
|
9486
|
+
dnl.sort(ciPrefixCompare);
|
9487
|
+
// First determine indentation levels, prefixes and names
|
9488
|
+
const
|
9489
|
+
indent = [],
|
9490
|
+
pref_ids = [],
|
9491
|
+
names = [],
|
9492
|
+
pref_names = {},
|
9493
|
+
xids = [];
|
9494
|
+
for(let i = 0; i < dnl.length; i++) {
|
9495
|
+
const pref = UI.prefixesAndName(MODEL.datasets[dnl[i]].name);
|
9496
|
+
// NOTE: only the name part (so no prefixes at all) will be shown
|
9497
|
+
names.push(pref.pop());
|
9498
|
+
indent.push(pref.length);
|
9499
|
+
// NOTE: ignore case but join again with ": " because prefixes
|
9500
|
+
// can contain any character; only the prefixer is "reserved"
|
9501
|
+
const pref_id = pref.join(UI.PREFIXER).toLowerCase();
|
9502
|
+
pref_ids.push(pref_id);
|
9503
|
+
pref_names[pref_id] = pref;
|
9504
|
+
}
|
9505
|
+
let sdid = 'dstr',
|
9506
|
+
prev_id = '',
|
9507
|
+
ind_div = '';
|
9243
9508
|
for(let i = 0; i < dnl.length; i++) {
|
9244
|
-
const
|
9509
|
+
const
|
9510
|
+
d = MODEL.datasets[dnl[i]],
|
9511
|
+
pid = pref_ids[i];
|
9512
|
+
if(indent[i]) {
|
9513
|
+
ind_div = '<div class="ds-indent" style="width: ' +
|
9514
|
+
indent[i] * indent_px + 'px">\u25B9</div>';
|
9515
|
+
} else {
|
9516
|
+
ind_div = '';
|
9517
|
+
}
|
9518
|
+
// NOTE: empty string should not add a collapse/expand row
|
9519
|
+
if(pid && pid != prev_id && xids.indexOf(pid) < 0) {
|
9520
|
+
// NOTE: XX: aa may be followed by XX: YY: ZZ: bb, which requires
|
9521
|
+
// *two* collapsable lines: XX: YY and XX: YY: ZZ: before adding
|
9522
|
+
// XX: YY: ZZ: bb
|
9523
|
+
const
|
9524
|
+
ps = pid.split(UI.PREFIXER),
|
9525
|
+
pps = prev_id.split(UI.PREFIXER),
|
9526
|
+
pn = pref_names[pid],
|
9527
|
+
lpl = [];
|
9528
|
+
let lindent = 0;
|
9529
|
+
// Ignore identical leading prefixes
|
9530
|
+
while(ps.length > 0 && pps.length > 0 && ps[0] === pps[0]) {
|
9531
|
+
lpl.push(ps.shift());
|
9532
|
+
pps.shift();
|
9533
|
+
pn.shift();
|
9534
|
+
lindent++;
|
9535
|
+
}
|
9536
|
+
// Add a "collapse" row for each new prefix
|
9537
|
+
while(ps.length > 0) {
|
9538
|
+
lpl.push(ps.shift());
|
9539
|
+
lindent++;
|
9540
|
+
const lpid = lpl.join(UI.PREFIXER);
|
9541
|
+
dl.push(['<tr data-prefix="', lpid, '" class="dataset',
|
9542
|
+
'" onclick="DATASET_MANAGER.selectPrefixRow(event);"><td>',
|
9543
|
+
// NOTE: data-prefix="x" signals that this is an extra row
|
9544
|
+
(lindent > 0 ?
|
9545
|
+
'<div data-prefix="x" style="width: ' + lindent * indent_px +
|
9546
|
+
'px"></div>' :
|
9547
|
+
''),
|
9548
|
+
'<div data-prefix="x" class="tree-btn">',
|
9549
|
+
(this.expanded_rows.indexOf(lpid) >= 0 ? '\u25BC' : '\u25BA'),
|
9550
|
+
'</div>', pn.shift(), '</td></tr>'].join(''));
|
9551
|
+
// Add to the list to prevent multiple c/x-rows for the same prefix
|
9552
|
+
xids.push(lpid);
|
9553
|
+
}
|
9554
|
+
}
|
9555
|
+
prev_id = pid;
|
9245
9556
|
let cls = ioclass[MODEL.ioType(d)];
|
9246
9557
|
if(d.outcome) {
|
9247
9558
|
cls += ' outcome';
|
@@ -9253,20 +9564,29 @@ class GUIDatasetManager extends DatasetManager {
|
|
9253
9564
|
if(Object.keys(d.modifiers).length > 0) cls += ' modif';
|
9254
9565
|
if(d.black_box) cls += ' blackbox';
|
9255
9566
|
cls = cls.trim();
|
9256
|
-
if(cls) cls = ' class="'+ cls + '"';
|
9567
|
+
if(cls) cls = ' class="' + cls + '"';
|
9257
9568
|
if(d === sd) sdid += i;
|
9258
9569
|
dl.push(['<tr id="dstr', i, '" class="dataset',
|
9259
9570
|
(d === sd ? ' sel-set' : ''),
|
9260
9571
|
(d.default_selector ? ' def-sel' : ''),
|
9572
|
+
'" data-prefix="', pid,
|
9261
9573
|
'" onclick="DATASET_MANAGER.selectDataset(event, \'',
|
9262
9574
|
dnl[i], '\');" onmouseover="DATASET_MANAGER.showInfo(\'', dnl[i],
|
9263
|
-
'\', event.shiftKey);"><td', cls, '>',
|
9264
|
-
'</td></tr>'].join(''));
|
9575
|
+
'\', event.shiftKey);"><td>', ind_div, '<div', cls, '>',
|
9576
|
+
names[i], '</td></tr>'].join(''));
|
9265
9577
|
}
|
9266
9578
|
this.dataset_table.innerHTML = dl.join('');
|
9267
|
-
|
9579
|
+
this.hideCollapsedRows();
|
9580
|
+
const e = document.getElementById(sdid);
|
9581
|
+
if(e) UI.scrollIntoView(e);
|
9582
|
+
this.updatePanes();
|
9583
|
+
}
|
9584
|
+
|
9585
|
+
updatePanes() {
|
9586
|
+
const
|
9587
|
+
sd = this.selected_dataset,
|
9588
|
+
btns = 'ds-data ds-clone ds-delete ds-rename';
|
9268
9589
|
if(sd) {
|
9269
|
-
this.dataset_table.innerHTML = dl.join('');
|
9270
9590
|
this.properties.style.display = 'block';
|
9271
9591
|
document.getElementById('dataset-default').innerHTML =
|
9272
9592
|
VM.sig4Dig(sd.default_value) +
|
@@ -9296,12 +9616,11 @@ class GUIDatasetManager extends DatasetManager {
|
|
9296
9616
|
this.outcome.classList.add('not-selected');
|
9297
9617
|
}
|
9298
9618
|
UI.setImportExportBox('dataset', MODEL.ioType(sd));
|
9299
|
-
const e = document.getElementById(sdid);
|
9300
|
-
UI.scrollIntoView(e);
|
9301
9619
|
UI.enableButtons(btns);
|
9302
9620
|
} else {
|
9303
9621
|
this.properties.style.display = 'none';
|
9304
9622
|
UI.disableButtons(btns);
|
9623
|
+
if(this.selected_prefix_row) UI.enableButtons('ds-rename');
|
9305
9624
|
}
|
9306
9625
|
this.updateModifiers();
|
9307
9626
|
}
|
@@ -9360,6 +9679,17 @@ class GUIDatasetManager extends DatasetManager {
|
|
9360
9679
|
} else {
|
9361
9680
|
UI.disableButtons(btns);
|
9362
9681
|
}
|
9682
|
+
// Check if dataset appears to "misuse" dataset modifiers
|
9683
|
+
const
|
9684
|
+
pml = sd.inferPrefixableModifiers,
|
9685
|
+
e = document.getElementById('ds-convert-modif-btn');
|
9686
|
+
if(pml.length > 0) {
|
9687
|
+
e.style.display = 'inline-block';
|
9688
|
+
e.title = 'Convert '+ pluralS(pml.length, 'modifier') +
|
9689
|
+
' to prefixed dataset(s)';
|
9690
|
+
} else {
|
9691
|
+
e.style.display = 'none';
|
9692
|
+
}
|
9363
9693
|
}
|
9364
9694
|
|
9365
9695
|
showInfo(id, shift) {
|
@@ -9442,8 +9772,36 @@ class GUIDatasetManager extends DatasetManager {
|
|
9442
9772
|
this.updateModifiers();
|
9443
9773
|
}
|
9444
9774
|
|
9445
|
-
|
9446
|
-
|
9775
|
+
get selectedPrefix() {
|
9776
|
+
// Returns the selected prefix (with its trailing colon-space)
|
9777
|
+
let prefix = '',
|
9778
|
+
tr = this.selected_prefix_row;
|
9779
|
+
while(tr) {
|
9780
|
+
const td = tr.firstElementChild;
|
9781
|
+
if(td && td.firstElementChild.dataset.prefix === 'x') {
|
9782
|
+
prefix = td.lastChild.textContent + UI.PREFIXER + prefix;
|
9783
|
+
tr = tr.previousSibling;
|
9784
|
+
} else {
|
9785
|
+
tr = null;
|
9786
|
+
}
|
9787
|
+
}
|
9788
|
+
return prefix;
|
9789
|
+
}
|
9790
|
+
|
9791
|
+
promptForDataset(shift=false) {
|
9792
|
+
// Shift signifies: add prefix of selected dataset (if any) to
|
9793
|
+
// the name field of the dialog
|
9794
|
+
let prefix = '';
|
9795
|
+
if(shift) {
|
9796
|
+
if(this.selected_dataset) {
|
9797
|
+
const p = UI.prefixesAndName(this.selected_dataset.name);
|
9798
|
+
p[p.length - 1] = '';
|
9799
|
+
prefix = p.join(UI.PREFIXER);
|
9800
|
+
} else if(this.selected_prefix) {
|
9801
|
+
prefix = this.selectedPrefix;
|
9802
|
+
}
|
9803
|
+
}
|
9804
|
+
this.new_modal.element('name').value = prefix;
|
9447
9805
|
this.new_modal.show('name');
|
9448
9806
|
}
|
9449
9807
|
|
@@ -9460,9 +9818,14 @@ class GUIDatasetManager extends DatasetManager {
|
|
9460
9818
|
promptForName() {
|
9461
9819
|
// Prompts the modeler for a new name for the selected dataset (if any)
|
9462
9820
|
if(this.selected_dataset) {
|
9821
|
+
this.rename_modal.element('title').innerText = 'Rename dataset';
|
9463
9822
|
this.rename_modal.element('name').value =
|
9464
9823
|
this.selected_dataset.displayName;
|
9465
9824
|
this.rename_modal.show('name');
|
9825
|
+
} else if(this.selected_prefix_row) {
|
9826
|
+
this.rename_modal.element('title').innerText = 'Rename datasets by prefix';
|
9827
|
+
this.rename_modal.element('name').value = this.selectedPrefix.slice(0, -2);
|
9828
|
+
this.rename_modal.show('name');
|
9466
9829
|
}
|
9467
9830
|
}
|
9468
9831
|
|
@@ -9477,16 +9840,67 @@ class GUIDatasetManager extends DatasetManager {
|
|
9477
9840
|
// Then try to rename -- this may generate a warning
|
9478
9841
|
if(this.selected_dataset.rename(n)) {
|
9479
9842
|
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
9843
|
if(EXPERIMENT_MANAGER.selected_experiment) {
|
9485
9844
|
EXPERIMENT_MANAGER.selected_experiment.inferVariables();
|
9486
9845
|
}
|
9487
|
-
|
9846
|
+
UI.updateControllerDialogs('CDEFJX');
|
9847
|
+
}
|
9848
|
+
} else if(this.selected_prefix_row) {
|
9849
|
+
// Create a list of datasets to be renamed
|
9850
|
+
let e = this.rename_modal.element('name'),
|
9851
|
+
prefix = e.value.trim();
|
9852
|
+
e.focus();
|
9853
|
+
while(prefix.endsWith(':')) prefix = prefix.slice(0, -1);
|
9854
|
+
// NOTE: prefix may be empty string, but otherwise should be a valid name
|
9855
|
+
if(prefix && !UI.validName(prefix)) {
|
9856
|
+
UI.warn('Invalid prefix');
|
9857
|
+
return;
|
9858
|
+
}
|
9859
|
+
prefix += UI.PREFIXER;
|
9860
|
+
const
|
9861
|
+
oldpref = this.selectedPrefix,
|
9862
|
+
key = oldpref.toLowerCase().split(UI.PREFIXER).join(':_'),
|
9863
|
+
newkey = prefix.toLowerCase().split(UI.PREFIXER).join(':_'),
|
9864
|
+
dsl = [];
|
9865
|
+
// No change if new prefix is identical to old prefix
|
9866
|
+
if(oldpref !== prefix) {
|
9867
|
+
for(let k in MODEL.datasets) if(MODEL.datasets.hasOwnProperty(k)) {
|
9868
|
+
if(k.startsWith(key)) dsl.push(k);
|
9869
|
+
}
|
9870
|
+
// NOTE: no check needed for mere upper/lower case changes
|
9871
|
+
if(newkey !== key) {
|
9872
|
+
let nc = 0;
|
9873
|
+
for(let i = 0; i < dsl.length; i++) {
|
9874
|
+
let nk = newkey + dsl[i].substring(key.length);
|
9875
|
+
if(MODEL.datasets[nk]) nc++;
|
9876
|
+
}
|
9877
|
+
if(nc) {
|
9878
|
+
UI.warn('Renaming ' + pluralS(dsl.length, 'dataset') +
|
9879
|
+
' would cause ' + pluralS(nc, 'name conflict'));
|
9880
|
+
return;
|
9881
|
+
}
|
9882
|
+
}
|
9883
|
+
// Reset counts of effects of a rename operation
|
9884
|
+
this.entity_count = 0;
|
9885
|
+
this.expression_count = 0;
|
9886
|
+
// Rename datasets one by one, suppressing notifications
|
9887
|
+
for(let i = 0; i < dsl.length; i++) {
|
9888
|
+
const d = MODEL.datasets[dsl[i]];
|
9889
|
+
d.rename(d.displayName.replace(oldpref, prefix), false);
|
9890
|
+
}
|
9891
|
+
let msg = 'Renamed ' + pluralS(dsl.length, 'dataset');
|
9892
|
+
if(MODEL.variable_count) msg += ', and updated ' +
|
9893
|
+
pluralS(MODEL.variable_count, 'variable') + ' in ' +
|
9894
|
+
pluralS(MODEL.expression_count, 'expression');
|
9895
|
+
UI.notify(msg);
|
9896
|
+
if(EXPERIMENT_MANAGER.selected_experiment) {
|
9897
|
+
EXPERIMENT_MANAGER.selected_experiment.inferVariables();
|
9898
|
+
}
|
9899
|
+
UI.updateControllerDialogs('CDEFJX');
|
9900
|
+
this.selectPrefixRow(prefix);
|
9488
9901
|
}
|
9489
9902
|
}
|
9903
|
+
this.rename_modal.hide();
|
9490
9904
|
}
|
9491
9905
|
|
9492
9906
|
cloneDataset() {
|
@@ -9626,16 +10040,13 @@ class GUIDatasetManager extends DatasetManager {
|
|
9626
10040
|
this.deleteModifier();
|
9627
10041
|
this.selected_modifier = m;
|
9628
10042
|
// Update all chartvariables referencing this dataset + old selector
|
10043
|
+
const vl = MODEL.datasetChartVariables;
|
9629
10044
|
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
|
-
}
|
10045
|
+
for(let i = 0; i < vl.length; i++) {
|
10046
|
+
if(v.object === this.selected_dataset &&
|
10047
|
+
v.attribute === oldm.selector) {
|
10048
|
+
v.attribute = m.selector;
|
10049
|
+
cv_cnt++;
|
9639
10050
|
}
|
9640
10051
|
}
|
9641
10052
|
// Also replace old selector in all expressions (count these as well)
|
@@ -9649,7 +10060,7 @@ class GUIDatasetManager extends DatasetManager {
|
|
9649
10060
|
UI.notify('Updated ' + msg.join(' and '));
|
9650
10061
|
// Also update these stay-on-top dialogs, as they may display a
|
9651
10062
|
// variable name for this dataset + modifier
|
9652
|
-
UI.updateControllerDialogs('
|
10063
|
+
UI.updateControllerDialogs('CDEFJX');
|
9653
10064
|
}
|
9654
10065
|
// NOTE: update dimensions only if dataset now has 2 or more modifiers
|
9655
10066
|
// (ignoring those with wildcards)
|
@@ -9701,6 +10112,138 @@ class GUIDatasetManager extends DatasetManager {
|
|
9701
10112
|
}
|
9702
10113
|
}
|
9703
10114
|
|
10115
|
+
promptToConvertModifiers() {
|
10116
|
+
// Convert modifiers of selected dataset to new prefixed datasets
|
10117
|
+
const
|
10118
|
+
ds = this.selected_dataset,
|
10119
|
+
md = this.conversion_modal;
|
10120
|
+
if(ds) {
|
10121
|
+
md.element('prefix').value = ds.displayName;
|
10122
|
+
md.show('prefix');
|
10123
|
+
}
|
10124
|
+
}
|
10125
|
+
|
10126
|
+
convertModifiers() {
|
10127
|
+
// Convert modifiers of selected dataset to new prefixed datasets
|
10128
|
+
if(!this.selected_dataset) return;
|
10129
|
+
const
|
10130
|
+
ds = this.selected_dataset,
|
10131
|
+
md = this.conversion_modal,
|
10132
|
+
e = md.element('prefix');
|
10133
|
+
let prefix = e.value.trim(),
|
10134
|
+
vcount = 0;
|
10135
|
+
e.focus();
|
10136
|
+
while(prefix.endsWith(':')) prefix = prefix.slice(0, -1);
|
10137
|
+
// NOTE: prefix may be empty string, but otherwise should be a valid name
|
10138
|
+
if(!UI.validName(prefix)) {
|
10139
|
+
UI.warn('Invalid prefix');
|
10140
|
+
return;
|
10141
|
+
}
|
10142
|
+
prefix += UI.PREFIXER;
|
10143
|
+
const
|
10144
|
+
dsn = ds.displayName,
|
10145
|
+
pml = ds.inferPrefixableModifiers,
|
10146
|
+
xl = MODEL.allExpressions,
|
10147
|
+
vl = MODEL.datasetVariables,
|
10148
|
+
nl = MODEL.notesWithTags;
|
10149
|
+
for(let i = 0; i < pml.length; i++) {
|
10150
|
+
// Create prefixed dataset with correct default value
|
10151
|
+
const
|
10152
|
+
m = pml[i],
|
10153
|
+
sel = m.selector,
|
10154
|
+
newds = MODEL.addDataset(prefix + sel);
|
10155
|
+
if(newds) {
|
10156
|
+
// Retain properties of the "parent" dataset
|
10157
|
+
newds.scale_unit = ds.scale_unit;
|
10158
|
+
newds.time_scale = ds.time_scale;
|
10159
|
+
newds.time_unit = ds.time_unit;
|
10160
|
+
// Set modifier's expression result as default value
|
10161
|
+
newds.default_value = m.expression.result(1);
|
10162
|
+
// Remove the modifier from the dataset
|
10163
|
+
delete ds.modifiers[UI.nameToID(sel)];
|
10164
|
+
// If it was the dataset default modifier, clear this default
|
10165
|
+
if(sel === ds.default_selector) ds.default_selector = '';
|
10166
|
+
// Rename variable in charts
|
10167
|
+
const
|
10168
|
+
from = dsn + UI.OA_SEPARATOR + sel,
|
10169
|
+
to = newds.displayName;
|
10170
|
+
for(let j = 0; j < vl.length; j++) {
|
10171
|
+
const v = vl[j];
|
10172
|
+
// NOTE: variable should match original dataset + selector
|
10173
|
+
if(v.displayName === from) {
|
10174
|
+
// Change to new dataset WITHOUT selector
|
10175
|
+
v.object = newds;
|
10176
|
+
v.attribute = '';
|
10177
|
+
vcount++;
|
10178
|
+
}
|
10179
|
+
}
|
10180
|
+
// Rename variable in the Sensitivity Analysis
|
10181
|
+
for(let j = 0; j < MODEL.sensitivity_parameters.length; j++) {
|
10182
|
+
if(MODEL.sensitivity_parameters[j] === from) {
|
10183
|
+
MODEL.sensitivity_parameters[j] = to;
|
10184
|
+
vcount++;
|
10185
|
+
}
|
10186
|
+
}
|
10187
|
+
for(let j = 0; j < MODEL.sensitivity_outcomes.length; j++) {
|
10188
|
+
if(MODEL.sensitivity_outcomes[j] === from) {
|
10189
|
+
MODEL.sensitivity_outcomes[j] = to;
|
10190
|
+
vcount++;
|
10191
|
+
}
|
10192
|
+
}
|
10193
|
+
// Rename variable in expressions and notes
|
10194
|
+
const re = new RegExp(
|
10195
|
+
// Handle multiple spaces between words
|
10196
|
+
'\\[\\s*' + escapeRegex(from).replace(/\s+/g, '\\s+')
|
10197
|
+
// Handle spaces around the separator |
|
10198
|
+
.replace('\\|', '\\s*\\|\\s*') +
|
10199
|
+
// Pattern ends at any character that is invalid for a
|
10200
|
+
// dataset modifier selector (unlike equation names)
|
10201
|
+
'\\s*[^a-zA-Z0-9\\+\\-\\%\\_]', 'gi');
|
10202
|
+
for(let j = 0; j < xl.length; j++) {
|
10203
|
+
const
|
10204
|
+
x = xl[j],
|
10205
|
+
matches = x.text.match(re);
|
10206
|
+
if(matches) {
|
10207
|
+
for(let k = 0; k < matches.length; k++) {
|
10208
|
+
// NOTE: each match will start with the opening bracket,
|
10209
|
+
// but end with the first "non-selector" character, which
|
10210
|
+
// will typically be ']', but may also be '@' (and now that
|
10211
|
+
// units can be converted, also the '>' of the arrow '->')
|
10212
|
+
x.text = x.text.replace(matches[k], '[' + to + matches[k].slice(-1));
|
10213
|
+
vcount ++;
|
10214
|
+
}
|
10215
|
+
// Force recompilation
|
10216
|
+
x.code = null;
|
10217
|
+
}
|
10218
|
+
}
|
10219
|
+
for(let j = 0; j < nl.length; j++) {
|
10220
|
+
const
|
10221
|
+
n = nl[j],
|
10222
|
+
matches = n.contents.match(re);
|
10223
|
+
if(matches) {
|
10224
|
+
for(let k = 0; k < matches.length; k++) {
|
10225
|
+
// See NOTE above for the use of `slice` here
|
10226
|
+
n.contents = n.contents.replace(matches[k], '[' + to + matches[k].slice(-1));
|
10227
|
+
vcount ++;
|
10228
|
+
}
|
10229
|
+
// Note fields must be parsed again
|
10230
|
+
n.parsed = false;
|
10231
|
+
}
|
10232
|
+
}
|
10233
|
+
}
|
10234
|
+
}
|
10235
|
+
if(vcount) UI.notify('Renamed ' + pluralS(vcount, 'variable') +
|
10236
|
+
' throughout the model');
|
10237
|
+
// Delete the original dataset unless it has series data
|
10238
|
+
if(ds.data.length === 0) this.deleteDataset();
|
10239
|
+
MODEL.updateDimensions();
|
10240
|
+
this.selected_dataset = null;
|
10241
|
+
this.selected_prefix_row = null;
|
10242
|
+
this.updateDialog();
|
10243
|
+
md.hide();
|
10244
|
+
this.selectPrefixRow(prefix);
|
10245
|
+
}
|
10246
|
+
|
9704
10247
|
updateLine() {
|
9705
10248
|
const
|
9706
10249
|
ln = document.getElementById('series-line-number'),
|
@@ -10061,7 +10604,7 @@ class EquationManager {
|
|
10061
10604
|
UI.notify('Updated ' + msg.join(' and '));
|
10062
10605
|
// Also update these stay-on-top dialogs, as they may display a
|
10063
10606
|
// variable name for this dataset + modifier
|
10064
|
-
UI.updateControllerDialogs('
|
10607
|
+
UI.updateControllerDialogs('CDEFJX');
|
10065
10608
|
}
|
10066
10609
|
// Always close the name prompt dialog, and update the equation manager
|
10067
10610
|
this.rename_modal.hide();
|
@@ -10182,6 +10725,12 @@ class GUIChartManager extends ChartManager {
|
|
10182
10725
|
'click', () => CHART_MANAGER.renameEquation());
|
10183
10726
|
document.getElementById('chart-edit-equation-btn').addEventListener(
|
10184
10727
|
'click', () => CHART_MANAGER.editEquation());
|
10728
|
+
document.getElementById('variable-color').addEventListener(
|
10729
|
+
'mouseenter', () => CHART_MANAGER.showPasteColor());
|
10730
|
+
document.getElementById('variable-color').addEventListener(
|
10731
|
+
'mouseleave', () => CHART_MANAGER.hidePasteColor());
|
10732
|
+
document.getElementById('variable-color').addEventListener(
|
10733
|
+
'click', (event) => CHART_MANAGER.copyPasteColor(event));
|
10185
10734
|
// NOTE: uses the color picker developed by James Daniel
|
10186
10735
|
this.color_picker = new iro.ColorPicker("#color-picker", {
|
10187
10736
|
width: 92,
|
@@ -10229,6 +10778,7 @@ class GUIChartManager extends ChartManager {
|
|
10229
10778
|
this.options_shown = true;
|
10230
10779
|
this.setRunsChart(false);
|
10231
10780
|
this.last_time_selected = 0;
|
10781
|
+
this.paste_color = '';
|
10232
10782
|
}
|
10233
10783
|
|
10234
10784
|
enterKey() {
|
@@ -10283,14 +10833,13 @@ class GUIChartManager extends ChartManager {
|
|
10283
10833
|
const
|
10284
10834
|
n = ev.dataTransfer.getData('text'),
|
10285
10835
|
obj = MODEL.objectByID(n);
|
10836
|
+
ev.preventDefault();
|
10286
10837
|
if(!obj) {
|
10287
10838
|
UI.alert(`Unknown entity ID "${n}"`);
|
10288
10839
|
} else if(this.chart_index >= 0) {
|
10289
|
-
// Only accept when all conditions are met
|
10290
|
-
ev.preventDefault();
|
10291
10840
|
if(obj instanceof DatasetModifier) {
|
10292
10841
|
// Equations can be added directly as chart variable
|
10293
|
-
this.addVariable(obj.
|
10842
|
+
this.addVariable(obj.selector);
|
10294
10843
|
return;
|
10295
10844
|
}
|
10296
10845
|
// For other entities, the attribute must be specified
|
@@ -10670,7 +11219,16 @@ class GUIChartManager extends ChartManager {
|
|
10670
11219
|
this.variable_index = vi;
|
10671
11220
|
this.updateDialog();
|
10672
11221
|
}
|
10673
|
-
|
11222
|
+
|
11223
|
+
setColorPicker(color) {
|
11224
|
+
// Robust way to set iro color picker color
|
11225
|
+
try {
|
11226
|
+
this.color_picker.color.hexString = color;
|
11227
|
+
} catch(e) {
|
11228
|
+
this.color_picker.color.rgbString = color;
|
11229
|
+
}
|
11230
|
+
}
|
11231
|
+
|
10674
11232
|
editVariable() {
|
10675
11233
|
// Shows the edit (or rather: format) variable dialog
|
10676
11234
|
if(this.chart_index >= 0 && this.variable_index >= 0) {
|
@@ -10680,11 +11238,7 @@ class GUIChartManager extends ChartManager {
|
|
10680
11238
|
this.variable_modal.element('scale').value = VM.sig4Dig(cv.scale_factor);
|
10681
11239
|
this.variable_modal.element('width').value = VM.sig4Dig(cv.line_width);
|
10682
11240
|
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
|
-
}
|
11241
|
+
this.setColorPicker(cv.color);
|
10688
11242
|
// Show change equation buttons only for equation variables
|
10689
11243
|
if(cv.object === MODEL.equations_dataset) {
|
10690
11244
|
this.change_equation_btns.style.display = 'block';
|
@@ -10695,6 +11249,34 @@ class GUIChartManager extends ChartManager {
|
|
10695
11249
|
}
|
10696
11250
|
}
|
10697
11251
|
|
11252
|
+
showPasteColor() {
|
11253
|
+
// Show last copied color (if any) as smaller square next to color box
|
11254
|
+
if(this.paste_color) {
|
11255
|
+
const pc = this.variable_modal.element('paste-color');
|
11256
|
+
pc.style.backgroundColor = this.paste_color;
|
11257
|
+
pc.style.display = 'inline-block';
|
11258
|
+
}
|
11259
|
+
}
|
11260
|
+
|
11261
|
+
hidePasteColor() {
|
11262
|
+
// Hide paste color box
|
11263
|
+
this.variable_modal.element('paste-color').style.display = 'none';
|
11264
|
+
}
|
11265
|
+
|
11266
|
+
copyPasteColor(event) {
|
11267
|
+
// Store the current color as past color, or set it to the current
|
11268
|
+
// paste color if this is defined and the Shift key was pressed
|
11269
|
+
event.stopPropagation();
|
11270
|
+
const cbox = this.variable_modal.element('color');
|
11271
|
+
if(event.shiftKey && this.paste_color) {
|
11272
|
+
cbox.style.backgroundColor = this.paste_color;
|
11273
|
+
this.setColorPicker(this.paste_color);
|
11274
|
+
} else {
|
11275
|
+
this.paste_color = cbox.style.backgroundColor;
|
11276
|
+
this.showPasteColor();
|
11277
|
+
}
|
11278
|
+
}
|
11279
|
+
|
10698
11280
|
toggleVariable(vi) {
|
10699
11281
|
window.event.stopPropagation();
|
10700
11282
|
if(vi >= 0 && this.chart_index >= 0) {
|
@@ -10925,7 +11507,7 @@ class GUIChartManager extends ChartManager {
|
|
10925
11507
|
}
|
10926
11508
|
|
10927
11509
|
renderChartAsPNG() {
|
10928
|
-
localStorage.removeItem('png-url');
|
11510
|
+
window.localStorage.removeItem('png-url');
|
10929
11511
|
FILE_MANAGER.renderSVGAsPNG(MODEL.charts[this.chart_index].svg);
|
10930
11512
|
}
|
10931
11513
|
|
@@ -12050,7 +12632,8 @@ class GUIExperimentManager extends ExperimentManager {
|
|
12050
12632
|
x.charts[i].title, '</td></tr>'].join(''));
|
12051
12633
|
}
|
12052
12634
|
this.chart_table.innerHTML = tr.join('');
|
12053
|
-
|
12635
|
+
// Do not show viewer unless at least 1 dependent variable has been defined
|
12636
|
+
if(x.charts.length === 0 && MODEL.outcomeNames.length === 0) canview = false;
|
12054
12637
|
if(tr.length >= this.suitable_charts.length) {
|
12055
12638
|
document.getElementById('xp-c-add-btn').classList.add('v-disab');
|
12056
12639
|
} else {
|
@@ -12070,7 +12653,7 @@ class GUIExperimentManager extends ExperimentManager {
|
|
12070
12653
|
dbtn.classList.add('v-disab');
|
12071
12654
|
cbtn.classList.add('v-disab');
|
12072
12655
|
}
|
12073
|
-
// Enable viewing only if > 1 dimensions and > 1
|
12656
|
+
// Enable viewing only if > 1 dimensions and > 1 outcome variables
|
12074
12657
|
if(canview) {
|
12075
12658
|
UI.enableButtons('xp-view');
|
12076
12659
|
} else {
|
@@ -12155,14 +12738,11 @@ class GUIExperimentManager extends ExperimentManager {
|
|
12155
12738
|
const x = this.selected_experiment;
|
12156
12739
|
if(x) {
|
12157
12740
|
x.inferVariables();
|
12158
|
-
if(x.selected_variable === '') {
|
12159
|
-
x.selected_variable = x.variables[0].displayName;
|
12160
|
-
}
|
12161
12741
|
const
|
12162
12742
|
ol = [],
|
12163
12743
|
vl = MODEL.outcomeNames;
|
12164
12744
|
for(let i = 0; i < x.variables.length; i++) {
|
12165
|
-
|
12745
|
+
addDistinct(x.variables[i].displayName, vl);
|
12166
12746
|
}
|
12167
12747
|
vl.sort(ciCompare);
|
12168
12748
|
for(let i = 0; i < vl.length; i++) {
|
@@ -12171,6 +12751,9 @@ class GUIExperimentManager extends ExperimentManager {
|
|
12171
12751
|
'>', vl[i], '</option>'].join(''));
|
12172
12752
|
}
|
12173
12753
|
document.getElementById('viewer-variable').innerHTML = ol.join('');
|
12754
|
+
if(x.selected_variable === '') {
|
12755
|
+
x.selected_variable = vl[0];
|
12756
|
+
}
|
12174
12757
|
}
|
12175
12758
|
}
|
12176
12759
|
|
@@ -14592,7 +15175,7 @@ class Finder {
|
|
14592
15175
|
const
|
14593
15176
|
raw = escapeRegex(se.displayName),
|
14594
15177
|
re = new RegExp(
|
14595
|
-
'\\[\\s
|
15178
|
+
'\\[\\s*!?' + raw.replace(/\s+/g, '\\s+') + '\\s*[\\|\\@\\]]');
|
14596
15179
|
// Check actor weight expressions
|
14597
15180
|
for(let k in MODEL.actors) if(MODEL.actors.hasOwnProperty(k)) {
|
14598
15181
|
const a = MODEL.actors[k];
|