linny-r 1.2.0 → 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 +55 -6
- package/static/linny-r.css +100 -22
- package/static/scripts/linny-r-ctrl.js +26 -4
- package/static/scripts/linny-r-gui.js +940 -119
- package/static/scripts/linny-r-model.js +203 -37
- 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;
|
@@ -4378,6 +4449,13 @@ class GUIController extends Controller {
|
|
4378
4449
|
const btns = topmod.getElementsByClassName('ok-btn');
|
4379
4450
|
if(btns.length > 0) btns[0].dispatchEvent(new Event('click'));
|
4380
4451
|
}
|
4452
|
+
} else if(this.dr_dialog_order.length > 0) {
|
4453
|
+
// Send ENTER key event to the top draggable dialog
|
4454
|
+
const last = this.dr_dialog_order.length - 1;
|
4455
|
+
if(last >= 0) {
|
4456
|
+
const mgr = window[this.dr_dialog_order[last].dataset.manager];
|
4457
|
+
if(mgr && 'enterKey' in mgr) mgr.enterKey();
|
4458
|
+
}
|
4381
4459
|
}
|
4382
4460
|
} else if(e.keyCode === 8 &&
|
4383
4461
|
ttype !== 'text' && ttype !== 'password' && ttype !== 'textarea') {
|
@@ -4392,7 +4470,18 @@ class GUIController extends Controller {
|
|
4392
4470
|
return;
|
4393
4471
|
}
|
4394
4472
|
}
|
4395
|
-
//
|
4473
|
+
// Up and down arrow keys
|
4474
|
+
if([38, 40].indexOf(e.keyCode) >= 0) {
|
4475
|
+
e.preventDefault();
|
4476
|
+
// Send event to the top draggable dialog
|
4477
|
+
const last = this.dr_dialog_order.length - 1;
|
4478
|
+
if(last >= 0) {
|
4479
|
+
const mgr = window[this.dr_dialog_order[last].dataset.manager];
|
4480
|
+
// NOTE: pass key direction as -1 for UP and +1 for DOWN
|
4481
|
+
if(mgr && 'upDownKey' in mgr) mgr.upDownKey(e.keyCode - 39);
|
4482
|
+
}
|
4483
|
+
}
|
4484
|
+
// end, home, Left and right arrow keys
|
4396
4485
|
if([35, 36, 37, 39].indexOf(e.keyCode) >= 0) e.preventDefault();
|
4397
4486
|
if(e.keyCode === 35) {
|
4398
4487
|
MODEL.t = MODEL.end_period - MODEL.start_period + 1;
|
@@ -4406,6 +4495,15 @@ class GUIController extends Controller {
|
|
4406
4495
|
this.stepBack(e);
|
4407
4496
|
} else if(e.keyCode === 39) {
|
4408
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
|
+
}
|
4409
4507
|
} else if(!e.shiftKey && !e.altKey &&
|
4410
4508
|
(!topmod || [65, 67, 86].indexOf(e.keyCode) < 0)) {
|
4411
4509
|
// Interpret special keys as shortcuts unless a modal dialog is open
|
@@ -5119,7 +5217,42 @@ class GUIController extends Controller {
|
|
5119
5217
|
cancelCloneSelection() {
|
5120
5218
|
this.modals.clone.hide();
|
5121
5219
|
this.updateButtons();
|
5122
|
-
}
|
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
|
+
}
|
5123
5256
|
|
5124
5257
|
//
|
5125
5258
|
// Interaction with modal dialogs to modify model or entity properties
|
@@ -5328,17 +5461,20 @@ class GUIController extends Controller {
|
|
5328
5461
|
if(!this.updateExpressionInput(
|
5329
5462
|
'process-IL', 'initial level', p.initial_level)) return false;
|
5330
5463
|
// Store original expression string
|
5331
|
-
const
|
5464
|
+
const
|
5465
|
+
px = p.pace_expression,
|
5466
|
+
pxt = p.pace_expression.text;
|
5332
5467
|
// Validate expression
|
5333
5468
|
if(!this.updateExpressionInput('process-pace', 'level change frequency',
|
5334
|
-
|
5469
|
+
px)) return false;
|
5335
5470
|
// NOTE: pace expression must be *static* and >= 1
|
5336
|
-
n =
|
5337
|
-
if(!
|
5471
|
+
n = px.result(1);
|
5472
|
+
if(!px.isStatic || n < 1) {
|
5338
5473
|
md.element('pace').focus();
|
5339
5474
|
this.warn('Level change frequency must be static and ≥ 1');
|
5340
5475
|
// Restore original expression string
|
5341
|
-
|
5476
|
+
px.text = pxt;
|
5477
|
+
px.code = null;
|
5342
5478
|
return false;
|
5343
5479
|
}
|
5344
5480
|
// Ignore fraction if a real number was entered.
|
@@ -6448,7 +6584,7 @@ class GUIFileManager {
|
|
6448
6584
|
}
|
6449
6585
|
|
6450
6586
|
renderDiagramAsPNG() {
|
6451
|
-
localStorage.removeItem('png-url');
|
6587
|
+
window.localStorage.removeItem('png-url');
|
6452
6588
|
UI.paper.fitToSize();
|
6453
6589
|
MODEL.alignToGrid();
|
6454
6590
|
this.renderSVGAsPNG(UI.paper.svg.outerHTML);
|
@@ -6474,7 +6610,7 @@ class GUIFileManager {
|
|
6474
6610
|
})
|
6475
6611
|
.then((data) => {
|
6476
6612
|
// Pass URL of image to the newly opened browser window
|
6477
|
-
localStorage.setItem('png-url', data);
|
6613
|
+
window.localStorage.setItem('png-url', data);
|
6478
6614
|
})
|
6479
6615
|
.catch((err) => UI.warn(UI.WARNING.NO_CONNECTION, err));
|
6480
6616
|
}
|
@@ -8478,7 +8614,7 @@ class GUIRepositoryBrowser extends RepositoryBrowser {
|
|
8478
8614
|
document.getElementById('repo-include-btn').addEventListener(
|
8479
8615
|
'click', () => REPOSITORY_BROWSER.includeModule());
|
8480
8616
|
document.getElementById('repo-load-btn').addEventListener(
|
8481
|
-
'click', () => REPOSITORY_BROWSER.
|
8617
|
+
'click', () => REPOSITORY_BROWSER.confirmLoadModuleAsModel());
|
8482
8618
|
document.getElementById('repo-store-btn').addEventListener(
|
8483
8619
|
'click', () => REPOSITORY_BROWSER.promptForStoring());
|
8484
8620
|
document.getElementById('repo-black-box-btn').addEventListener(
|
@@ -8525,6 +8661,12 @@ class GUIRepositoryBrowser extends RepositoryBrowser {
|
|
8525
8661
|
this.include_modal.element('actor').addEventListener(
|
8526
8662
|
'blur', () => REPOSITORY_BROWSER.updateActors());
|
8527
8663
|
|
8664
|
+
this.confirm_load_modal = new ModalDialog('confirm-load-from-repo');
|
8665
|
+
this.confirm_load_modal.ok.addEventListener(
|
8666
|
+
'click', () => REPOSITORY_BROWSER.loadModuleAsModel());
|
8667
|
+
this.confirm_load_modal.cancel.addEventListener(
|
8668
|
+
'click', () => REPOSITORY_BROWSER.confirm_load_modal.hide());
|
8669
|
+
|
8528
8670
|
this.confirm_delete_modal = new ModalDialog('confirm-delete-from-repo');
|
8529
8671
|
this.confirm_delete_modal.ok.addEventListener(
|
8530
8672
|
'click', () => REPOSITORY_BROWSER.deleteFromRepository());
|
@@ -8536,6 +8678,31 @@ class GUIRepositoryBrowser extends RepositoryBrowser {
|
|
8536
8678
|
super.reset();
|
8537
8679
|
this.last_time_selected = 0;
|
8538
8680
|
}
|
8681
|
+
|
8682
|
+
enterKey() {
|
8683
|
+
// Open "edit properties" dialog for the selected entity
|
8684
|
+
const srl = this.modules_table.getElementsByClassName('sel-set');
|
8685
|
+
if(srl.length > 0) {
|
8686
|
+
const r = this.modules_table.rows[srl[0].rowIndex];
|
8687
|
+
if(r) {
|
8688
|
+
// Ensure that click will be interpreted as double-click
|
8689
|
+
this.last_time_selected = Date.now();
|
8690
|
+
r.dispatchEvent(new Event('click'));
|
8691
|
+
}
|
8692
|
+
}
|
8693
|
+
}
|
8694
|
+
|
8695
|
+
upDownKey(dir) {
|
8696
|
+
// Select row above or below the selected one (if possible)
|
8697
|
+
const srl = this.modules_table.getElementsByClassName('sel-set');
|
8698
|
+
if(srl.length > 0) {
|
8699
|
+
const r = this.modules_table.rows[srl[0].rowIndex + dir];
|
8700
|
+
if(r) {
|
8701
|
+
UI.scrollIntoView(r);
|
8702
|
+
r.dispatchEvent(new Event('click'));
|
8703
|
+
}
|
8704
|
+
}
|
8705
|
+
}
|
8539
8706
|
|
8540
8707
|
get isLocalHost() {
|
8541
8708
|
// Returns TRUE if first repository on the list is 'local host'
|
@@ -8718,7 +8885,7 @@ class GUIRepositoryBrowser extends RepositoryBrowser {
|
|
8718
8885
|
// Consider click to be "double" if it occurred less than 300 ms ago
|
8719
8886
|
if(dt < 300) {
|
8720
8887
|
this.last_time_selected = 0;
|
8721
|
-
this.
|
8888
|
+
this.includeModule();
|
8722
8889
|
return;
|
8723
8890
|
}
|
8724
8891
|
}
|
@@ -8967,6 +9134,7 @@ class GUIRepositoryBrowser extends RepositoryBrowser {
|
|
8967
9134
|
|
8968
9135
|
loadModuleAsModel() {
|
8969
9136
|
// Loads selected module as model
|
9137
|
+
this.confirm_load_modal.hide();
|
8970
9138
|
if(this.repository_index >= 0 && this.module_index >= 0) {
|
8971
9139
|
// NOTE: when loading new model, the stay-on-top dialogs must be reset
|
8972
9140
|
UI.hideStayOnTopDialogs();
|
@@ -8983,6 +9151,17 @@ class GUIRepositoryBrowser extends RepositoryBrowser {
|
|
8983
9151
|
r.loadModule(this.module_index, true);
|
8984
9152
|
}
|
8985
9153
|
}
|
9154
|
+
|
9155
|
+
confirmLoadModuleAsModel() {
|
9156
|
+
// Prompts modeler to confirm loading the selected module as model
|
9157
|
+
if(this.repository_index >= 0 && this.module_index >= 0 &&
|
9158
|
+
document.getElementById('repo-load-btn').classList.contains('enab')) {
|
9159
|
+
const r = this.repositories[this.repository_index];
|
9160
|
+
this.confirm_load_modal.element('mod-name').innerText =
|
9161
|
+
r.module_names[this.module_index];
|
9162
|
+
this.confirm_load_modal.show();
|
9163
|
+
}
|
9164
|
+
}
|
8986
9165
|
|
8987
9166
|
confirmDeleteFromRepository() {
|
8988
9167
|
// Prompts modeler to confirm deletion of the selected module
|
@@ -9019,7 +9198,9 @@ class GUIDatasetManager extends DatasetManager {
|
|
9019
9198
|
this.close_btn.addEventListener(
|
9020
9199
|
'click', (event) => UI.toggleDialog(event));
|
9021
9200
|
document.getElementById('ds-new-btn').addEventListener(
|
9022
|
-
|
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));
|
9023
9204
|
document.getElementById('ds-data-btn').addEventListener(
|
9024
9205
|
'click', () => DATASET_MANAGER.editData());
|
9025
9206
|
document.getElementById('ds-rename-btn').addEventListener(
|
@@ -9034,7 +9215,7 @@ class GUIDatasetManager extends DatasetManager {
|
|
9034
9215
|
this.filter_text = document.getElementById('ds-filter-text');
|
9035
9216
|
this.filter_text.addEventListener(
|
9036
9217
|
'input', () => DATASET_MANAGER.changeFilter());
|
9037
|
-
this.
|
9218
|
+
this.dataset_table = document.getElementById('dataset-table');
|
9038
9219
|
// Data properties pane
|
9039
9220
|
this.properties = document.getElementById('dataset-properties');
|
9040
9221
|
// Toggle buttons at bottom of dialog
|
@@ -9056,6 +9237,10 @@ class GUIDatasetManager extends DatasetManager {
|
|
9056
9237
|
'click', () => DATASET_MANAGER.editExpression());
|
9057
9238
|
document.getElementById('ds-delete-modif-btn').addEventListener(
|
9058
9239
|
'click', () => DATASET_MANAGER.deleteModifier());
|
9240
|
+
document.getElementById('ds-convert-modif-btn').addEventListener(
|
9241
|
+
'click', () => DATASET_MANAGER.promptToConvertModifiers());
|
9242
|
+
// Modifier table
|
9243
|
+
this.modifier_table = document.getElementById('dataset-modif-table');
|
9059
9244
|
// Modal dialogs
|
9060
9245
|
this.new_modal = new ModalDialog('new-dataset');
|
9061
9246
|
this.new_modal.ok.addEventListener(
|
@@ -9067,6 +9252,11 @@ class GUIDatasetManager extends DatasetManager {
|
|
9067
9252
|
'click', () => DATASET_MANAGER.renameDataset());
|
9068
9253
|
this.rename_modal.cancel.addEventListener(
|
9069
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());
|
9070
9260
|
this.new_selector_modal = new ModalDialog('new-selector');
|
9071
9261
|
this.new_selector_modal.ok.addEventListener(
|
9072
9262
|
'click', () => DATASET_MANAGER.newModifier());
|
@@ -9101,20 +9291,181 @@ class GUIDatasetManager extends DatasetManager {
|
|
9101
9291
|
|
9102
9292
|
reset() {
|
9103
9293
|
super.reset();
|
9294
|
+
this.selected_prefix_row = null;
|
9104
9295
|
this.selected_modifier = null;
|
9105
9296
|
this.edited_expression = null;
|
9106
9297
|
this.filter_pattern = null;
|
9107
|
-
this.
|
9298
|
+
this.clicked_object = null;
|
9299
|
+
this.last_time_clicked = 0;
|
9300
|
+
this.focal_table = null;
|
9301
|
+
this.expanded_rows = [];
|
9302
|
+
}
|
9303
|
+
|
9304
|
+
doubleClicked(obj) {
|
9305
|
+
const
|
9306
|
+
now = Date.now(),
|
9307
|
+
dt = now - this.last_time_clicked;
|
9308
|
+
this.last_time_clicked = now;
|
9309
|
+
if(obj === this.clicked_object) {
|
9310
|
+
// Consider click to be "double" if it occurred less than 300 ms ago
|
9311
|
+
if(dt < 300) {
|
9312
|
+
this.last_time_clicked = 0;
|
9313
|
+
return true;
|
9314
|
+
}
|
9315
|
+
}
|
9316
|
+
this.clicked_object = obj;
|
9317
|
+
return false;
|
9318
|
+
}
|
9319
|
+
|
9320
|
+
enterKey() {
|
9321
|
+
// Open "edit" dialog for the selected dataset or modifier expression
|
9322
|
+
const srl = this.focal_table.getElementsByClassName('sel-set');
|
9323
|
+
if(srl.length > 0) {
|
9324
|
+
const r = this.focal_table.rows[srl[0].rowIndex];
|
9325
|
+
if(r) {
|
9326
|
+
const e = new Event('click');
|
9327
|
+
if(this.focal_table === this.dataset_table) {
|
9328
|
+
// Emulate Alt-click in the table to open the time series dialog
|
9329
|
+
e.altKey = true;
|
9330
|
+
r.dispatchEvent(e);
|
9331
|
+
} else if(this.focal_table === this.modifier_table) {
|
9332
|
+
// Emulate a double-click on the second cell to edit the expression
|
9333
|
+
this.last_time_clicked = Date.now();
|
9334
|
+
r.cells[1].dispatchEvent(e);
|
9335
|
+
}
|
9336
|
+
}
|
9337
|
+
}
|
9338
|
+
}
|
9339
|
+
|
9340
|
+
upDownKey(dir) {
|
9341
|
+
// Select row above or below the selected one (if possible)
|
9342
|
+
const srl = this.focal_table.getElementsByClassName('sel-set');
|
9343
|
+
if(srl.length > 0) {
|
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
|
+
}
|
9348
|
+
if(r) {
|
9349
|
+
UI.scrollIntoView(r);
|
9350
|
+
// NOTE: cell, not row, listens for onclick event
|
9351
|
+
if(this.focal_table === this.modifier_table) r = r.cells[1];
|
9352
|
+
r.dispatchEvent(new Event('click'));
|
9353
|
+
}
|
9354
|
+
}
|
9355
|
+
}
|
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');
|
9108
9452
|
}
|
9109
9453
|
|
9110
9454
|
updateDialog() {
|
9111
9455
|
const
|
9456
|
+
indent_px = 14,
|
9112
9457
|
dl = [],
|
9113
9458
|
dnl = [],
|
9114
9459
|
sd = this.selected_dataset,
|
9115
|
-
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
|
+
};
|
9116
9467
|
for(let d in MODEL.datasets) if(MODEL.datasets.hasOwnProperty(d) &&
|
9117
|
-
|
9468
|
+
// NOTE: do not list "black-boxed" entities
|
9118
9469
|
!d.startsWith(UI.BLACK_BOX) &&
|
9119
9470
|
// NOTE: do not list the equations dataset
|
9120
9471
|
MODEL.datasets[d] !== MODEL.equations_dataset) {
|
@@ -9123,10 +9474,76 @@ class GUIDatasetManager extends DatasetManager {
|
|
9123
9474
|
dnl.push(d);
|
9124
9475
|
}
|
9125
9476
|
}
|
9126
|
-
dnl.sort(
|
9127
|
-
|
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 = [];
|
9128
9485
|
for(let i = 0; i < dnl.length; i++) {
|
9129
|
-
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;
|
9130
9547
|
let cls = ioclass[MODEL.ioType(d)];
|
9131
9548
|
if(d.outcome) {
|
9132
9549
|
cls += ' outcome';
|
@@ -9138,20 +9555,29 @@ class GUIDatasetManager extends DatasetManager {
|
|
9138
9555
|
if(Object.keys(d.modifiers).length > 0) cls += ' modif';
|
9139
9556
|
if(d.black_box) cls += ' blackbox';
|
9140
9557
|
cls = cls.trim();
|
9141
|
-
if(cls) cls = ' class="'+ cls + '"';
|
9558
|
+
if(cls) cls = ' class="' + cls + '"';
|
9142
9559
|
if(d === sd) sdid += i;
|
9143
9560
|
dl.push(['<tr id="dstr', i, '" class="dataset',
|
9144
9561
|
(d === sd ? ' sel-set' : ''),
|
9145
9562
|
(d.default_selector ? ' def-sel' : ''),
|
9563
|
+
'" data-prefix="', pid,
|
9146
9564
|
'" onclick="DATASET_MANAGER.selectDataset(event, \'',
|
9147
9565
|
dnl[i], '\');" onmouseover="DATASET_MANAGER.showInfo(\'', dnl[i],
|
9148
|
-
'\', event.shiftKey);"><td', cls, '>',
|
9149
|
-
'</td></tr>'].join(''));
|
9566
|
+
'\', event.shiftKey);"><td>', ind_div, '<div', cls, '>',
|
9567
|
+
names[i], '</td></tr>'].join(''));
|
9150
9568
|
}
|
9151
|
-
this.
|
9152
|
-
|
9569
|
+
this.dataset_table.innerHTML = dl.join('');
|
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';
|
9153
9580
|
if(sd) {
|
9154
|
-
this.table.innerHTML = dl.join('');
|
9155
9581
|
this.properties.style.display = 'block';
|
9156
9582
|
document.getElementById('dataset-default').innerHTML =
|
9157
9583
|
VM.sig4Dig(sd.default_value) +
|
@@ -9181,12 +9607,11 @@ class GUIDatasetManager extends DatasetManager {
|
|
9181
9607
|
this.outcome.classList.add('not-selected');
|
9182
9608
|
}
|
9183
9609
|
UI.setImportExportBox('dataset', MODEL.ioType(sd));
|
9184
|
-
const e = document.getElementById(sdid);
|
9185
|
-
UI.scrollIntoView(e);
|
9186
9610
|
UI.enableButtons(btns);
|
9187
9611
|
} else {
|
9188
9612
|
this.properties.style.display = 'none';
|
9189
9613
|
UI.disableButtons(btns);
|
9614
|
+
if(this.selected_prefix_row) UI.enableButtons('ds-rename');
|
9190
9615
|
}
|
9191
9616
|
this.updateModifiers();
|
9192
9617
|
}
|
@@ -9234,7 +9659,7 @@ class GUIDatasetManager extends DatasetManager {
|
|
9234
9659
|
m.selector, '</td><td class="dataset-expression',
|
9235
9660
|
clk, ');">', m.expression.text, '</td></tr>'].join(''));
|
9236
9661
|
}
|
9237
|
-
|
9662
|
+
this.modifier_table.innerHTML = ml.join('');
|
9238
9663
|
ttls.style.display = 'block';
|
9239
9664
|
msa.style.display = 'block';
|
9240
9665
|
mbtns.style.display = 'block';
|
@@ -9245,6 +9670,17 @@ class GUIDatasetManager extends DatasetManager {
|
|
9245
9670
|
} else {
|
9246
9671
|
UI.disableButtons(btns);
|
9247
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
|
+
}
|
9248
9684
|
}
|
9249
9685
|
|
9250
9686
|
showInfo(id, shift) {
|
@@ -9279,16 +9715,13 @@ class GUIDatasetManager extends DatasetManager {
|
|
9279
9715
|
|
9280
9716
|
selectDataset(event, id) {
|
9281
9717
|
// Select dataset, or edit it when Alt- or double-clicked
|
9718
|
+
this.focal_table = this.dataset_table;
|
9282
9719
|
const
|
9283
9720
|
d = MODEL.datasets[id] || null,
|
9284
|
-
|
9285
|
-
dt = now - this.last_time_selected,
|
9286
|
-
// Consider click to be "double" if it occurred less than 300 ms ago
|
9287
|
-
edit = event.altKey || (d === this.selected_dataset && dt < 300);
|
9721
|
+
edit = event.altKey || this.doubleClicked(d);
|
9288
9722
|
this.selected_dataset = d;
|
9289
|
-
this.last_time_selected = now;
|
9290
9723
|
if(d && edit) {
|
9291
|
-
this.
|
9724
|
+
this.last_time_clicked = 0;
|
9292
9725
|
this.editData();
|
9293
9726
|
return;
|
9294
9727
|
}
|
@@ -9298,19 +9731,13 @@ class GUIDatasetManager extends DatasetManager {
|
|
9298
9731
|
selectModifier(event, id, x=true) {
|
9299
9732
|
// Select modifier, or when double-clicked, edit its expression or the
|
9300
9733
|
// name of the modifier
|
9734
|
+
this.focal_table = this.modifier_table;
|
9301
9735
|
if(this.selected_dataset) {
|
9302
9736
|
const m = this.selected_dataset.modifiers[UI.nameToID(id)],
|
9303
|
-
|
9304
|
-
dt = now - this.last_time_selected,
|
9305
|
-
// NOTE: Alt-click and double-click indicate: edit
|
9306
|
-
// Consider click to be "double" if the same modifier was clicked
|
9307
|
-
// less than 300 ms ago
|
9308
|
-
edit = event.altKey || (m === this.selected_modifier && dt < 300);
|
9309
|
-
this.last_time_selected = now;
|
9737
|
+
edit = event.altKey || this.doubleClicked(m);
|
9310
9738
|
if(event.shiftKey) {
|
9311
9739
|
// NOTE: prepare to update HTML class of selected dataset
|
9312
|
-
const el =
|
9313
|
-
.getElementsByClassName('sel-set')[0];
|
9740
|
+
const el = this.dataset_table.getElementsByClassName('sel-set')[0];
|
9314
9741
|
// Toggle dataset default selector
|
9315
9742
|
if(m.selector === this.selected_dataset.default_selector) {
|
9316
9743
|
this.selected_dataset.default_selector = '';
|
@@ -9322,7 +9749,7 @@ class GUIDatasetManager extends DatasetManager {
|
|
9322
9749
|
}
|
9323
9750
|
this.selected_modifier = m;
|
9324
9751
|
if(edit) {
|
9325
|
-
this.
|
9752
|
+
this.last_time_clicked = 0;
|
9326
9753
|
if(x) {
|
9327
9754
|
this.editExpression();
|
9328
9755
|
} else {
|
@@ -9336,8 +9763,36 @@ class GUIDatasetManager extends DatasetManager {
|
|
9336
9763
|
this.updateModifiers();
|
9337
9764
|
}
|
9338
9765
|
|
9339
|
-
|
9340
|
-
|
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;
|
9341
9796
|
this.new_modal.show('name');
|
9342
9797
|
}
|
9343
9798
|
|
@@ -9354,9 +9809,14 @@ class GUIDatasetManager extends DatasetManager {
|
|
9354
9809
|
promptForName() {
|
9355
9810
|
// Prompts the modeler for a new name for the selected dataset (if any)
|
9356
9811
|
if(this.selected_dataset) {
|
9812
|
+
this.rename_modal.element('title').innerText = 'Rename dataset';
|
9357
9813
|
this.rename_modal.element('name').value =
|
9358
9814
|
this.selected_dataset.displayName;
|
9359
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');
|
9360
9820
|
}
|
9361
9821
|
}
|
9362
9822
|
|
@@ -9371,16 +9831,67 @@ class GUIDatasetManager extends DatasetManager {
|
|
9371
9831
|
// Then try to rename -- this may generate a warning
|
9372
9832
|
if(this.selected_dataset.rename(n)) {
|
9373
9833
|
this.rename_modal.hide();
|
9374
|
-
this.updateDialog();
|
9375
|
-
// Also update Chart manager and Experiment viewer, as these may
|
9376
|
-
// display a variable name for this dataset
|
9377
|
-
CHART_MANAGER.updateDialog();
|
9378
9834
|
if(EXPERIMENT_MANAGER.selected_experiment) {
|
9379
9835
|
EXPERIMENT_MANAGER.selected_experiment.inferVariables();
|
9380
9836
|
}
|
9381
|
-
|
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);
|
9382
9892
|
}
|
9383
9893
|
}
|
9894
|
+
this.rename_modal.hide();
|
9384
9895
|
}
|
9385
9896
|
|
9386
9897
|
cloneDataset() {
|
@@ -9520,16 +10031,13 @@ class GUIDatasetManager extends DatasetManager {
|
|
9520
10031
|
this.deleteModifier();
|
9521
10032
|
this.selected_modifier = m;
|
9522
10033
|
// Update all chartvariables referencing this dataset + old selector
|
10034
|
+
const vl = MODEL.datasetChartVariables;
|
9523
10035
|
let cv_cnt = 0;
|
9524
|
-
for(let i = 0; i <
|
9525
|
-
|
9526
|
-
|
9527
|
-
|
9528
|
-
|
9529
|
-
v.attribute === oldm.selector) {
|
9530
|
-
v.attribute = m.selector;
|
9531
|
-
cv_cnt++;
|
9532
|
-
}
|
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++;
|
9533
10041
|
}
|
9534
10042
|
}
|
9535
10043
|
// Also replace old selector in all expressions (count these as well)
|
@@ -9543,7 +10051,7 @@ class GUIDatasetManager extends DatasetManager {
|
|
9543
10051
|
UI.notify('Updated ' + msg.join(' and '));
|
9544
10052
|
// Also update these stay-on-top dialogs, as they may display a
|
9545
10053
|
// variable name for this dataset + modifier
|
9546
|
-
UI.updateControllerDialogs('
|
10054
|
+
UI.updateControllerDialogs('CDEFJX');
|
9547
10055
|
}
|
9548
10056
|
// NOTE: update dimensions only if dataset now has 2 or more modifiers
|
9549
10057
|
// (ignoring those with wildcards)
|
@@ -9595,6 +10103,138 @@ class GUIDatasetManager extends DatasetManager {
|
|
9595
10103
|
}
|
9596
10104
|
}
|
9597
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
|
+
|
9598
10238
|
updateLine() {
|
9599
10239
|
const
|
9600
10240
|
ln = document.getElementById('series-line-number'),
|
@@ -9743,7 +10383,49 @@ class EquationManager {
|
|
9743
10383
|
this.visible = false;
|
9744
10384
|
this.selected_modifier = null;
|
9745
10385
|
this.edited_expression = null;
|
9746
|
-
this.
|
10386
|
+
this.last_time_clicked = 0;
|
10387
|
+
}
|
10388
|
+
|
10389
|
+
doubleClicked(obj) {
|
10390
|
+
const
|
10391
|
+
now = Date.now(),
|
10392
|
+
dt = now - this.last_time_clicked;
|
10393
|
+
this.last_time_clicked = now;
|
10394
|
+
if(obj === this.clicked_object) {
|
10395
|
+
// Consider click to be "double" if it occurred less than 300 ms ago
|
10396
|
+
if(dt < 300) {
|
10397
|
+
this.last_time_clicked = 0;
|
10398
|
+
return true;
|
10399
|
+
}
|
10400
|
+
}
|
10401
|
+
this.clicked_object = obj;
|
10402
|
+
return false;
|
10403
|
+
}
|
10404
|
+
|
10405
|
+
enterKey() {
|
10406
|
+
// Open the expression editor for the selected equation
|
10407
|
+
const srl = this.table.getElementsByClassName('sel-set');
|
10408
|
+
if(srl.length > 0) {
|
10409
|
+
const r = this.table.rows[srl[0].rowIndex];
|
10410
|
+
if(r) {
|
10411
|
+
// Emulate a double-click on the second cell to edit the expression
|
10412
|
+
this.last_time_clicked = Date.now();
|
10413
|
+
r.cells[1].dispatchEvent(new Event('click'));
|
10414
|
+
}
|
10415
|
+
}
|
10416
|
+
}
|
10417
|
+
|
10418
|
+
upDownKey(dir) {
|
10419
|
+
// Select row above or below the selected one (if possible)
|
10420
|
+
const srl = this.table.getElementsByClassName('sel-set');
|
10421
|
+
if(srl.length > 0) {
|
10422
|
+
const r = this.table.rows[srl[0].rowIndex + dir];
|
10423
|
+
if(r) {
|
10424
|
+
UI.scrollIntoView(r);
|
10425
|
+
// NOTE: not row but cell listens for onclick
|
10426
|
+
r.cells[1].dispatchEvent(new Event('click'));
|
10427
|
+
}
|
10428
|
+
}
|
9747
10429
|
}
|
9748
10430
|
|
9749
10431
|
updateDialog() {
|
@@ -9789,14 +10471,9 @@ class EquationManager {
|
|
9789
10471
|
if(MODEL.equations_dataset) {
|
9790
10472
|
const
|
9791
10473
|
m = MODEL.equations_dataset.modifiers[UI.nameToID(id)] || null,
|
9792
|
-
|
9793
|
-
dt = now - this.last_time_selected,
|
9794
|
-
// Consider click to be "double" if it occurred less than 300 ms ago
|
9795
|
-
edit = event.altKey || (m === this.selected_modifier && dt < 300);
|
9796
|
-
this.last_time_selected = now;
|
10474
|
+
edit = event.altKey || this.doubleClicked(m);
|
9797
10475
|
this.selected_modifier = m;
|
9798
10476
|
if(m && edit) {
|
9799
|
-
this.last_time_selected = 0;
|
9800
10477
|
if(x) {
|
9801
10478
|
this.editEquation();
|
9802
10479
|
} else {
|
@@ -9918,7 +10595,7 @@ class EquationManager {
|
|
9918
10595
|
UI.notify('Updated ' + msg.join(' and '));
|
9919
10596
|
// Also update these stay-on-top dialogs, as they may display a
|
9920
10597
|
// variable name for this dataset + modifier
|
9921
|
-
UI.updateControllerDialogs('
|
10598
|
+
UI.updateControllerDialogs('CDEFJX');
|
9922
10599
|
}
|
9923
10600
|
// Always close the name prompt dialog, and update the equation manager
|
9924
10601
|
this.rename_modal.hide();
|
@@ -10039,6 +10716,12 @@ class GUIChartManager extends ChartManager {
|
|
10039
10716
|
'click', () => CHART_MANAGER.renameEquation());
|
10040
10717
|
document.getElementById('chart-edit-equation-btn').addEventListener(
|
10041
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));
|
10042
10725
|
// NOTE: uses the color picker developed by James Daniel
|
10043
10726
|
this.color_picker = new iro.ColorPicker("#color-picker", {
|
10044
10727
|
width: 92,
|
@@ -10086,6 +10769,32 @@ class GUIChartManager extends ChartManager {
|
|
10086
10769
|
this.options_shown = true;
|
10087
10770
|
this.setRunsChart(false);
|
10088
10771
|
this.last_time_selected = 0;
|
10772
|
+
this.paste_color = '';
|
10773
|
+
}
|
10774
|
+
|
10775
|
+
enterKey() {
|
10776
|
+
// Open "edit" dialog for the selected chart variable
|
10777
|
+
const srl = this.variables_table.getElementsByClassName('sel-set');
|
10778
|
+
if(srl.length > 0) {
|
10779
|
+
const r = this.variables_table.rows[srl[0].rowIndex];
|
10780
|
+
if(r) {
|
10781
|
+
// Emulate a double-click to edit the variable properties
|
10782
|
+
this.last_time_selected = Date.now();
|
10783
|
+
r.dispatchEvent(new Event('click'));
|
10784
|
+
}
|
10785
|
+
}
|
10786
|
+
}
|
10787
|
+
|
10788
|
+
upDownKey(dir) {
|
10789
|
+
// Select row above or below the selected one (if possible)
|
10790
|
+
const srl = this.variables_table.getElementsByClassName('sel-set');
|
10791
|
+
if(srl.length > 0) {
|
10792
|
+
const r = this.variables_table.rows[srl[0].rowIndex + dir];
|
10793
|
+
if(r) {
|
10794
|
+
UI.scrollIntoView(r);
|
10795
|
+
r.dispatchEvent(new Event('click'));
|
10796
|
+
}
|
10797
|
+
}
|
10089
10798
|
}
|
10090
10799
|
|
10091
10800
|
setRunsChart(show) {
|
@@ -10115,14 +10824,13 @@ class GUIChartManager extends ChartManager {
|
|
10115
10824
|
const
|
10116
10825
|
n = ev.dataTransfer.getData('text'),
|
10117
10826
|
obj = MODEL.objectByID(n);
|
10827
|
+
ev.preventDefault();
|
10118
10828
|
if(!obj) {
|
10119
10829
|
UI.alert(`Unknown entity ID "${n}"`);
|
10120
10830
|
} else if(this.chart_index >= 0) {
|
10121
|
-
// Only accept when all conditions are met
|
10122
|
-
ev.preventDefault();
|
10123
10831
|
if(obj instanceof DatasetModifier) {
|
10124
10832
|
// Equations can be added directly as chart variable
|
10125
|
-
this.addVariable(obj.
|
10833
|
+
this.addVariable(obj.selector);
|
10126
10834
|
return;
|
10127
10835
|
}
|
10128
10836
|
// For other entities, the attribute must be specified
|
@@ -10502,7 +11210,16 @@ class GUIChartManager extends ChartManager {
|
|
10502
11210
|
this.variable_index = vi;
|
10503
11211
|
this.updateDialog();
|
10504
11212
|
}
|
10505
|
-
|
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
|
+
|
10506
11223
|
editVariable() {
|
10507
11224
|
// Shows the edit (or rather: format) variable dialog
|
10508
11225
|
if(this.chart_index >= 0 && this.variable_index >= 0) {
|
@@ -10512,11 +11229,7 @@ class GUIChartManager extends ChartManager {
|
|
10512
11229
|
this.variable_modal.element('scale').value = VM.sig4Dig(cv.scale_factor);
|
10513
11230
|
this.variable_modal.element('width').value = VM.sig4Dig(cv.line_width);
|
10514
11231
|
this.variable_modal.element('color').style.backgroundColor = cv.color;
|
10515
|
-
|
10516
|
-
this.color_picker.color.hexString = cv.color;
|
10517
|
-
} catch(e) {
|
10518
|
-
this.color_picker.color.rgbString = cv.color;
|
10519
|
-
}
|
11232
|
+
this.setColorPicker(cv.color);
|
10520
11233
|
// Show change equation buttons only for equation variables
|
10521
11234
|
if(cv.object === MODEL.equations_dataset) {
|
10522
11235
|
this.change_equation_btns.style.display = 'block';
|
@@ -10527,6 +11240,34 @@ class GUIChartManager extends ChartManager {
|
|
10527
11240
|
}
|
10528
11241
|
}
|
10529
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
|
+
|
10530
11271
|
toggleVariable(vi) {
|
10531
11272
|
window.event.stopPropagation();
|
10532
11273
|
if(vi >= 0 && this.chart_index >= 0) {
|
@@ -10757,7 +11498,7 @@ class GUIChartManager extends ChartManager {
|
|
10757
11498
|
}
|
10758
11499
|
|
10759
11500
|
renderChartAsPNG() {
|
10760
|
-
localStorage.removeItem('png-url');
|
11501
|
+
window.localStorage.removeItem('png-url');
|
10761
11502
|
FILE_MANAGER.renderSVGAsPNG(MODEL.charts[this.chart_index].svg);
|
10762
11503
|
}
|
10763
11504
|
|
@@ -11552,7 +12293,10 @@ class GUIExperimentManager extends ExperimentManager {
|
|
11552
12293
|
this.default_message = document.getElementById('experiment-default-message');
|
11553
12294
|
|
11554
12295
|
this.design = document.getElementById('experiment-design');
|
12296
|
+
this.experiment_table = document.getElementById('experiment-table');
|
11555
12297
|
this.params_div = document.getElementById('experiment-params-div');
|
12298
|
+
this.dimension_table = document.getElementById('experiment-dim-table');
|
12299
|
+
this.chart_table = document.getElementById('experiment-chart-table');
|
11556
12300
|
// NOTE: the Exclude input field responds to several events
|
11557
12301
|
this.exclude = document.getElementById('experiment-exclude');
|
11558
12302
|
this.exclude.addEventListener(
|
@@ -11749,9 +12493,22 @@ class GUIExperimentManager extends ExperimentManager {
|
|
11749
12493
|
this.edited_dimension_index = -1;
|
11750
12494
|
this.edited_combi_selector_index = -1;
|
11751
12495
|
this.color_scale = new ColorScale('no');
|
12496
|
+
this.focal_table = null;
|
11752
12497
|
this.designMode();
|
11753
12498
|
}
|
11754
12499
|
|
12500
|
+
upDownKey(dir) {
|
12501
|
+
// Select row above or below the selected one (if possible)
|
12502
|
+
const srl = this.focal_table.getElementsByClassName('sel-set');
|
12503
|
+
if(srl.length > 0) {
|
12504
|
+
const r = this.focal_table.rows[srl[0].rowIndex + dir];
|
12505
|
+
if(r) {
|
12506
|
+
UI.scrollIntoView(r);
|
12507
|
+
r.dispatchEvent(new Event('click'));
|
12508
|
+
}
|
12509
|
+
}
|
12510
|
+
}
|
12511
|
+
|
11755
12512
|
updateDialog() {
|
11756
12513
|
this.updateChartList();
|
11757
12514
|
// Warn modeler if no meaningful experiments can be defined
|
@@ -11785,7 +12542,7 @@ class GUIExperimentManager extends ExperimentManager {
|
|
11785
12542
|
'\');" onmouseover="EXPERIMENT_MANAGER.showInfo(', xi,
|
11786
12543
|
', event.shiftKey);"><td>', x.title, '</td></tr>'].join(''));
|
11787
12544
|
}
|
11788
|
-
|
12545
|
+
this.experiment_table.innerHTML = xl.join('');
|
11789
12546
|
const
|
11790
12547
|
btns = 'xp-rename xp-view xp-delete xp-ignore',
|
11791
12548
|
icnt = document.getElementById('xp-ignore-count');
|
@@ -11849,7 +12606,7 @@ class GUIExperimentManager extends ExperimentManager {
|
|
11849
12606
|
setString(x.dimensions[i]),
|
11850
12607
|
'</td></tr>'].join(''));
|
11851
12608
|
}
|
11852
|
-
|
12609
|
+
this.dimension_table.innerHTML = tr.join('');
|
11853
12610
|
// Add button must be enabled only if there still are unused dimensions
|
11854
12611
|
if(x.available_dimensions.length > 0) {
|
11855
12612
|
document.getElementById('xp-d-add-btn').classList.remove('v-disab');
|
@@ -11865,8 +12622,9 @@ class GUIExperimentManager extends ExperimentManager {
|
|
11865
12622
|
i, '\');"><td>',
|
11866
12623
|
x.charts[i].title, '</td></tr>'].join(''));
|
11867
12624
|
}
|
11868
|
-
|
11869
|
-
|
12625
|
+
this.chart_table.innerHTML = tr.join('');
|
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;
|
11870
12628
|
if(tr.length >= this.suitable_charts.length) {
|
11871
12629
|
document.getElementById('xp-c-add-btn').classList.add('v-disab');
|
11872
12630
|
} else {
|
@@ -11886,7 +12644,7 @@ class GUIExperimentManager extends ExperimentManager {
|
|
11886
12644
|
dbtn.classList.add('v-disab');
|
11887
12645
|
cbtn.classList.add('v-disab');
|
11888
12646
|
}
|
11889
|
-
// Enable viewing only if > 1 dimensions and > 1
|
12647
|
+
// Enable viewing only if > 1 dimensions and > 1 outcome variables
|
11890
12648
|
if(canview) {
|
11891
12649
|
UI.enableButtons('xp-view');
|
11892
12650
|
} else {
|
@@ -11971,14 +12729,11 @@ class GUIExperimentManager extends ExperimentManager {
|
|
11971
12729
|
const x = this.selected_experiment;
|
11972
12730
|
if(x) {
|
11973
12731
|
x.inferVariables();
|
11974
|
-
if(x.selected_variable === '') {
|
11975
|
-
x.selected_variable = x.variables[0].displayName;
|
11976
|
-
}
|
11977
12732
|
const
|
11978
12733
|
ol = [],
|
11979
12734
|
vl = MODEL.outcomeNames;
|
11980
12735
|
for(let i = 0; i < x.variables.length; i++) {
|
11981
|
-
|
12736
|
+
addDistinct(x.variables[i].displayName, vl);
|
11982
12737
|
}
|
11983
12738
|
vl.sort(ciCompare);
|
11984
12739
|
for(let i = 0; i < vl.length; i++) {
|
@@ -11987,6 +12742,9 @@ class GUIExperimentManager extends ExperimentManager {
|
|
11987
12742
|
'>', vl[i], '</option>'].join(''));
|
11988
12743
|
}
|
11989
12744
|
document.getElementById('viewer-variable').innerHTML = ol.join('');
|
12745
|
+
if(x.selected_variable === '') {
|
12746
|
+
x.selected_variable = vl[0];
|
12747
|
+
}
|
11990
12748
|
}
|
11991
12749
|
}
|
11992
12750
|
|
@@ -12525,6 +13283,8 @@ N = ${rr.N}, vector length = ${rr.vector.length}` : '')].join('');
|
|
12525
13283
|
|
12526
13284
|
selectParameter(p) {
|
12527
13285
|
this.selected_parameter = p;
|
13286
|
+
this.focal_table = (p.startsWith('d') ? this.dimension_table :
|
13287
|
+
this.chart_table);
|
12528
13288
|
this.updateDialog();
|
12529
13289
|
}
|
12530
13290
|
|
@@ -14142,7 +14902,10 @@ class Finder {
|
|
14142
14902
|
this.copy_btn = document.getElementById('finder-copy-btn');
|
14143
14903
|
this.copy_btn.addEventListener(
|
14144
14904
|
'click', (event) => FINDER.copyAttributesToClipboard(event.shiftKey));
|
14145
|
-
|
14905
|
+
this.entity_table = document.getElementById('finder-table');
|
14906
|
+
this.item_table = document.getElementById('finder-item-table');
|
14907
|
+
this.expression_table = document.getElementById('finder-expression-table');
|
14908
|
+
|
14146
14909
|
// Attribute headers are used by Finder to output entity attribute values
|
14147
14910
|
this.attribute_headers = {
|
14148
14911
|
A: 'ACTORS:\tWeight\tCash IN\tCash OUT\tCash FLOW',
|
@@ -14174,6 +14937,47 @@ class Finder {
|
|
14174
14937
|
this.product_cluster_index = 0;
|
14175
14938
|
}
|
14176
14939
|
|
14940
|
+
doubleClicked(obj) {
|
14941
|
+
const
|
14942
|
+
now = Date.now(),
|
14943
|
+
dt = now - this.last_time_clicked;
|
14944
|
+
this.last_time_clicked = now;
|
14945
|
+
if(obj === this.clicked_object) {
|
14946
|
+
// Consider click to be "double" if it occurred less than 300 ms ago
|
14947
|
+
if(dt < 300) {
|
14948
|
+
this.last_time_clicked = 0;
|
14949
|
+
return true;
|
14950
|
+
}
|
14951
|
+
}
|
14952
|
+
this.clicked_object = obj;
|
14953
|
+
return false;
|
14954
|
+
}
|
14955
|
+
|
14956
|
+
enterKey() {
|
14957
|
+
// Open "edit properties" dialog for the selected entity
|
14958
|
+
const srl = this.entity_table.getElementsByClassName('sel-set');
|
14959
|
+
if(srl.length > 0) {
|
14960
|
+
const r = this.entity_table.rows[srl[0].rowIndex];
|
14961
|
+
if(r) {
|
14962
|
+
const e = new Event('click');
|
14963
|
+
e.altKey = true;
|
14964
|
+
r.dispatchEvent(e);
|
14965
|
+
}
|
14966
|
+
}
|
14967
|
+
}
|
14968
|
+
|
14969
|
+
upDownKey(dir) {
|
14970
|
+
// Select row above or below the selected one (if possible)
|
14971
|
+
const srl = this.entity_table.getElementsByClassName('sel-set');
|
14972
|
+
if(srl.length > 0) {
|
14973
|
+
const r = this.entity_table.rows[srl[0].rowIndex + dir];
|
14974
|
+
if(r) {
|
14975
|
+
UI.scrollIntoView(r);
|
14976
|
+
r.dispatchEvent(new Event('click'));
|
14977
|
+
}
|
14978
|
+
}
|
14979
|
+
}
|
14980
|
+
|
14177
14981
|
updateDialog() {
|
14178
14982
|
const
|
14179
14983
|
el = [],
|
@@ -14289,7 +15093,7 @@ class Finder {
|
|
14289
15093
|
if(e === se) seid += i;
|
14290
15094
|
el.push(['<tr id="etr', i, '" class="dataset',
|
14291
15095
|
(e === se ? ' sel-set' : ''), '" onclick="FINDER.selectEntity(\'',
|
14292
|
-
enl[i], '\');" onmouseover="FINDER.showInfo(\'', enl[i],
|
15096
|
+
enl[i], '\', event.altKey);" onmouseover="FINDER.showInfo(\'', enl[i],
|
14293
15097
|
'\', event.shiftKey);"><td draggable="true" ',
|
14294
15098
|
'ondragstart="FINDER.drag(event);"><img class="finder" src="images/',
|
14295
15099
|
e.type.toLowerCase(), '.png">', e.displayName,
|
@@ -14297,7 +15101,7 @@ class Finder {
|
|
14297
15101
|
}
|
14298
15102
|
// NOTE: reset `selected_entity` if not in the new list
|
14299
15103
|
if(seid === 'etr') this.selected_entity = null;
|
14300
|
-
|
15104
|
+
this.entity_table.innerHTML = el.join('');
|
14301
15105
|
UI.scrollIntoView(document.getElementById(seid));
|
14302
15106
|
document.getElementById('finder-count').innerHTML = pluralS(
|
14303
15107
|
el.length, 'entity', 'entities');
|
@@ -14362,7 +15166,7 @@ class Finder {
|
|
14362
15166
|
const
|
14363
15167
|
raw = escapeRegex(se.displayName),
|
14364
15168
|
re = new RegExp(
|
14365
|
-
'\\[\\s
|
15169
|
+
'\\[\\s*!?' + raw.replace(/\s+/g, '\\s+') + '\\s*[\\|\\@\\]]');
|
14366
15170
|
// Check actor weight expressions
|
14367
15171
|
for(let k in MODEL.actors) if(MODEL.actors.hasOwnProperty(k)) {
|
14368
15172
|
const a = MODEL.actors[k];
|
@@ -14452,7 +15256,7 @@ class Finder {
|
|
14452
15256
|
e.type.toLowerCase(), '.png">', e.displayName,
|
14453
15257
|
'</td></tr>'].join(''));
|
14454
15258
|
}
|
14455
|
-
|
15259
|
+
this.item_table.innerHTML = el.join('');
|
14456
15260
|
// Clear the table row list
|
14457
15261
|
el.length = 0;
|
14458
15262
|
// Now fill it with entity+attribute having a matching expression
|
@@ -14480,7 +15284,7 @@ class Finder {
|
|
14480
15284
|
'<img class="finder" src="images/', img, '.png">', td, '</td></tr>'
|
14481
15285
|
].join(''));
|
14482
15286
|
}
|
14483
|
-
|
15287
|
+
this.expression_table.innerHTML = el.join('');
|
14484
15288
|
document.getElementById('finder-expression-hdr').innerHTML =
|
14485
15289
|
pluralS(el.length, 'expression');
|
14486
15290
|
}
|
@@ -14515,10 +15319,37 @@ class Finder {
|
|
14515
15319
|
if(e) DOCUMENTATION_MANAGER.update(e, shift);
|
14516
15320
|
}
|
14517
15321
|
|
14518
|
-
selectEntity(id) {
|
14519
|
-
// Looks up entity, selects it in the left pane, and updates the
|
14520
|
-
|
15322
|
+
selectEntity(id, alt=false) {
|
15323
|
+
// Looks up entity, selects it in the left pane, and updates the
|
15324
|
+
// right pane; opens the "edit properties" modal dialog on double-click
|
15325
|
+
// and Alt-click if the entity is editable
|
15326
|
+
const obj = MODEL.objectByID(id);
|
15327
|
+
this.selected_entity = obj;
|
14521
15328
|
this.updateDialog();
|
15329
|
+
if(!obj) return;
|
15330
|
+
if(alt || this.doubleClicked(obj)) {
|
15331
|
+
if(obj instanceof Process) {
|
15332
|
+
UI.showProcessPropertiesDialog(obj);
|
15333
|
+
} else if(obj instanceof Product) {
|
15334
|
+
UI.showProductPropertiesDialog(obj);
|
15335
|
+
} else if(obj instanceof Link) {
|
15336
|
+
UI.showLinkPropertiesDialog(obj);
|
15337
|
+
} else if(obj instanceof Note) {
|
15338
|
+
obj.showNotePropertiesDialog();
|
15339
|
+
} else if(obj instanceof Dataset) {
|
15340
|
+
if(UI.hidden('dataset-dlg')) {
|
15341
|
+
UI.buttons.dataset.dispatchEvent(new Event('click'));
|
15342
|
+
}
|
15343
|
+
DATASET_MANAGER.selected_dataset = obj;
|
15344
|
+
DATASET_MANAGER.updateDialog();
|
15345
|
+
} else if(obj instanceof DatasetModifier) {
|
15346
|
+
if(UI.hidden('equation-dlg')) {
|
15347
|
+
UI.buttons.equation.dispatchEvent(new Event('click'));
|
15348
|
+
}
|
15349
|
+
EQUATION_MANAGER.selected_modifier = obj;
|
15350
|
+
EQUATION_MANAGER.updateDialog();
|
15351
|
+
}
|
15352
|
+
}
|
14522
15353
|
}
|
14523
15354
|
|
14524
15355
|
reveal(id) {
|
@@ -14569,22 +15400,12 @@ class Finder {
|
|
14569
15400
|
// NOTE: return the object to save a second lookup by revealExpression
|
14570
15401
|
return obj;
|
14571
15402
|
}
|
14572
|
-
|
15403
|
+
|
14573
15404
|
revealExpression(id, attr, shift=false, alt=false) {
|
14574
|
-
const
|
14575
|
-
|
14576
|
-
|
14577
|
-
|
14578
|
-
this.last_time_clicked = now;
|
14579
|
-
if(obj === this.clicked_object) {
|
14580
|
-
// Consider click to be "double" if it occurred less than 300 ms ago
|
14581
|
-
if(dt < 300) {
|
14582
|
-
this.last_time_clicked = 0;
|
14583
|
-
shift = true;
|
14584
|
-
}
|
14585
|
-
}
|
14586
|
-
this.clicked_object = obj;
|
14587
|
-
if(obj && attr && (shift || alt)) {
|
15405
|
+
const obj = this.reveal(id);
|
15406
|
+
if(!obj) return;
|
15407
|
+
shift = shift || this.doubleClicked(obj);
|
15408
|
+
if(attr && (shift || alt)) {
|
14588
15409
|
if(obj instanceof Process) {
|
14589
15410
|
// NOTE: the second argument makes the dialog focus on the specified
|
14590
15411
|
// attribute input field; the third makes it open the expression editor
|