linny-r 1.6.7 → 1.7.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 +7 -7
- package/console.js +104 -220
- package/package.json +1 -1
- package/server.js +39 -133
- package/static/index.html +64 -5
- package/static/linny-r.css +41 -0
- package/static/scripts/linny-r-ctrl.js +31 -29
- package/static/scripts/linny-r-gui-controller.js +168 -25
- package/static/scripts/linny-r-gui-monitor.js +21 -9
- package/static/scripts/linny-r-milp.js +363 -188
- package/static/scripts/linny-r-model.js +26 -12
- package/static/scripts/linny-r-vm.js +82 -45
@@ -443,12 +443,13 @@ class GUIController extends Controller {
|
|
443
443
|
// Keep track of time since last message displayed on the infoline.
|
444
444
|
this.time_last_message = new Date('01 Jan 2001 00:00:00 GMT');
|
445
445
|
this.message_display_time = 3000;
|
446
|
+
this.last_message_type = '';
|
446
447
|
|
447
448
|
// Initialize "main" modals, i.e., those that relate to the controller,
|
448
449
|
// not to other dialog objects.
|
449
450
|
const main_modals = ['logon', 'model', 'load', 'password', 'settings',
|
450
451
|
'actors', 'add-process', 'add-product', 'move', 'note', 'clone',
|
451
|
-
'replace', 'expression'];
|
452
|
+
'replace', 'expression', 'server', 'solver'];
|
452
453
|
for(let i = 0; i < main_modals.length; i++) {
|
453
454
|
this.modals[main_modals[i]] = new ModalDialog(main_modals[i]);
|
454
455
|
}
|
@@ -580,8 +581,10 @@ class GUIController extends Controller {
|
|
580
581
|
// NOTE: When user name is specified, solver is not on local host.
|
581
582
|
const hl = document.getElementById('host-logo');
|
582
583
|
hl.classList.add('local-server');
|
583
|
-
hl.addEventListener('click', () => UI.
|
584
|
+
hl.addEventListener('click', () => UI.showServerModal());
|
584
585
|
}
|
586
|
+
this.server_modal = new ModalDialog('server');
|
587
|
+
|
585
588
|
|
586
589
|
// Vertical tool bar buttons:
|
587
590
|
this.buttons.clone.addEventListener('click',
|
@@ -703,10 +706,27 @@ class GUIController extends Controller {
|
|
703
706
|
// Ensure that model documentation can no longer be edited.
|
704
707
|
DOCUMENTATION_MANAGER.clearEntity([MODEL]);
|
705
708
|
});
|
706
|
-
// 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.
|
707
712
|
this.modals.settings.element('scale-units-btn').addEventListener('click',
|
708
|
-
// Open the scale units modal dialog on top of the settings dialog.
|
709
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());
|
710
730
|
|
711
731
|
// Modals related to vertical toolbar buttons.
|
712
732
|
this.modals['add-process'].ok.addEventListener('click',
|
@@ -795,7 +815,7 @@ class GUIController extends Controller {
|
|
795
815
|
|
796
816
|
// The REPLACE dialog appears when a product is Ctrl-clicked.
|
797
817
|
this.modals.replace.ok.addEventListener('click',
|
798
|
-
() => UI.replaceProduct());
|
818
|
+
() => UI.replaceProduct());
|
799
819
|
this.modals.replace.cancel.addEventListener('click',
|
800
820
|
() => UI.modals.replace.hide());
|
801
821
|
|
@@ -870,21 +890,25 @@ class GUIController extends Controller {
|
|
870
890
|
}
|
871
891
|
|
872
892
|
loadModelFromXML(xml) {
|
873
|
-
//
|
893
|
+
// Parse `xml` and update the GUI.
|
874
894
|
const loaded = MODEL.parseXML(xml);
|
875
|
-
// 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.
|
876
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
|
+
}
|
877
901
|
this.updateScaleUnitList();
|
878
902
|
this.drawDiagram(MODEL);
|
879
|
-
// Cursor may have been set to `waiting` when decrypting
|
903
|
+
// Cursor may have been set to `waiting` when decrypting.
|
880
904
|
this.normalCursor();
|
881
905
|
this.setMessage('');
|
882
906
|
this.updateButtons();
|
883
907
|
// Undoable operations no longer apply!
|
884
908
|
UNDO_STACK.clear();
|
885
|
-
// Autosaving should start anew
|
909
|
+
// Autosaving should start anew.
|
886
910
|
AUTO_SAVE.setAutoSaveInterval();
|
887
|
-
// Signal success or failure
|
911
|
+
// Signal success or failure.
|
888
912
|
return loaded;
|
889
913
|
}
|
890
914
|
|
@@ -898,15 +922,15 @@ class GUIController extends Controller {
|
|
898
922
|
MODEL.clearSelection();
|
899
923
|
this.paper.drawModel(MODEL);
|
900
924
|
this.updateButtons();
|
901
|
-
// NOTE:
|
902
|
-
// cluster into view
|
925
|
+
// NOTE: When "moving up" in the cluster hierarchy, bring the former
|
926
|
+
// focal cluster into view.
|
903
927
|
if(fc.cluster == MODEL.focal_cluster) {
|
904
928
|
this.scrollIntoView(fc.shape.element.childNodes[0]);
|
905
929
|
}
|
906
930
|
}
|
907
931
|
|
908
932
|
drawDiagram(mdl) {
|
909
|
-
// "Queue" a draw request (to avoid redrawing too often)
|
933
|
+
// "Queue" a draw request (to avoid redrawing too often).
|
910
934
|
if(this.busy_drawing) {
|
911
935
|
this.draw_requests += 1;
|
912
936
|
} else {
|
@@ -952,19 +976,81 @@ class GUIController extends Controller {
|
|
952
976
|
if(a.links.indexOf(link) >= 0) this.paper.drawArrow(a);
|
953
977
|
}
|
954
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
|
+
}
|
955
1039
|
|
956
1040
|
shutDownServer() {
|
957
1041
|
// This terminates the local host server script and display a plain
|
958
1042
|
// HTML message in the browser with a restart button.
|
1043
|
+
this.modals.server.hide();
|
959
1044
|
if(!SOLVER.user_id) window.open('./shutdown', '_self');
|
960
1045
|
}
|
961
1046
|
|
962
1047
|
shutDownToUpdate() {
|
963
|
-
//
|
1048
|
+
// Signal server that an update is required. This will close the
|
964
1049
|
// local host Linny-R server. If this server was started by the
|
965
1050
|
// standard OS batch script, this script will proceed to update
|
966
1051
|
// Linny-R via npm and then restart the server again. If not, the
|
967
1052
|
// fetch request will time out, anf the user will be warned.
|
1053
|
+
this.modals.server.hide();
|
968
1054
|
if(SOLVER.user_id) return;
|
969
1055
|
fetch('update/')
|
970
1056
|
.then((response) => {
|
@@ -1022,6 +1108,8 @@ class GUIController extends Controller {
|
|
1022
1108
|
`Linny-R version ${m[1]} has been installed.`,
|
1023
1109
|
'To continue, you must reload this page, and',
|
1024
1110
|
'confirm when prompted by your browser.');
|
1111
|
+
// Hide "update" button in server dialog.
|
1112
|
+
UI.modals.server.element('update').style.display = 'none';
|
1025
1113
|
} else {
|
1026
1114
|
// Inform user that install appears to have failed.
|
1027
1115
|
msg.push(
|
@@ -1046,6 +1134,10 @@ class GUIController extends Controller {
|
|
1046
1134
|
preventUpdate() {
|
1047
1135
|
// Signal server that no update is required.
|
1048
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';
|
1049
1141
|
fetch('no-update/')
|
1050
1142
|
.then((response) => {
|
1051
1143
|
if(!response.ok) {
|
@@ -2572,28 +2664,32 @@ class GUIController extends Controller {
|
|
2572
2664
|
// Displays message on infoline unless no type (= plain text) and some
|
2573
2665
|
// info, warning or error message is already displayed
|
2574
2666
|
super.setMessage(msg, type);
|
2667
|
+
const types = ['notification', 'warning', 'error'];
|
2575
2668
|
let d = new Date(),
|
2576
2669
|
t = d.getTime(),
|
2577
|
-
dt = t - this.time_last_message
|
2670
|
+
dt = t - this.time_last_message,
|
2671
|
+
mti = types.indexOf(type),
|
2672
|
+
lmti = types.indexOf(this.last_message_type);
|
2578
2673
|
if(type) {
|
2579
|
-
//
|
2580
|
-
this.time_last_message = t;
|
2581
|
-
dt = this.message_display_time;
|
2582
|
-
SOUNDS[type].play().catch(() => {
|
2583
|
-
console.log('NOTICE: Sounds will only play after first user action');
|
2584
|
-
});
|
2674
|
+
// Only log "real" messages.
|
2585
2675
|
const
|
2586
2676
|
now = [d.getHours(), d.getMinutes().toString().padStart(2, '0'),
|
2587
2677
|
d.getSeconds().toString().padStart(2, '0')].join(':'),
|
2588
2678
|
im = {time: now, text: msg, status: type};
|
2589
2679
|
DOCUMENTATION_MANAGER.addMessage(im);
|
2590
|
-
// When receiver is active, add message to its log
|
2680
|
+
// When receiver is active, add message to its log.
|
2591
2681
|
if(RECEIVER.active) RECEIVER.log(`[${now}] ${msg}`);
|
2592
2682
|
}
|
2593
|
-
// Display text only if previous message has "timed out" or was
|
2594
|
-
|
2683
|
+
// Display text only if previous message has "timed out" or was less
|
2684
|
+
// urgent than this one.
|
2685
|
+
if(lmti < 0 || mti > lmti || dt >= this.message_display_time) {
|
2686
|
+
this.time_last_message = t;
|
2687
|
+
this.last_message_type = type;
|
2688
|
+
if(type) SOUNDS[type].play().catch(() => {
|
2689
|
+
console.log('NOTICE: Sounds will only play after first user action');
|
2690
|
+
});
|
2595
2691
|
const il = document.getElementById('info-line');
|
2596
|
-
il.classList.remove(
|
2692
|
+
il.classList.remove(...types);
|
2597
2693
|
il.classList.add(type);
|
2598
2694
|
il.innerHTML = msg;
|
2599
2695
|
}
|
@@ -3516,6 +3612,53 @@ console.log('HERE name conflicts', name_conflicts, mapping);
|
|
3516
3612
|
if(redraw) this.drawDiagram(model);
|
3517
3613
|
}
|
3518
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
|
+
|
3519
3662
|
// Note modal
|
3520
3663
|
|
3521
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
|