linny-r 1.4.2 → 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 +21 -612
- package/static/linny-r.css +32 -364
- package/static/scripts/linny-r-gui.js +188 -41
- package/static/scripts/linny-r-model.js +3 -0
- package/static/scripts/linny-r-utils.js +7 -2
- package/static/scripts/linny-r-vm.js +10 -0
@@ -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],
|
@@ -15331,9 +15442,11 @@ class Finder {
|
|
15331
15442
|
this.close_btn = document.getElementById('finder-close-btn');
|
15332
15443
|
// Make toolbar buttons responsive
|
15333
15444
|
this.close_btn.addEventListener('click', (e) => UI.toggleDialog(e));
|
15334
|
-
this.entities = [];
|
15335
15445
|
this.filter_input = document.getElementById('finder-filter-text');
|
15336
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));
|
15337
15450
|
this.copy_btn = document.getElementById('finder-copy-btn');
|
15338
15451
|
this.copy_btn.addEventListener(
|
15339
15452
|
'click', (event) => FINDER.copyAttributesToClipboard(event.shiftKey));
|
@@ -15341,7 +15454,7 @@ class Finder {
|
|
15341
15454
|
this.item_table = document.getElementById('finder-item-table');
|
15342
15455
|
this.expression_table = document.getElementById('finder-expression-table');
|
15343
15456
|
|
15344
|
-
|
15457
|
+
// Attribute headers are used by Finder to output entity attribute values.
|
15345
15458
|
this.attribute_headers = {
|
15346
15459
|
A: 'ACTORS:\tWeight\tCash IN\tCash OUT\tCash FLOW',
|
15347
15460
|
B: 'CONSTRAINTS (no attributes)',
|
@@ -15354,12 +15467,15 @@ class Finder {
|
|
15354
15467
|
Q: 'PRODUCTS:\tLower bound\tUpper bound\tInitial level\tPrice' +
|
15355
15468
|
'\tLevel\tCost price\tHighest cost price'
|
15356
15469
|
};
|
15357
|
-
// Set own properties
|
15470
|
+
// Set own properties.
|
15471
|
+
this.entities = [];
|
15472
|
+
this.filtered_types = [];
|
15358
15473
|
this.reset();
|
15359
15474
|
}
|
15360
15475
|
|
15361
15476
|
reset() {
|
15362
15477
|
this.entities.length = 0;
|
15478
|
+
this.filtered_types.length = 0;
|
15363
15479
|
this.selected_entity = null;
|
15364
15480
|
this.filter_input.value = '';
|
15365
15481
|
this.filter_pattern = null;
|
@@ -15368,7 +15484,7 @@ class Finder {
|
|
15368
15484
|
this.last_time_clicked = 0;
|
15369
15485
|
this.clicked_object = null;
|
15370
15486
|
// Product cluster index "remembers" for which cluster a product was
|
15371
|
-
// 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.
|
15372
15488
|
this.product_cluster_index = 0;
|
15373
15489
|
}
|
15374
15490
|
|
@@ -15378,7 +15494,7 @@ class Finder {
|
|
15378
15494
|
dt = now - this.last_time_clicked;
|
15379
15495
|
this.last_time_clicked = now;
|
15380
15496
|
if(obj === this.clicked_object) {
|
15381
|
-
// 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.
|
15382
15498
|
if(dt < 300) {
|
15383
15499
|
this.last_time_clicked = 0;
|
15384
15500
|
return true;
|
@@ -15422,6 +15538,7 @@ class Finder {
|
|
15422
15538
|
fp = this.filter_pattern && this.filter_pattern.length > 0;
|
15423
15539
|
let imgs = '';
|
15424
15540
|
this.entities.length = 0;
|
15541
|
+
this.filtered_types.length = 0;
|
15425
15542
|
// No list unless a pattern OR a specified SUB-set of entity types
|
15426
15543
|
if(fp || et && et !== VM.entity_letters) {
|
15427
15544
|
if(et.indexOf('A') >= 0) {
|
@@ -15430,6 +15547,7 @@ class Finder {
|
|
15430
15547
|
if(!fp || patternMatch(MODEL.actors[k].name, this.filter_pattern)) {
|
15431
15548
|
enl.push(k);
|
15432
15549
|
this.entities.push(MODEL.actors[k]);
|
15550
|
+
addDistinct('A', this.filtered_types);
|
15433
15551
|
}
|
15434
15552
|
}
|
15435
15553
|
}
|
@@ -15441,6 +15559,7 @@ class Finder {
|
|
15441
15559
|
MODEL.processes[k].displayName, this.filter_pattern))) {
|
15442
15560
|
enl.push(k);
|
15443
15561
|
this.entities.push(MODEL.processes[k]);
|
15562
|
+
addDistinct('P', this.filtered_types);
|
15444
15563
|
}
|
15445
15564
|
}
|
15446
15565
|
}
|
@@ -15451,6 +15570,7 @@ class Finder {
|
|
15451
15570
|
MODEL.products[k].displayName, this.filter_pattern))) {
|
15452
15571
|
enl.push(k);
|
15453
15572
|
this.entities.push(MODEL.products[k]);
|
15573
|
+
addDistinct('Q', this.filtered_types);
|
15454
15574
|
}
|
15455
15575
|
}
|
15456
15576
|
}
|
@@ -15461,6 +15581,7 @@ class Finder {
|
|
15461
15581
|
MODEL.clusters[k].displayName, this.filter_pattern))) {
|
15462
15582
|
enl.push(k);
|
15463
15583
|
this.entities.push(MODEL.clusters[k]);
|
15584
|
+
addDistinct('C', this.filtered_types);
|
15464
15585
|
}
|
15465
15586
|
}
|
15466
15587
|
}
|
@@ -15474,6 +15595,7 @@ class Finder {
|
|
15474
15595
|
if(ds !== MODEL.equations_dataset) {
|
15475
15596
|
enl.push(k);
|
15476
15597
|
this.entities.push(MODEL.datasets[k]);
|
15598
|
+
addDistinct('D', this.filtered_types);
|
15477
15599
|
}
|
15478
15600
|
}
|
15479
15601
|
}
|
@@ -15487,6 +15609,7 @@ class Finder {
|
|
15487
15609
|
this.filter_pattern)) {
|
15488
15610
|
enl.push(k);
|
15489
15611
|
this.entities.push(MODEL.equations_dataset.modifiers[k]);
|
15612
|
+
addDistinct('E', this.filtered_types);
|
15490
15613
|
}
|
15491
15614
|
}
|
15492
15615
|
}
|
@@ -15503,6 +15626,7 @@ class Finder {
|
|
15503
15626
|
if(!bb && (!fp || patternMatch(ldn, this.filter_pattern))) {
|
15504
15627
|
enl.push(k);
|
15505
15628
|
this.entities.push(l);
|
15629
|
+
addDistinct('L', this.filtered_types);
|
15506
15630
|
}
|
15507
15631
|
}
|
15508
15632
|
}
|
@@ -15515,6 +15639,7 @@ class Finder {
|
|
15515
15639
|
MODEL.constraints[k].displayName, this.filter_pattern))) {
|
15516
15640
|
enl.push(k);
|
15517
15641
|
this.entities.push(MODEL.constraints[k]);
|
15642
|
+
addDistinct('B', this.filtered_types);
|
15518
15643
|
}
|
15519
15644
|
}
|
15520
15645
|
}
|
@@ -15540,10 +15665,24 @@ class Finder {
|
|
15540
15665
|
UI.scrollIntoView(document.getElementById(seid));
|
15541
15666
|
document.getElementById('finder-count').innerHTML = pluralS(
|
15542
15667
|
el.length, 'entity', 'entities');
|
15543
|
-
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) {
|
15544
15674
|
this.copy_btn.style.display = 'block';
|
15545
|
-
|
15546
|
-
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
|
+
}
|
15547
15686
|
}
|
15548
15687
|
this.updateRightPane();
|
15549
15688
|
}
|
@@ -15769,6 +15908,10 @@ class Finder {
|
|
15769
15908
|
UI.showProductPropertiesDialog(obj);
|
15770
15909
|
} else if(obj instanceof Link) {
|
15771
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);
|
15772
15915
|
} else if(obj instanceof Note) {
|
15773
15916
|
obj.showNotePropertiesDialog();
|
15774
15917
|
} else if(obj instanceof Dataset) {
|
@@ -15884,12 +16027,19 @@ class Finder {
|
|
15884
16027
|
}
|
15885
16028
|
}
|
15886
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
|
+
|
15887
16037
|
copyAttributesToClipboard(shift) {
|
15888
|
-
// Copy relevant entity attributes as tab-separated text to clipboard
|
15889
|
-
// NOTE:
|
15890
|
-
// that for each defined attribute (and if model has been
|
15891
|
-
// inferred attribute) has a property with its value.
|
15892
|
-
// 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
|
15893
16043
|
const ea_dict = {A: [], B: [], C: [], D: [], E: [], L: [], P: [], Q: []};
|
15894
16044
|
let e = this.selected_entity;
|
15895
16045
|
if(shift && e) {
|
@@ -15909,18 +16059,15 @@ class Finder {
|
|
15909
16059
|
etl = seq[i],
|
15910
16060
|
ead = ea_dict[etl];
|
15911
16061
|
if(ead && ead.length > 0) {
|
15912
|
-
// No blank line before first entity type
|
16062
|
+
// No blank line before first entity type.
|
15913
16063
|
if(text.length > 0) text.push('');
|
15914
|
-
|
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`);
|
15915
16067
|
if(!MODEL.infer_cost_prices) {
|
15916
|
-
//
|
15917
|
-
|
15918
|
-
|
15919
|
-
ah = ah.substring(0, p);
|
15920
|
-
} else {
|
15921
|
-
// SOC is exogenous, and hence comes before F in header => replace
|
15922
|
-
ah = ah.replace('\tShare of cost', '');
|
15923
|
-
}
|
16068
|
+
// If no cost price calculation, trim associated attributes
|
16069
|
+
// from the header.
|
16070
|
+
ah = ah.replace('\tCost price', '').replace('\tShare of cost', '');
|
15924
16071
|
}
|
15925
16072
|
text.push(ah);
|
15926
16073
|
attr.length = 0;
|
@@ -7339,6 +7339,7 @@ class Process extends Node {
|
|
7339
7339
|
a.LB = this.lower_bound.asAttribute;
|
7340
7340
|
a.UB = (this.equal_bounds ? a.LB : this.upper_bound.asAttribute);
|
7341
7341
|
a.IL = this.initial_level.asAttribute;
|
7342
|
+
a.LCF = this.pace_expression.asAttribute;
|
7342
7343
|
if(MODEL.solved) {
|
7343
7344
|
const t = MODEL.t;
|
7344
7345
|
a.L = this.level[t];
|
@@ -7600,6 +7601,8 @@ class Product extends Node {
|
|
7600
7601
|
if(MODEL.infer_cost_prices) {
|
7601
7602
|
a.CP = this.cost_price[t];
|
7602
7603
|
a.HCP = this.highest_cost_price[t];
|
7604
|
+
// Highest cost price may be undefined if product has no inflows.
|
7605
|
+
if(a.HCP === VM.MINUS_INFINITY) a.HCP = '';
|
7603
7606
|
}
|
7604
7607
|
}
|
7605
7608
|
return a;
|
@@ -186,9 +186,14 @@ function uniformDecimals(data) {
|
|
186
186
|
}
|
187
187
|
}
|
188
188
|
|
189
|
+
function capitalized(s) {
|
190
|
+
// Returns string `s` with its first letter capitalized.
|
191
|
+
return s.charAt(0).toUpperCase() + s.slice(1);
|
192
|
+
}
|
193
|
+
|
189
194
|
function ellipsedText(text, n=50, m=10) {
|
190
|
-
// Returns `text` with ellipsis " ... " between its first `n` and
|
191
|
-
// characters
|
195
|
+
// Returns `text` with ellipsis " ... " between its first `n` and
|
196
|
+
// last `m` characters.
|
192
197
|
if(text.length <= n + m + 3) return text;
|
193
198
|
return text.slice(0, n) + ' \u2026 ' + text.slice(text.length - m);
|
194
199
|
}
|
@@ -2070,6 +2070,16 @@ class VirtualMachine {
|
|
2070
2070
|
P: this.process_attr,
|
2071
2071
|
Q: this.product_attr
|
2072
2072
|
};
|
2073
|
+
this.entity_attribute_names = {};
|
2074
|
+
for(let i = 0; i < this.entity_letters.length; i++) {
|
2075
|
+
const
|
2076
|
+
el = this.entity_letters.charAt(i),
|
2077
|
+
ac = this.attribute_codes[el];
|
2078
|
+
this.entity_attribute_names[el] = [];
|
2079
|
+
for(let j = 0; j < ac.length; j++) {
|
2080
|
+
this.entity_attribute_names[el].push(ac[j]);
|
2081
|
+
}
|
2082
|
+
}
|
2073
2083
|
// Level-based attributes are computed only AFTER optimization
|
2074
2084
|
this.level_based_attr = ['L', 'CP', 'HCP', 'CF', 'CI', 'CO', 'F', 'A'];
|
2075
2085
|
this.object_types = ['Process', 'Product', 'Cluster', 'Link', 'Constraint',
|