linny-r 1.6.8 → 1.7.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +7 -7
- package/console.js +250 -305
- package/package.json +1 -1
- package/server.js +40 -134
- package/static/index.html +64 -5
- package/static/linny-r.css +41 -0
- package/static/scripts/linny-r-ctrl.js +47 -42
- package/static/scripts/linny-r-gui-controller.js +153 -15
- package/static/scripts/linny-r-gui-monitor.js +21 -9
- package/static/scripts/linny-r-gui-paper.js +18 -18
- package/static/scripts/linny-r-gui-receiver.js +5 -5
- package/static/scripts/linny-r-milp.js +363 -188
- package/static/scripts/linny-r-model.js +43 -29
- package/static/scripts/linny-r-utils.js +20 -3
- package/static/scripts/linny-r-vm.js +162 -73
@@ -260,7 +260,7 @@ class Controller {
|
|
260
260
|
return VM.sig2Dig(n) + ' ' + 'kMGTP'.charAt(m) + 'B';
|
261
261
|
}
|
262
262
|
|
263
|
-
// Shapes are only used to draw model diagrams
|
263
|
+
// Shapes are only used to draw model diagrams.
|
264
264
|
|
265
265
|
createShape(mdl) {
|
266
266
|
if(this.paper) return new Shape(mdl);
|
@@ -275,7 +275,7 @@ class Controller {
|
|
275
275
|
if(shape) shape.removeFromDOM();
|
276
276
|
}
|
277
277
|
|
278
|
-
// Methods to ensure proper naming of entities
|
278
|
+
// Methods to ensure proper naming of entities.
|
279
279
|
|
280
280
|
cleanName(name) {
|
281
281
|
// Returns `name` without the object-attribute separator |, backslashes,
|
@@ -429,7 +429,7 @@ class Controller {
|
|
429
429
|
|
430
430
|
|
431
431
|
nameToID(name) {
|
432
|
-
//
|
432
|
+
// Return a name in lower case with link arrow replaced by three
|
433
433
|
// underscores, constraint link arrow by four underscores, and spaces
|
434
434
|
// converted to underscores; in this way, IDs will always be valid
|
435
435
|
// JavaScript object properties.
|
@@ -442,7 +442,7 @@ class Controller {
|
|
442
442
|
// Empty string signals failure.
|
443
443
|
return '';
|
444
444
|
}
|
445
|
-
// NOTE:
|
445
|
+
// NOTE: Replace single quotes by Unicode apostrophe so that they
|
446
446
|
// cannot interfere with JavaScript strings delimited by single quotes.
|
447
447
|
return name.toLowerCase().replace(/\s/g, '_').replace("'", '\u2019');
|
448
448
|
}
|
@@ -485,8 +485,8 @@ class Controller {
|
|
485
485
|
// Methods to notify modeler
|
486
486
|
|
487
487
|
setMessage(msg, type, cause=null) {
|
488
|
-
// Only log errors and warnings on the browser console
|
489
|
-
// NOTE:
|
488
|
+
// Only log errors and warnings on the browser console.
|
489
|
+
// NOTE: Optionally, the JavaScript error can be passed via `cause`.
|
490
490
|
if(type === 'error' || type === 'warning') {
|
491
491
|
// Add type unless message already starts with it
|
492
492
|
type = type.toUpperCase() + ':';
|
@@ -547,8 +547,8 @@ class Controller {
|
|
547
547
|
}
|
548
548
|
|
549
549
|
postResponseOK(text, notify=false) {
|
550
|
-
//
|
551
|
-
// the modeler if second argument is TRUE
|
550
|
+
// Check whether server reponse text is warning or error, and notify
|
551
|
+
// the modeler if second argument is TRUE.
|
552
552
|
let mtype = 'notification';
|
553
553
|
if(text.startsWith('ERROR:')) {
|
554
554
|
mtype = 'error';
|
@@ -563,20 +563,20 @@ class Controller {
|
|
563
563
|
}
|
564
564
|
|
565
565
|
loginPrompt() {
|
566
|
-
// The VM needs credentials - his should only occur for the GUI
|
566
|
+
// The VM needs credentials - his should only occur for the GUI.
|
567
567
|
console.log('WARNING: VM needs credentials, but GUI not active');
|
568
568
|
}
|
569
569
|
|
570
570
|
resetModel() {
|
571
|
-
//
|
571
|
+
// Reset the Virtual Machine (clears solution).
|
572
572
|
VM.reset();
|
573
|
-
// Redraw model in the browser (GUI only)
|
573
|
+
// Redraw model in the browser (GUI only).
|
574
574
|
MODEL.clearSelection();
|
575
575
|
this.drawDiagram(MODEL);
|
576
576
|
}
|
577
577
|
|
578
578
|
stopSolving() {
|
579
|
-
// Notify user only if VM was halted
|
579
|
+
// Notify user only if VM was halted.
|
580
580
|
if(VM.halted) {
|
581
581
|
this.notify('Solver HALTED');
|
582
582
|
} else {
|
@@ -585,8 +585,9 @@ class Controller {
|
|
585
585
|
}
|
586
586
|
|
587
587
|
// NOTE: The following UI functions are implemented as "dummy" methods
|
588
|
-
// because they are called by the Virtual Machine and/or by other
|
589
|
-
// while they can only be meaningfully performed by the
|
588
|
+
// because they are called by the Virtual Machine and/or by other
|
589
|
+
// controllers while they can only be meaningfully performed by the
|
590
|
+
// GUI controller.
|
590
591
|
addListeners() {}
|
591
592
|
readyToReset() {}
|
592
593
|
updateScaleUnitList() {}
|
@@ -616,27 +617,27 @@ class RepositoryBrowser {
|
|
616
617
|
this.repositories = [];
|
617
618
|
this.repository_index = -1;
|
618
619
|
this.module_index = -1;
|
619
|
-
// Get the repository list from the server
|
620
|
+
// Get the repository list from the server.
|
620
621
|
this.getRepositories();
|
621
622
|
this.reset();
|
622
623
|
}
|
623
624
|
|
624
625
|
reset() {
|
625
626
|
this.visible = false;
|
626
|
-
// NOTE:
|
627
|
-
// (1) they are properties of the local host, and hence model-independent
|
627
|
+
// NOTE: Do NOT reset repository list or module index, because:
|
628
|
+
// (1) they are properties of the local host, and hence model-independent;
|
628
629
|
// (2) they must be known when loading a module as model, whereas the
|
629
|
-
// loadingModel method hides and resets all stay-on-top dialogs
|
630
|
+
// loadingModel method hides and resets all stay-on-top dialogs.
|
630
631
|
}
|
631
632
|
|
632
633
|
get isLocalHost() {
|
633
|
-
//
|
634
|
+
// Return TRUE if first repository on the list is 'local host'.
|
634
635
|
return this.repositories.length > 0 &&
|
635
636
|
this.repositories[0].name === 'local host';
|
636
637
|
}
|
637
638
|
|
638
639
|
getRepositories() {
|
639
|
-
//
|
640
|
+
// Get the list of repository names from the server.
|
640
641
|
this.repositories.length = 0;
|
641
642
|
fetch('repo/', postData({action: 'list'}))
|
642
643
|
.then((response) => {
|
@@ -647,14 +648,14 @@ class RepositoryBrowser {
|
|
647
648
|
})
|
648
649
|
.then((data) => {
|
649
650
|
if(UI.postResponseOK(data)) {
|
650
|
-
// NOTE:
|
651
|
+
// NOTE: Trim to prevent empty name strings.
|
651
652
|
const rl = data.trim().split('\n');
|
652
653
|
for(let i = 0; i < rl.length; i++) {
|
653
654
|
this.addRepository(rl[i].trim());
|
654
655
|
}
|
655
656
|
}
|
656
|
-
// NOTE:
|
657
|
-
// unless the list is empty
|
657
|
+
// NOTE: Set index to first repository on list (typically the
|
658
|
+
// local host repository) unless the list is empty.
|
658
659
|
this.repository_index = Math.min(0, this.repositories.length - 1);
|
659
660
|
this.updateDialog();
|
660
661
|
})
|
@@ -662,7 +663,7 @@ class RepositoryBrowser {
|
|
662
663
|
}
|
663
664
|
|
664
665
|
repositoryByName(n) {
|
665
|
-
//
|
666
|
+
// Return the repository having name `n` if already known, or NULL.
|
666
667
|
for(let i = 0; i < this.repositories.length; i++) {
|
667
668
|
if(this.repositories[i].name === n) {
|
668
669
|
return this.repositories[i];
|
@@ -672,8 +673,8 @@ class RepositoryBrowser {
|
|
672
673
|
}
|
673
674
|
|
674
675
|
asFileName(s) {
|
675
|
-
//
|
676
|
-
// special characters converted to underscores
|
676
|
+
// Return string `s` with whitespace converted to a single dash, and
|
677
|
+
// special characters converted to underscores.
|
677
678
|
return s.normalize('NFKD').trim()
|
678
679
|
.replace(/[\s\-]+/g, '-')
|
679
680
|
.replace(/[^A-Za-z0-9_\-]/g, '_')
|
@@ -681,12 +682,13 @@ class RepositoryBrowser {
|
|
681
682
|
}
|
682
683
|
|
683
684
|
loadModuleAsModel() {
|
684
|
-
//
|
685
|
+
// Load selected module as model.
|
685
686
|
if(this.repository_index >= 0 && this.module_index >= 0) {
|
686
|
-
// NOTE:
|
687
|
+
// NOTE: When loading new model, the stay-on-top dialogs must be
|
688
|
+
// reset (GUI only; for console this is a "dummy" method).
|
687
689
|
UI.hideStayOnTopDialogs();
|
688
690
|
const r = this.repositories[this.repository_index];
|
689
|
-
// NOTE: pass FALSE to indicate "no inclusion; load XML as model"
|
691
|
+
// NOTE: pass FALSE to indicate "no inclusion; load XML as model".
|
690
692
|
r.loadModule(this.module_index, false);
|
691
693
|
}
|
692
694
|
}
|
@@ -1289,6 +1291,8 @@ class ExperimentManager {
|
|
1289
1291
|
// Only now compute the simulation run time (number of time steps)
|
1290
1292
|
xr.time_steps = MODEL.end_period - MODEL.start_period + 1;
|
1291
1293
|
VM.callback = this.callback;
|
1294
|
+
// NOTE: Asynchronous call. All follow-up actions must be performed
|
1295
|
+
// by the callback function.
|
1292
1296
|
VM.solveModel();
|
1293
1297
|
}
|
1294
1298
|
}
|
@@ -1315,11 +1319,11 @@ class ExperimentManager {
|
|
1315
1319
|
}
|
1316
1320
|
|
1317
1321
|
processRestOfRun() {
|
1318
|
-
//
|
1322
|
+
// Perform post-processing after run results have been added.
|
1319
1323
|
const x = MODEL.running_experiment;
|
1320
1324
|
if(!x) return;
|
1321
1325
|
const aci = x.active_combination_index;
|
1322
|
-
// Always add solver messages
|
1326
|
+
// Always add solver messages.
|
1323
1327
|
x.runs[aci].addMessages();
|
1324
1328
|
const n = x.combinations.length;
|
1325
1329
|
if(!VM.halted && aci < n - 1 && aci != x.single_run) {
|
@@ -1330,8 +1334,8 @@ class ExperimentManager {
|
|
1330
1334
|
} else {
|
1331
1335
|
x.active_combination_index++;
|
1332
1336
|
let delay = 5;
|
1333
|
-
// NOTE:
|
1334
|
-
// allow enough time for report writing
|
1337
|
+
// NOTE: When executing a remote command, wait for 1 second to
|
1338
|
+
// allow enough time for report writing.
|
1335
1339
|
if(RECEIVER.active && RECEIVER.experiment) {
|
1336
1340
|
UI.setMessage('Reporting run #' + (x.active_combination_index - 1));
|
1337
1341
|
delay = 1000;
|
@@ -1352,8 +1356,8 @@ class ExperimentManager {
|
|
1352
1356
|
`Experiment <em>${x.title}</em> terminated during run #${aci}`);
|
1353
1357
|
RECEIVER.deactivate();
|
1354
1358
|
}
|
1355
|
-
// No more runs => stop experiment, and perform call-back
|
1356
|
-
// NOTE:
|
1359
|
+
// No more runs => stop experiment, and perform call-back.
|
1360
|
+
// NOTE: If call-back is successful, the receiver will resume listening.
|
1357
1361
|
if(RECEIVER.active) {
|
1358
1362
|
RECEIVER.experiment = '';
|
1359
1363
|
RECEIVER.callBack();
|
@@ -1363,34 +1367,35 @@ class ExperimentManager {
|
|
1363
1367
|
MODEL.parseSettings(x.original_model_settings);
|
1364
1368
|
MODEL.round_sequence = x.original_round_sequence;
|
1365
1369
|
// Reset the Virtual Machine so t=0 at the status line,
|
1366
|
-
// and ALL expressions are reset as well
|
1370
|
+
// and ALL expressions are reset as well.
|
1367
1371
|
VM.reset();
|
1368
1372
|
this.readyButtons();
|
1369
1373
|
}
|
1370
1374
|
this.drawTable();
|
1371
|
-
// Reset the model, as results of last run will be showing still
|
1375
|
+
// Reset the model, as results of last run will be showing still.
|
1372
1376
|
UI.resetModel();
|
1373
1377
|
CHART_MANAGER.resetChartVectors();
|
1374
|
-
// NOTE:
|
1378
|
+
// NOTE: Clear chart only when done; charts do not update when an
|
1379
|
+
// experiment is running.
|
1375
1380
|
if(!MODEL.running_experiment) CHART_MANAGER.updateDialog();
|
1376
1381
|
}
|
1377
1382
|
|
1378
1383
|
stopExperiment() {
|
1379
|
-
// Interrupt solver but retain data on server (and no resume)
|
1384
|
+
// Interrupt solver but retain data on server (and no resume).
|
1380
1385
|
VM.halt();
|
1381
1386
|
}
|
1382
1387
|
|
1383
1388
|
showProgress(ci, p, n) {
|
1384
|
-
// Report progress on the console
|
1389
|
+
// Report progress on the console.
|
1385
1390
|
console.log('\nRun', ci, `(${p}% of ${n})`);
|
1386
1391
|
}
|
1387
1392
|
|
1388
1393
|
resumeButtons() {
|
1389
|
-
// Console experiments cannot be paused, and hence not resumed
|
1394
|
+
// Console experiments cannot be paused, and hence not resumed.
|
1390
1395
|
return false;
|
1391
1396
|
}
|
1392
1397
|
|
1393
|
-
// Dummy methods: actions that are meaningful only for the graphical UI
|
1398
|
+
// Dummy methods: actions that are meaningful only for the graphical UI.
|
1394
1399
|
drawTable() {}
|
1395
1400
|
readyButtons() {}
|
1396
1401
|
pausedButtons() {}
|
@@ -449,7 +449,7 @@ class GUIController extends Controller {
|
|
449
449
|
// not to other dialog objects.
|
450
450
|
const main_modals = ['logon', 'model', 'load', 'password', 'settings',
|
451
451
|
'actors', 'add-process', 'add-product', 'move', 'note', 'clone',
|
452
|
-
'replace', 'expression'];
|
452
|
+
'replace', 'expression', 'server', 'solver'];
|
453
453
|
for(let i = 0; i < main_modals.length; i++) {
|
454
454
|
this.modals[main_modals[i]] = new ModalDialog(main_modals[i]);
|
455
455
|
}
|
@@ -581,8 +581,10 @@ class GUIController extends Controller {
|
|
581
581
|
// NOTE: When user name is specified, solver is not on local host.
|
582
582
|
const hl = document.getElementById('host-logo');
|
583
583
|
hl.classList.add('local-server');
|
584
|
-
hl.addEventListener('click', () => UI.
|
584
|
+
hl.addEventListener('click', () => UI.showServerModal());
|
585
585
|
}
|
586
|
+
this.server_modal = new ModalDialog('server');
|
587
|
+
|
586
588
|
|
587
589
|
// Vertical tool bar buttons:
|
588
590
|
this.buttons.clone.addEventListener('click',
|
@@ -704,10 +706,27 @@ class GUIController extends Controller {
|
|
704
706
|
// Ensure that model documentation can no longer be edited.
|
705
707
|
DOCUMENTATION_MANAGER.clearEntity([MODEL]);
|
706
708
|
});
|
707
|
-
// Make the scale units
|
709
|
+
// Make the scale units and solver preferences buttons of the settings
|
710
|
+
// dialog responsive. Clicking will open these dialogs on top of the
|
711
|
+
// settings modal dialog.
|
708
712
|
this.modals.settings.element('scale-units-btn').addEventListener('click',
|
709
|
-
// Open the scale units modal dialog on top of the settings dialog.
|
710
713
|
() => SCALE_UNIT_MANAGER.show());
|
714
|
+
this.modals.settings.element('solver-prefs-btn').addEventListener('click',
|
715
|
+
() => UI.showSolverPreferencesDialog());
|
716
|
+
// Make solver modal elements responsive.
|
717
|
+
this.modals.solver.ok.addEventListener('click',
|
718
|
+
() => UI.updateSolverPreferences());
|
719
|
+
this.modals.solver.cancel.addEventListener('click',
|
720
|
+
() => UI.modals.solver.hide());
|
721
|
+
// Make server modal elements responsive.
|
722
|
+
this.modals.server.ok.addEventListener('click',
|
723
|
+
() => UI.changeSolver(UI.modals.server.element('solver').value));
|
724
|
+
this.modals.server.cancel.addEventListener('click',
|
725
|
+
() => UI.modals.server.hide());
|
726
|
+
this.modals.server.element('update').addEventListener('click',
|
727
|
+
() => UI.shutDownToUpdate());
|
728
|
+
this.modals.server.element('shut-down').addEventListener('click',
|
729
|
+
() => UI.shutDownServer());
|
711
730
|
|
712
731
|
// Modals related to vertical toolbar buttons.
|
713
732
|
this.modals['add-process'].ok.addEventListener('click',
|
@@ -796,7 +815,7 @@ class GUIController extends Controller {
|
|
796
815
|
|
797
816
|
// The REPLACE dialog appears when a product is Ctrl-clicked.
|
798
817
|
this.modals.replace.ok.addEventListener('click',
|
799
|
-
() => UI.replaceProduct());
|
818
|
+
() => UI.replaceProduct());
|
800
819
|
this.modals.replace.cancel.addEventListener('click',
|
801
820
|
() => UI.modals.replace.hide());
|
802
821
|
|
@@ -871,21 +890,25 @@ class GUIController extends Controller {
|
|
871
890
|
}
|
872
891
|
|
873
892
|
loadModelFromXML(xml) {
|
874
|
-
//
|
893
|
+
// Parse `xml` and update the GUI.
|
875
894
|
const loaded = MODEL.parseXML(xml);
|
876
|
-
// If not a valid Linny-R model, ensure that the current model is clean
|
895
|
+
// If not a valid Linny-R model, ensure that the current model is clean.
|
877
896
|
if(!loaded) MODEL = new LinnyRModel();
|
897
|
+
// If model specifies a preferred solver, immediately try to switch.
|
898
|
+
if(MODEL.preferred_solver !== VM.solver_name) {
|
899
|
+
UI.changeSolver(MODEL.preferred_solver);
|
900
|
+
}
|
878
901
|
this.updateScaleUnitList();
|
879
902
|
this.drawDiagram(MODEL);
|
880
|
-
// Cursor may have been set to `waiting` when decrypting
|
903
|
+
// Cursor may have been set to `waiting` when decrypting.
|
881
904
|
this.normalCursor();
|
882
905
|
this.setMessage('');
|
883
906
|
this.updateButtons();
|
884
907
|
// Undoable operations no longer apply!
|
885
908
|
UNDO_STACK.clear();
|
886
|
-
// Autosaving should start anew
|
909
|
+
// Autosaving should start anew.
|
887
910
|
AUTO_SAVE.setAutoSaveInterval();
|
888
|
-
// Signal success or failure
|
911
|
+
// Signal success or failure.
|
889
912
|
return loaded;
|
890
913
|
}
|
891
914
|
|
@@ -899,15 +922,15 @@ class GUIController extends Controller {
|
|
899
922
|
MODEL.clearSelection();
|
900
923
|
this.paper.drawModel(MODEL);
|
901
924
|
this.updateButtons();
|
902
|
-
// NOTE:
|
903
|
-
// cluster into view
|
925
|
+
// NOTE: When "moving up" in the cluster hierarchy, bring the former
|
926
|
+
// focal cluster into view.
|
904
927
|
if(fc.cluster == MODEL.focal_cluster) {
|
905
928
|
this.scrollIntoView(fc.shape.element.childNodes[0]);
|
906
929
|
}
|
907
930
|
}
|
908
931
|
|
909
932
|
drawDiagram(mdl) {
|
910
|
-
// "Queue" a draw request (to avoid redrawing too often)
|
933
|
+
// "Queue" a draw request (to avoid redrawing too often).
|
911
934
|
if(this.busy_drawing) {
|
912
935
|
this.draw_requests += 1;
|
913
936
|
} else {
|
@@ -953,19 +976,81 @@ class GUIController extends Controller {
|
|
953
976
|
if(a.links.indexOf(link) >= 0) this.paper.drawArrow(a);
|
954
977
|
}
|
955
978
|
}
|
979
|
+
|
980
|
+
showServerModal() {
|
981
|
+
// Prepare and show the server modal dialog.
|
982
|
+
const
|
983
|
+
md = this.modals.server,
|
984
|
+
host = md.element('host-div'),
|
985
|
+
sd = md.element('solver-div'),
|
986
|
+
nsd = md.element('no-solver-div'),
|
987
|
+
html = [];
|
988
|
+
host.innerText = 'Server on ' + VM.server;
|
989
|
+
if(VM.server === 'local host') {
|
990
|
+
host.title = 'Linny-R directory is ' + VM.working_directory;
|
991
|
+
}
|
992
|
+
for(let i = 0; i < VM.solver_list.length; i++) {
|
993
|
+
const s = VM.solver_list[i];
|
994
|
+
html.push(['<option value="', s,
|
995
|
+
(s === VM.solver_name ? '"selected="selected' : ''),
|
996
|
+
'">', VM.solver_names[s], '</option>'].join(''));
|
997
|
+
}
|
998
|
+
md.element('solver').innerHTML = html.join('');
|
999
|
+
if(html.length) {
|
1000
|
+
sd.style.display = 'block';
|
1001
|
+
nsd.style.display = 'none';
|
1002
|
+
} else {
|
1003
|
+
sd.style.display = 'none';
|
1004
|
+
nsd.style.display = 'block';
|
1005
|
+
}
|
1006
|
+
md.show();
|
1007
|
+
}
|
1008
|
+
|
1009
|
+
changeSolver(sid) {
|
1010
|
+
// Change preferred solver to `sid` if specified.
|
1011
|
+
if(!sid) return;
|
1012
|
+
const
|
1013
|
+
md = this.modals.server,
|
1014
|
+
mps = MODEL.preferred_solver;
|
1015
|
+
md.hide();
|
1016
|
+
if(mps && mps !== sid) {
|
1017
|
+
UI.warn('Model setttings designate ' + VM.solver_names[mps] +
|
1018
|
+
' as preferred solver');
|
1019
|
+
return;
|
1020
|
+
}
|
1021
|
+
const pd = postData({action: 'change', solver: sid});
|
1022
|
+
fetch('solver/', pd)
|
1023
|
+
.then((response) => {
|
1024
|
+
if(!response.ok) {
|
1025
|
+
UI.alert(`ERROR ${response.status}: ${response.statusText}`);
|
1026
|
+
}
|
1027
|
+
return response.text();
|
1028
|
+
})
|
1029
|
+
.then((data) => {
|
1030
|
+
if(UI.postResponseOK(data, true)) {
|
1031
|
+
VM.solver_name = sid;
|
1032
|
+
UI.modals.server.hide();
|
1033
|
+
}
|
1034
|
+
})
|
1035
|
+
.catch((err) => {
|
1036
|
+
UI.warn(UI.WARNING.NO_CONNECTION, err);
|
1037
|
+
});
|
1038
|
+
}
|
956
1039
|
|
957
1040
|
shutDownServer() {
|
958
1041
|
// This terminates the local host server script and display a plain
|
959
1042
|
// HTML message in the browser with a restart button.
|
1043
|
+
this.modals.server.hide();
|
960
1044
|
if(!SOLVER.user_id) window.open('./shutdown', '_self');
|
961
1045
|
}
|
962
1046
|
|
963
1047
|
shutDownToUpdate() {
|
964
|
-
//
|
1048
|
+
// Signal server that an update is required. This will close the
|
965
1049
|
// local host Linny-R server. If this server was started by the
|
966
1050
|
// standard OS batch script, this script will proceed to update
|
967
1051
|
// Linny-R via npm and then restart the server again. If not, the
|
968
1052
|
// fetch request will time out, anf the user will be warned.
|
1053
|
+
this.modals.server.hide();
|
969
1054
|
if(SOLVER.user_id) return;
|
970
1055
|
fetch('update/')
|
971
1056
|
.then((response) => {
|
@@ -1023,6 +1108,8 @@ class GUIController extends Controller {
|
|
1023
1108
|
`Linny-R version ${m[1]} has been installed.`,
|
1024
1109
|
'To continue, you must reload this page, and',
|
1025
1110
|
'confirm when prompted by your browser.');
|
1111
|
+
// Hide "update" button in server dialog.
|
1112
|
+
UI.modals.server.element('update').style.display = 'none';
|
1026
1113
|
} else {
|
1027
1114
|
// Inform user that install appears to have failed.
|
1028
1115
|
msg.push(
|
@@ -1047,6 +1134,10 @@ class GUIController extends Controller {
|
|
1047
1134
|
preventUpdate() {
|
1048
1135
|
// Signal server that no update is required.
|
1049
1136
|
if(SOLVER.user_id) return;
|
1137
|
+
// Show "update" button in server dialog to permit updating later.
|
1138
|
+
const btn = this.modals.server.element('update');
|
1139
|
+
btn.innerText = 'Update Linny-R to version ' + this.newer_version;
|
1140
|
+
btn.style.display = 'block';
|
1050
1141
|
fetch('no-update/')
|
1051
1142
|
.then((response) => {
|
1052
1143
|
if(!response.ok) {
|
@@ -2591,7 +2682,7 @@ class GUIController extends Controller {
|
|
2591
2682
|
}
|
2592
2683
|
// Display text only if previous message has "timed out" or was less
|
2593
2684
|
// urgent than this one.
|
2594
|
-
if(mti > lmti || dt >= this.message_display_time) {
|
2685
|
+
if(lmti < 0 || mti > lmti || dt >= this.message_display_time) {
|
2595
2686
|
this.time_last_message = t;
|
2596
2687
|
this.last_message_type = type;
|
2597
2688
|
if(type) SOUNDS[type].play().catch(() => {
|
@@ -3521,6 +3612,53 @@ console.log('HERE name conflicts', name_conflicts, mapping);
|
|
3521
3612
|
if(redraw) this.drawDiagram(model);
|
3522
3613
|
}
|
3523
3614
|
|
3615
|
+
// Solver preferences modal
|
3616
|
+
|
3617
|
+
showSolverPreferencesDialog() {
|
3618
|
+
const
|
3619
|
+
md = this.modals.solver,
|
3620
|
+
html = ['<option value="">(default)</option>'];
|
3621
|
+
for(let i = 0; i < VM.solver_list.length; i++) {
|
3622
|
+
const s = VM.solver_list[i];
|
3623
|
+
html.push(['<option value="', s,
|
3624
|
+
(s === MODEL.preferred_solver ? '"selected="selected' : ''),
|
3625
|
+
'">', VM.solver_names[s], '</option>'].join(''));
|
3626
|
+
}
|
3627
|
+
md.element('preference').innerHTML = html.join('');
|
3628
|
+
md.element('int-feasibility').value = MODEL.integer_tolerance;
|
3629
|
+
md.element('mip-gap').value = MODEL.MIP_gap;
|
3630
|
+
md.show();
|
3631
|
+
}
|
3632
|
+
|
3633
|
+
updateSolverPreferences() {
|
3634
|
+
// Set values for solver preferences.
|
3635
|
+
const
|
3636
|
+
md = this.modals.solver,
|
3637
|
+
it = md.element('int-feasibility'),
|
3638
|
+
mg = md.element('mip-gap');
|
3639
|
+
let itol = 5e-7,
|
3640
|
+
mgap = 1e-4;
|
3641
|
+
// Validate input, assuming default values for empty fields.
|
3642
|
+
if(it.value.trim()) itol = UI.validNumericInput('solver-int-feasibility',
|
3643
|
+
'integer feasibility tolerance');
|
3644
|
+
if(itol === false) return false;
|
3645
|
+
if(mg.value.trim()) mgap = UI.validNumericInput('solver-mip-gap',
|
3646
|
+
'relative MIP gap');
|
3647
|
+
if(mgap === false) return false;
|
3648
|
+
// Modify solver preferences for the current model.
|
3649
|
+
const ps = md.element('preference').value;
|
3650
|
+
if(ps !== MODEL.preferred_solver) {
|
3651
|
+
MODEL.preferred_solver = ps;
|
3652
|
+
// Immediately try to change to the preferred solver, as this is
|
3653
|
+
// an asynchronous call that may take time to complete.
|
3654
|
+
UI.changeSolver(ps);
|
3655
|
+
}
|
3656
|
+
MODEL.integer_tolerance = Math.max(1e-9, Math.min(0.1, itol));
|
3657
|
+
MODEL.MIP_gap = Math.max(0, Math.min(0.5, mgap));
|
3658
|
+
// Close the dialog.
|
3659
|
+
md.hide();
|
3660
|
+
}
|
3661
|
+
|
3524
3662
|
// Note modal
|
3525
3663
|
|
3526
3664
|
showNotePropertiesDialog(n=null) {
|
@@ -326,13 +326,13 @@ class GUIMonitor {
|
|
326
326
|
} else if(jsr.server) {
|
327
327
|
VM.solver_token = jsr.token;
|
328
328
|
VM.solver_name = jsr.solver;
|
329
|
-
// Remote solver may indicate user-specific solver time limit
|
329
|
+
// Remote solver may indicate user-specific solver time limit.
|
330
330
|
let utl = '';
|
331
331
|
if(jsr.time_limit) {
|
332
332
|
VM.max_solver_time = jsr.time_limit;
|
333
333
|
utl = ` -- ${VM.solver_name} solver: ` +
|
334
334
|
`max. ${VM.max_solver_time} seconds per block`;
|
335
|
-
// If user has a set time limit, no restrictions on tableau size
|
335
|
+
// If user has a set time limit, no restrictions on tableau size.
|
336
336
|
VM.max_tableau_size = 0;
|
337
337
|
}
|
338
338
|
UI.notify('Logged on to ' + jsr.server + utl);
|
@@ -346,12 +346,15 @@ class GUIMonitor {
|
|
346
346
|
}
|
347
347
|
|
348
348
|
connectToServer() {
|
349
|
-
// Prompts for credentials if not connected yet
|
350
|
-
// NOTE:
|
351
|
-
// is left blank
|
349
|
+
// Prompts for credentials if not connected yet.
|
350
|
+
// NOTE: No authentication prompt if SOLVER.user_id in `linny-r-config.js`
|
351
|
+
// is left blank.
|
352
352
|
if(!VM.solver_user) {
|
353
|
+
VM.connected = false;
|
353
354
|
VM.solver_token = 'local host';
|
354
|
-
fetch('solver/', postData({
|
355
|
+
fetch('solver/', postData({
|
356
|
+
action: 'logon',
|
357
|
+
solver: MODEL.preferred_solver || VM.solver_name}))
|
355
358
|
.then((response) => {
|
356
359
|
if(!response.ok) {
|
357
360
|
UI.alert(`ERROR ${response.status}: ${response.statusText}`);
|
@@ -362,10 +365,15 @@ class GUIMonitor {
|
|
362
365
|
try {
|
363
366
|
const
|
364
367
|
jsr = JSON.parse(data),
|
365
|
-
|
366
|
-
|
368
|
+
sname = VM.solver_names[jsr.solver] || 'unknown',
|
369
|
+
svr = `Solver on ${jsr.server} is ${sname}`;
|
370
|
+
if(jsr.solver !== VM.solver_name) UI.notify(svr);
|
371
|
+
VM.server = jsr.server;
|
372
|
+
VM.working_directory = jsr.path;
|
367
373
|
VM.solver_name = jsr.solver;
|
374
|
+
VM.solver_list = jsr.solver_list;
|
368
375
|
document.getElementById('host-logo').title = svr;
|
376
|
+
VM.connected = true;
|
369
377
|
} catch(err) {
|
370
378
|
console.log(err, data);
|
371
379
|
UI.alert('ERROR: Unexpected data from server: ' +
|
@@ -381,6 +389,7 @@ class GUIMonitor {
|
|
381
389
|
}
|
382
390
|
|
383
391
|
submitBlockToSolver() {
|
392
|
+
// Post MILP model plus relevant metadata to the server.
|
384
393
|
let top = MODEL.timeout_period;
|
385
394
|
if(VM.max_solver_time && top > VM.max_solver_time) {
|
386
395
|
top = VM.max_solver_time;
|
@@ -398,7 +407,10 @@ UI.logHeapSize(`BEFORE creating post data`);
|
|
398
407
|
round: VM.round_sequence[VM.current_round],
|
399
408
|
columns: VM.columnsInBlock,
|
400
409
|
data: VM.lines,
|
401
|
-
|
410
|
+
solver: MODEL.preferred_solver,
|
411
|
+
timeout: top,
|
412
|
+
inttol: MODEL.integer_tolerance,
|
413
|
+
mipgap: MODEL.MIP_gap
|
402
414
|
});
|
403
415
|
UI.logHeapSize(`AFTER creating post data`);
|
404
416
|
// Immediately free the memory taken up by VM.lines
|