linny-r 1.4.1 → 1.4.3
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 +69 -35
- package/package.json +1 -1
- package/server.js +114 -43
- package/static/images/octaeder.svg +993 -0
- package/static/index.html +22 -617
- package/static/linny-r.css +32 -378
- package/static/scripts/linny-r-ctrl.js +59 -5
- package/static/scripts/linny-r-gui.js +198 -56
- package/static/scripts/linny-r-model.js +127 -41
- package/static/scripts/linny-r-utils.js +32 -11
- package/static/scripts/linny-r-vm.js +40 -14
@@ -2789,13 +2789,15 @@ class Paper {
|
|
2789
2789
|
} // END of class Paper
|
2790
2790
|
|
2791
2791
|
|
2792
|
-
// CLASS ModalDialog provides basic modal dialog functionality
|
2792
|
+
// CLASS ModalDialog provides basic modal dialog functionality.
|
2793
2793
|
class ModalDialog {
|
2794
2794
|
constructor(id) {
|
2795
2795
|
this.id = id;
|
2796
2796
|
this.modal = document.getElementById(id + '-modal');
|
2797
2797
|
this.dialog = document.getElementById(id + '-dlg');
|
2798
|
-
// NOTE:
|
2798
|
+
// NOTE: Dialog title and button properties will be `undefined` if
|
2799
|
+
// not in the header DIV child of the dialog DIV element.
|
2800
|
+
this.title = this.dialog.getElementsByClassName('dlg-title')[0];
|
2799
2801
|
this.ok = this.dialog.getElementsByClassName('ok-btn')[0];
|
2800
2802
|
this.cancel = this.dialog.getElementsByClassName('cancel-btn')[0];
|
2801
2803
|
this.info = this.dialog.getElementsByClassName('info-btn')[0];
|
@@ -3266,9 +3268,13 @@ class GUIController extends Controller {
|
|
3266
3268
|
// The CHECK UPDATE dialog appears when a new version is detected
|
3267
3269
|
this.check_update_modal = new ModalDialog('check-update');
|
3268
3270
|
this.check_update_modal.ok.addEventListener('click',
|
3269
|
-
() => UI.
|
3271
|
+
() => UI.shutDownToUpdate());
|
3270
3272
|
this.check_update_modal.cancel.addEventListener('click',
|
3271
|
-
() => UI.
|
3273
|
+
() => UI.preventUpdate());
|
3274
|
+
|
3275
|
+
// The UPDATING modal appears when updating has started.
|
3276
|
+
// NOTE: This modal has no OK or CANCEL buttons.
|
3277
|
+
this.updating_modal = new ModalDialog('updating');
|
3272
3278
|
|
3273
3279
|
// Add all draggable stay-on-top dialogs as controller properties
|
3274
3280
|
|
@@ -3399,7 +3405,7 @@ class GUIController extends Controller {
|
|
3399
3405
|
}
|
3400
3406
|
|
3401
3407
|
drawLinkArrows(cluster, link) {
|
3402
|
-
// Draw all arrows in `cluster` that represent `link
|
3408
|
+
// Draw all arrows in `cluster` that represent `link`.
|
3403
3409
|
for(let i = 0; i < cluster.arrows.length; i++) {
|
3404
3410
|
const a = cluster.arrows[i];
|
3405
3411
|
if(a.links.indexOf(link) >= 0) this.paper.drawArrow(a);
|
@@ -3407,19 +3413,124 @@ class GUIController extends Controller {
|
|
3407
3413
|
}
|
3408
3414
|
|
3409
3415
|
shutDownServer() {
|
3410
|
-
//
|
3416
|
+
// This terminates the local host server script and display a plain
|
3417
|
+
// HTML message in the browser with a restart button.
|
3411
3418
|
if(!SOLVER.user_id) window.open('./shutdown', '_self');
|
3412
3419
|
}
|
3413
3420
|
|
3421
|
+
shutDownToUpdate() {
|
3422
|
+
// Sisgnal server that an update is required. This will close the
|
3423
|
+
// local host Linny-R server. If this server was started by the
|
3424
|
+
// standard OS batch script, this script will proceed to update
|
3425
|
+
// Linny-R via npm and then restart the server again. If not, the
|
3426
|
+
// fetch request will time out, anf the user will be warned.
|
3427
|
+
if(SOLVER.user_id) return;
|
3428
|
+
fetch('update/')
|
3429
|
+
.then((response) => {
|
3430
|
+
if(!response.ok) {
|
3431
|
+
UI.alert(`ERROR ${response.status}: ${response.statusText}`);
|
3432
|
+
}
|
3433
|
+
return response.text();
|
3434
|
+
})
|
3435
|
+
.then((data) => {
|
3436
|
+
if(UI.postResponseOK(data, true)) {
|
3437
|
+
UI.check_update_modal.hide();
|
3438
|
+
if(data.startsWith('Installing')) UI.waitToRestart();
|
3439
|
+
}
|
3440
|
+
})
|
3441
|
+
.catch((err) => {
|
3442
|
+
UI.warn(UI.WARNING.NO_CONNECTION, err);
|
3443
|
+
});
|
3444
|
+
}
|
3445
|
+
|
3446
|
+
waitToRestart() {
|
3447
|
+
// Shows the "update in progress" dialog and then fetches the current
|
3448
|
+
// version page from the server. Always wait for 5 seconds to permit
|
3449
|
+
// reading the text, and ensure that the server has been stopped.
|
3450
|
+
// Only then try to restart.
|
3451
|
+
if(SOLVER.user_id) return;
|
3452
|
+
UI.updating_modal.show();
|
3453
|
+
setTimeout(() => UI.tryToRestart(0), 5000);
|
3454
|
+
}
|
3455
|
+
|
3456
|
+
tryToRestart(trials) {
|
3457
|
+
// Fetch the current version number from the server. This may take
|
3458
|
+
// a wile, as the server was shut down and restarts only after npm
|
3459
|
+
// has updated the Linny-R software. Typically, this takes only a few
|
3460
|
+
// seconds, but the connection with the npm server may be slow.
|
3461
|
+
// Default timeout on Firefox (90 seconds) and Chrome (300 seconds)
|
3462
|
+
// should amply suffice, though, hence no provision for a second attempt.
|
3463
|
+
fetch('version/')
|
3464
|
+
.then((response) => {
|
3465
|
+
if(!response.ok) {
|
3466
|
+
UI.alert(`ERROR ${response.status}: ${response.statusText}`);
|
3467
|
+
}
|
3468
|
+
return response.text();
|
3469
|
+
})
|
3470
|
+
.then((data) => {
|
3471
|
+
if(UI.postResponseOK(data)) {
|
3472
|
+
// Change the dialog text in case the user does not confirm
|
3473
|
+
// when prompted by the browser to leave the page.
|
3474
|
+
const
|
3475
|
+
m = data.match(/(\d+\.\d+\.\d+)/),
|
3476
|
+
md = UI.updating_modal;
|
3477
|
+
md.title.innerText = 'Update terminated';
|
3478
|
+
let msg = [];
|
3479
|
+
if(m) {
|
3480
|
+
msg.push(
|
3481
|
+
`Linny-R version ${m[1]} has been installed.`,
|
3482
|
+
'To continue, you must reload this page, and',
|
3483
|
+
'confirm when prompted by your browser.');
|
3484
|
+
} else {
|
3485
|
+
// Inform user that install appears to have failed.
|
3486
|
+
msg.push(
|
3487
|
+
'Installation of new version may <strong>not</strong> have',
|
3488
|
+
'been successful. Please check the CLI for',
|
3489
|
+
'error messages or warnings.');
|
3490
|
+
}
|
3491
|
+
md.element('msg').innerHTML = msg.join('<br>');
|
3492
|
+
// Reload `index.html`. This will start Linny-R anew.
|
3493
|
+
window.open('./', '_self');
|
3494
|
+
}
|
3495
|
+
})
|
3496
|
+
.catch((err) => {
|
3497
|
+
if(trials < 10) {
|
3498
|
+
setTimeout(() => UI.tryToRestart(trials + 1), 5000);
|
3499
|
+
} else {
|
3500
|
+
UI.warn(UI.WARNING.NO_CONNECTION, err);
|
3501
|
+
}
|
3502
|
+
});
|
3503
|
+
}
|
3504
|
+
|
3505
|
+
preventUpdate() {
|
3506
|
+
// Signal server that no update is required.
|
3507
|
+
if(SOLVER.user_id) return;
|
3508
|
+
fetch('no-update/')
|
3509
|
+
.then((response) => {
|
3510
|
+
if(!response.ok) {
|
3511
|
+
UI.alert(`ERROR ${response.status}: ${response.statusText}`);
|
3512
|
+
}
|
3513
|
+
return response.text();
|
3514
|
+
})
|
3515
|
+
.then((data) => {
|
3516
|
+
if(UI.postResponseOK(data, true)) UI.check_update_modal.hide();
|
3517
|
+
})
|
3518
|
+
.catch((err) => {
|
3519
|
+
UI.warn(UI.WARNING.NO_CONNECTION, err);
|
3520
|
+
UI.check_update_modal.hide();
|
3521
|
+
});
|
3522
|
+
}
|
3523
|
+
|
3414
3524
|
loginPrompt() {
|
3415
|
-
// Show the server logon modal
|
3525
|
+
// Show the server logon modal.
|
3416
3526
|
this.modals.logon.element('name').value = SOLVER.user_id;
|
3417
3527
|
this.modals.logon.element('password').value = '';
|
3418
3528
|
this.modals.logon.show('password');
|
3419
3529
|
}
|
3420
3530
|
|
3421
3531
|
rotatingIcon(rotate=false) {
|
3422
|
-
// Controls the appearance of the Linny-R icon
|
3532
|
+
// Controls the appearance of the Linny-R icon in the top-left
|
3533
|
+
// corner of the browser window.
|
3423
3534
|
const
|
3424
3535
|
si = document.getElementById('static-icon'),
|
3425
3536
|
ri = document.getElementById('rotating-icon');
|
@@ -3433,47 +3544,47 @@ class GUIController extends Controller {
|
|
3433
3544
|
}
|
3434
3545
|
|
3435
3546
|
updateTimeStep(t=MODEL.simulationTimeStep) {
|
3436
|
-
//
|
3437
|
-
// NOTE:
|
3547
|
+
// Display `t` as the current time step.
|
3548
|
+
// NOTE: The Virtual Machine passes its relative time `VM.t`.
|
3438
3549
|
document.getElementById('step').innerHTML = t;
|
3439
3550
|
}
|
3440
3551
|
|
3441
3552
|
stopSolving() {
|
3442
|
-
// Reset solver-related GUI elements and notify modeler
|
3553
|
+
// Reset solver-related GUI elements and notify modeler.
|
3443
3554
|
super.stopSolving();
|
3444
3555
|
this.buttons.solve.classList.remove('off');
|
3445
3556
|
this.buttons.stop.classList.remove('blink');
|
3446
3557
|
this.buttons.stop.classList.add('off');
|
3447
3558
|
this.rotatingIcon(false);
|
3448
|
-
// Update the time step on the status bar
|
3559
|
+
// Update the time step on the status bar.
|
3449
3560
|
this.updateTimeStep();
|
3450
3561
|
}
|
3451
3562
|
|
3452
3563
|
readyToSolve() {
|
3453
|
-
// Set Stop and Reset buttons to their initial state
|
3564
|
+
// Set Stop and Reset buttons to their initial state.
|
3454
3565
|
UI.buttons.stop.classList.remove('blink');
|
3455
3566
|
// Hide the reset button
|
3456
3567
|
UI.buttons.reset.classList.add('off');
|
3457
3568
|
}
|
3458
3569
|
|
3459
3570
|
startSolving() {
|
3460
|
-
// Hide Start button and show Stop button
|
3571
|
+
// Hide Start button and show Stop button.
|
3461
3572
|
UI.buttons.solve.classList.add('off');
|
3462
3573
|
UI.buttons.stop.classList.remove('off');
|
3463
3574
|
}
|
3464
3575
|
|
3465
3576
|
waitToStop() {
|
3466
|
-
// Make Stop button blink to indicate "halting -- please wait"
|
3577
|
+
// Make Stop button blink to indicate "halting -- please wait".
|
3467
3578
|
UI.buttons.stop.classList.add('blink');
|
3468
3579
|
}
|
3469
3580
|
|
3470
3581
|
readyToReset() {
|
3471
|
-
// Show the Reset button
|
3582
|
+
// Show the Reset button.
|
3472
3583
|
UI.buttons.reset.classList.remove('off');
|
3473
3584
|
}
|
3474
3585
|
|
3475
3586
|
reset() {
|
3476
|
-
// Reset properties related to cursor position on diagram
|
3587
|
+
// Reset properties related to cursor position on diagram.
|
3477
3588
|
this.on_node = null;
|
3478
3589
|
this.on_arrow = null;
|
3479
3590
|
this.on_cluster = null;
|
@@ -3527,7 +3638,7 @@ class GUIController extends Controller {
|
|
3527
3638
|
jumpToIssue() {
|
3528
3639
|
// Set time step to the one of the warning message for the issue
|
3529
3640
|
// index, redraw the diagram if needed, and display the message
|
3530
|
-
// on the infoline
|
3641
|
+
// on the infoline.
|
3531
3642
|
if(VM.issue_index >= 0) {
|
3532
3643
|
const
|
3533
3644
|
issue = VM.issue_list[VM.issue_index],
|
@@ -7356,7 +7467,8 @@ NOTE: Grouping groups results in a single group, e.g., (1;2);(3;4;5) evaluates a
|
|
7356
7467
|
// is passed to differentiate between the DOM elements to be used
|
7357
7468
|
const
|
7358
7469
|
type = document.getElementById(prefix + 'variable-obj').value,
|
7359
|
-
n_list = this.namesByType(VM.object_types[type]).sort(
|
7470
|
+
n_list = this.namesByType(VM.object_types[type]).sort(
|
7471
|
+
(a, b) => UI.compareFullNames(a, b)),
|
7360
7472
|
vn = document.getElementById(prefix + 'variable-name'),
|
7361
7473
|
options = [];
|
7362
7474
|
// Add "empty" as first and initial option, but disable it.
|
@@ -7398,7 +7510,7 @@ NOTE: Grouping groups results in a single group, e.g., (1;2);(3;4;5) evaluates a
|
|
7398
7510
|
slist.push(d.modifiers[m].selector);
|
7399
7511
|
}
|
7400
7512
|
// Sort to present equations in alphabetical order
|
7401
|
-
slist.sort(
|
7513
|
+
slist.sort((a, b) => UI.compareFullNames(a, b));
|
7402
7514
|
for(let i = 0; i < slist.length; i++) {
|
7403
7515
|
options.push(`<option value="${slist[i]}">${slist[i]}</option>`);
|
7404
7516
|
}
|
@@ -9900,13 +10012,7 @@ class GUIDatasetManager extends DatasetManager {
|
|
9900
10012
|
dl = [],
|
9901
10013
|
dnl = [],
|
9902
10014
|
sd = this.selected_dataset,
|
9903
|
-
ioclass = ['', 'import', 'export']
|
9904
|
-
ciPrefixCompare = (a, b) => {
|
9905
|
-
const
|
9906
|
-
pa = a.split(':_').join(' '),
|
9907
|
-
pb = b.split(':_').join(' ');
|
9908
|
-
return ciCompare(pa, pb);
|
9909
|
-
};
|
10015
|
+
ioclass = ['', 'import', 'export'];
|
9910
10016
|
for(let d in MODEL.datasets) if(MODEL.datasets.hasOwnProperty(d) &&
|
9911
10017
|
// NOTE: do not list "black-boxed" entities
|
9912
10018
|
!d.startsWith(UI.BLACK_BOX) &&
|
@@ -9917,7 +10023,7 @@ class GUIDatasetManager extends DatasetManager {
|
|
9917
10023
|
dnl.push(d);
|
9918
10024
|
}
|
9919
10025
|
}
|
9920
|
-
dnl.sort(
|
10026
|
+
dnl.sort((a, b) => UI.compareFullNames(a, b, true));
|
9921
10027
|
// First determine indentation levels, prefixes and names
|
9922
10028
|
const
|
9923
10029
|
indent = [],
|
@@ -10678,7 +10784,7 @@ class GUIDatasetManager extends DatasetManager {
|
|
10678
10784
|
const
|
10679
10785
|
ln = document.getElementById('series-line-number'),
|
10680
10786
|
lc = document.getElementById('series-line-count');
|
10681
|
-
ln.innerHTML = this.series_data.value.
|
10787
|
+
ln.innerHTML = this.series_data.value.substring(0,
|
10682
10788
|
this.series_data.selectionStart).split('\n').length;
|
10683
10789
|
lc.innerHTML = this.series_data.value.split('\n').length;
|
10684
10790
|
}
|
@@ -12190,7 +12296,7 @@ class GUISensitivityAnalysis extends SensitivityAnalysis {
|
|
12190
12296
|
const
|
12191
12297
|
ds_dict = MODEL.listOfAllSelectors,
|
12192
12298
|
html = [],
|
12193
|
-
sl = Object.keys(ds_dict).sort(
|
12299
|
+
sl = Object.keys(ds_dict).sort((a, b) => UI.compareFullNames(a, b, true));
|
12194
12300
|
for(let i = 0; i < sl.length; i++) {
|
12195
12301
|
const
|
12196
12302
|
s = sl[i],
|
@@ -13175,7 +13281,7 @@ class GUIExperimentManager extends ExperimentManager {
|
|
13175
13281
|
for(let i = 0; i < x.variables.length; i++) {
|
13176
13282
|
addDistinct(x.variables[i].displayName, vl);
|
13177
13283
|
}
|
13178
|
-
vl.sort(
|
13284
|
+
vl.sort((a, b) => UI.compareFullNames(a, b));
|
13179
13285
|
for(let i = 0; i < vl.length; i++) {
|
13180
13286
|
ol.push(['<option value="', vl[i], '"',
|
13181
13287
|
(vl[i] == x.selected_variable ? ' selected="selected"' : ''),
|
@@ -15336,9 +15442,11 @@ class Finder {
|
|
15336
15442
|
this.close_btn = document.getElementById('finder-close-btn');
|
15337
15443
|
// Make toolbar buttons responsive
|
15338
15444
|
this.close_btn.addEventListener('click', (e) => UI.toggleDialog(e));
|
15339
|
-
this.entities = [];
|
15340
15445
|
this.filter_input = document.getElementById('finder-filter-text');
|
15341
15446
|
this.filter_input.addEventListener('input', () => FINDER.changeFilter());
|
15447
|
+
this.edit_btn = document.getElementById('finder-edit-btn');
|
15448
|
+
this.edit_btn.addEventListener(
|
15449
|
+
'click', (event) => FINDER.editAttributes(event.shiftKey));
|
15342
15450
|
this.copy_btn = document.getElementById('finder-copy-btn');
|
15343
15451
|
this.copy_btn.addEventListener(
|
15344
15452
|
'click', (event) => FINDER.copyAttributesToClipboard(event.shiftKey));
|
@@ -15346,7 +15454,7 @@ class Finder {
|
|
15346
15454
|
this.item_table = document.getElementById('finder-item-table');
|
15347
15455
|
this.expression_table = document.getElementById('finder-expression-table');
|
15348
15456
|
|
15349
|
-
|
15457
|
+
// Attribute headers are used by Finder to output entity attribute values.
|
15350
15458
|
this.attribute_headers = {
|
15351
15459
|
A: 'ACTORS:\tWeight\tCash IN\tCash OUT\tCash FLOW',
|
15352
15460
|
B: 'CONSTRAINTS (no attributes)',
|
@@ -15359,12 +15467,15 @@ class Finder {
|
|
15359
15467
|
Q: 'PRODUCTS:\tLower bound\tUpper bound\tInitial level\tPrice' +
|
15360
15468
|
'\tLevel\tCost price\tHighest cost price'
|
15361
15469
|
};
|
15362
|
-
// Set own properties
|
15470
|
+
// Set own properties.
|
15471
|
+
this.entities = [];
|
15472
|
+
this.filtered_types = [];
|
15363
15473
|
this.reset();
|
15364
15474
|
}
|
15365
15475
|
|
15366
15476
|
reset() {
|
15367
15477
|
this.entities.length = 0;
|
15478
|
+
this.filtered_types.length = 0;
|
15368
15479
|
this.selected_entity = null;
|
15369
15480
|
this.filter_input.value = '';
|
15370
15481
|
this.filter_pattern = null;
|
@@ -15373,7 +15484,7 @@ class Finder {
|
|
15373
15484
|
this.last_time_clicked = 0;
|
15374
15485
|
this.clicked_object = null;
|
15375
15486
|
// Product cluster index "remembers" for which cluster a product was
|
15376
|
-
// last revealed, so it can reveal the next cluster when clicked again
|
15487
|
+
// last revealed, so it can reveal the next cluster when clicked again.
|
15377
15488
|
this.product_cluster_index = 0;
|
15378
15489
|
}
|
15379
15490
|
|
@@ -15383,7 +15494,7 @@ class Finder {
|
|
15383
15494
|
dt = now - this.last_time_clicked;
|
15384
15495
|
this.last_time_clicked = now;
|
15385
15496
|
if(obj === this.clicked_object) {
|
15386
|
-
// Consider click to be "double" if it occurred less than 300 ms ago
|
15497
|
+
// Consider click to be "double" if it occurred less than 300 ms ago.
|
15387
15498
|
if(dt < 300) {
|
15388
15499
|
this.last_time_clicked = 0;
|
15389
15500
|
return true;
|
@@ -15427,6 +15538,7 @@ class Finder {
|
|
15427
15538
|
fp = this.filter_pattern && this.filter_pattern.length > 0;
|
15428
15539
|
let imgs = '';
|
15429
15540
|
this.entities.length = 0;
|
15541
|
+
this.filtered_types.length = 0;
|
15430
15542
|
// No list unless a pattern OR a specified SUB-set of entity types
|
15431
15543
|
if(fp || et && et !== VM.entity_letters) {
|
15432
15544
|
if(et.indexOf('A') >= 0) {
|
@@ -15435,6 +15547,7 @@ class Finder {
|
|
15435
15547
|
if(!fp || patternMatch(MODEL.actors[k].name, this.filter_pattern)) {
|
15436
15548
|
enl.push(k);
|
15437
15549
|
this.entities.push(MODEL.actors[k]);
|
15550
|
+
addDistinct('A', this.filtered_types);
|
15438
15551
|
}
|
15439
15552
|
}
|
15440
15553
|
}
|
@@ -15446,6 +15559,7 @@ class Finder {
|
|
15446
15559
|
MODEL.processes[k].displayName, this.filter_pattern))) {
|
15447
15560
|
enl.push(k);
|
15448
15561
|
this.entities.push(MODEL.processes[k]);
|
15562
|
+
addDistinct('P', this.filtered_types);
|
15449
15563
|
}
|
15450
15564
|
}
|
15451
15565
|
}
|
@@ -15456,6 +15570,7 @@ class Finder {
|
|
15456
15570
|
MODEL.products[k].displayName, this.filter_pattern))) {
|
15457
15571
|
enl.push(k);
|
15458
15572
|
this.entities.push(MODEL.products[k]);
|
15573
|
+
addDistinct('Q', this.filtered_types);
|
15459
15574
|
}
|
15460
15575
|
}
|
15461
15576
|
}
|
@@ -15466,6 +15581,7 @@ class Finder {
|
|
15466
15581
|
MODEL.clusters[k].displayName, this.filter_pattern))) {
|
15467
15582
|
enl.push(k);
|
15468
15583
|
this.entities.push(MODEL.clusters[k]);
|
15584
|
+
addDistinct('C', this.filtered_types);
|
15469
15585
|
}
|
15470
15586
|
}
|
15471
15587
|
}
|
@@ -15479,6 +15595,7 @@ class Finder {
|
|
15479
15595
|
if(ds !== MODEL.equations_dataset) {
|
15480
15596
|
enl.push(k);
|
15481
15597
|
this.entities.push(MODEL.datasets[k]);
|
15598
|
+
addDistinct('D', this.filtered_types);
|
15482
15599
|
}
|
15483
15600
|
}
|
15484
15601
|
}
|
@@ -15492,6 +15609,7 @@ class Finder {
|
|
15492
15609
|
this.filter_pattern)) {
|
15493
15610
|
enl.push(k);
|
15494
15611
|
this.entities.push(MODEL.equations_dataset.modifiers[k]);
|
15612
|
+
addDistinct('E', this.filtered_types);
|
15495
15613
|
}
|
15496
15614
|
}
|
15497
15615
|
}
|
@@ -15508,6 +15626,7 @@ class Finder {
|
|
15508
15626
|
if(!bb && (!fp || patternMatch(ldn, this.filter_pattern))) {
|
15509
15627
|
enl.push(k);
|
15510
15628
|
this.entities.push(l);
|
15629
|
+
addDistinct('L', this.filtered_types);
|
15511
15630
|
}
|
15512
15631
|
}
|
15513
15632
|
}
|
@@ -15520,11 +15639,12 @@ class Finder {
|
|
15520
15639
|
MODEL.constraints[k].displayName, this.filter_pattern))) {
|
15521
15640
|
enl.push(k);
|
15522
15641
|
this.entities.push(MODEL.constraints[k]);
|
15642
|
+
addDistinct('B', this.filtered_types);
|
15523
15643
|
}
|
15524
15644
|
}
|
15525
15645
|
}
|
15526
15646
|
}
|
15527
|
-
enl.sort(
|
15647
|
+
enl.sort((a, b) => UI.compareFullNames(a, b, true));
|
15528
15648
|
}
|
15529
15649
|
document.getElementById('finder-entity-imgs').innerHTML = imgs;
|
15530
15650
|
let seid = 'etr';
|
@@ -15545,10 +15665,24 @@ class Finder {
|
|
15545
15665
|
UI.scrollIntoView(document.getElementById(seid));
|
15546
15666
|
document.getElementById('finder-count').innerHTML = pluralS(
|
15547
15667
|
el.length, 'entity', 'entities');
|
15548
|
-
if
|
15668
|
+
// Only show the edit button if all filtered entities are of the
|
15669
|
+
// same type.
|
15670
|
+
let n = el.length;
|
15671
|
+
this.edit_btn.style.display = 'none';
|
15672
|
+
this.copy_btn.style.display = 'none';
|
15673
|
+
if(n > 0) {
|
15549
15674
|
this.copy_btn.style.display = 'block';
|
15550
|
-
|
15551
|
-
this.
|
15675
|
+
const ft = this.filtered_types[0];
|
15676
|
+
if(this.filtered_types.length === 1 && 'DE'.indexOf(ft) < 0) {
|
15677
|
+
// NOTE: Attributes of "no actor" and top cluster cannot be edited.
|
15678
|
+
if((ft === 'A' && enl.indexOf('(no_actor)') >= 0) ||
|
15679
|
+
(ft === 'C' && enl.indexOf('(top_cluster)') >= 0)) n--;
|
15680
|
+
if(n > 0) {
|
15681
|
+
this.edit_btn.title = 'Edit attributes of ' +
|
15682
|
+
pluralS(n, VM.entity_names[ft]);
|
15683
|
+
this.edit_btn.style.display = 'block';
|
15684
|
+
}
|
15685
|
+
}
|
15552
15686
|
}
|
15553
15687
|
this.updateRightPane();
|
15554
15688
|
}
|
@@ -15774,6 +15908,10 @@ class Finder {
|
|
15774
15908
|
UI.showProductPropertiesDialog(obj);
|
15775
15909
|
} else if(obj instanceof Link) {
|
15776
15910
|
UI.showLinkPropertiesDialog(obj);
|
15911
|
+
} else if(obj instanceof Cluster && obj !== MODEL.top_cluster) {
|
15912
|
+
UI.showClusterPropertiesDialog(obj);
|
15913
|
+
} else if(obj instanceof Actor) {
|
15914
|
+
ACTOR_MANAGER.showEditActorDialog(obj.name, obj.weight.text);
|
15777
15915
|
} else if(obj instanceof Note) {
|
15778
15916
|
obj.showNotePropertiesDialog();
|
15779
15917
|
} else if(obj instanceof Dataset) {
|
@@ -15889,12 +16027,19 @@ class Finder {
|
|
15889
16027
|
}
|
15890
16028
|
}
|
15891
16029
|
|
16030
|
+
editAttributes(shift) {
|
16031
|
+
// Show the Edit properties dialog for the filtered-out entities.
|
16032
|
+
// These must all be of the same type, or the edit button will not
|
16033
|
+
// show. Just in case, check anyway.
|
16034
|
+
|
16035
|
+
}
|
16036
|
+
|
15892
16037
|
copyAttributesToClipboard(shift) {
|
15893
|
-
// Copy relevant entity attributes as tab-separated text to clipboard
|
15894
|
-
// NOTE:
|
15895
|
-
// that for each defined attribute (and if model has been
|
15896
|
-
// inferred attribute) has a property with its value.
|
15897
|
-
// expressions, the expression text is used
|
16038
|
+
// Copy relevant entity attributes as tab-separated text to clipboard.
|
16039
|
+
// NOTE: All entity types have "get" `attributes` that returns an
|
16040
|
+
// object that for each defined attribute (and if model has been
|
16041
|
+
// solved also each inferred attribute) has a property with its value.
|
16042
|
+
// For dynamic expressions, the expression text is used
|
15898
16043
|
const ea_dict = {A: [], B: [], C: [], D: [], E: [], L: [], P: [], Q: []};
|
15899
16044
|
let e = this.selected_entity;
|
15900
16045
|
if(shift && e) {
|
@@ -15914,18 +16059,15 @@ class Finder {
|
|
15914
16059
|
etl = seq[i],
|
15915
16060
|
ead = ea_dict[etl];
|
15916
16061
|
if(ead && ead.length > 0) {
|
15917
|
-
// No blank line before first entity type
|
16062
|
+
// No blank line before first entity type.
|
15918
16063
|
if(text.length > 0) text.push('');
|
15919
|
-
|
16064
|
+
const en = capitalized(VM.entity_names[etl]);
|
16065
|
+
let ah = en + '\t' + VM.entity_attribute_names[etl].join('\t');
|
16066
|
+
if(etl === 'L' || etl === 'B') ah = ah.replace(en, `${en} FROM\tTO`);
|
15920
16067
|
if(!MODEL.infer_cost_prices) {
|
15921
|
-
//
|
15922
|
-
|
15923
|
-
|
15924
|
-
ah = ah.substr(0, p);
|
15925
|
-
} else {
|
15926
|
-
// SOC is exogenous, and hence comes before F in header => replace
|
15927
|
-
ah = ah.replace('\tShare of cost', '');
|
15928
|
-
}
|
16068
|
+
// If no cost price calculation, trim associated attributes
|
16069
|
+
// from the header.
|
16070
|
+
ah = ah.replace('\tCost price', '').replace('\tShare of cost', '');
|
15929
16071
|
}
|
15930
16072
|
text.push(ah);
|
15931
16073
|
attr.length = 0;
|
@@ -16191,7 +16333,7 @@ class GUIReceiver {
|
|
16191
16333
|
return response.text();
|
16192
16334
|
})
|
16193
16335
|
.then((data) => {
|
16194
|
-
// For experiments, only display server response if warning or error
|
16336
|
+
// For experiments, only display server response if warning or error.
|
16195
16337
|
UI.postResponseOK(data, !RECEIVER.experiment);
|
16196
16338
|
// If execution completed, perform the call-back action if the
|
16197
16339
|
// receiver is active (so not when auto-reporting a run).
|