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
package/server.js
CHANGED
@@ -137,13 +137,13 @@ const MILPSolver = require('./static/scripts/linny-r-milp.js');
|
|
137
137
|
// - workspace=[path] will create workspace in [path] instead of (main)/user
|
138
138
|
const SETTINGS = commandLineSettings();
|
139
139
|
|
140
|
-
// The workspace defines the paths to directories where Linny-R can write files
|
140
|
+
// The workspace defines the paths to directories where Linny-R can write files.
|
141
141
|
const WORKSPACE = createWorkspace();
|
142
142
|
|
143
|
-
// Initialize the solver
|
144
|
-
const SOLVER = new MILPSolver(SETTINGS, WORKSPACE);
|
143
|
+
// Initialize the solver.
|
144
|
+
const SOLVER = new MILPSolver(SETTINGS.preferred_solver, WORKSPACE);
|
145
145
|
|
146
|
-
// Create launch script
|
146
|
+
// Create launch script.
|
147
147
|
createLaunchScript();
|
148
148
|
|
149
149
|
// Create the HTTP server.
|
@@ -1262,11 +1262,24 @@ function processRequest(req, res, cmd, data) {
|
|
1262
1262
|
// NOTE: On remote servers, solver actions require authentication.
|
1263
1263
|
if(action === 'logon') {
|
1264
1264
|
// No authentication -- simply return the passed token, "local host"
|
1265
|
-
// as server name,
|
1266
|
-
|
1267
|
-
|
1265
|
+
// as server name, the name of the solver, the list of installed
|
1266
|
+
// solvers, and the Linny-R directory.
|
1267
|
+
serveJSON(res, {
|
1268
|
+
token: 'local host',
|
1269
|
+
server: 'local host',
|
1270
|
+
solver: SOLVER.id,
|
1271
|
+
solver_list: Object.keys(SOLVER.solver_list),
|
1272
|
+
path: WORKING_DIRECTORY
|
1273
|
+
});
|
1268
1274
|
} else if(action === 'png') {
|
1269
1275
|
convertSVGtoPNG(req, res, sp);
|
1276
|
+
} else if(action === 'change') {
|
1277
|
+
const sid = sp.get('solver');
|
1278
|
+
if(SOLVER.changeDefault(sid)) {
|
1279
|
+
servePlainText(res, 'Default solver set to ' + SOLVER.name);
|
1280
|
+
} else {
|
1281
|
+
servePlainText(res, 'WARNING: Failed to change solver to ' + sid);
|
1282
|
+
}
|
1270
1283
|
} else if(action === 'solve') {
|
1271
1284
|
serveJSON(res, SOLVER.solveBlock(sp));
|
1272
1285
|
} else {
|
@@ -1282,7 +1295,6 @@ function processRequest(req, res, cmd, data) {
|
|
1282
1295
|
serveHTML(res, SHUTDOWN_MESSAGE);
|
1283
1296
|
SERVER.close();
|
1284
1297
|
} else if(cmd === 'version') {
|
1285
|
-
logAction('HERE version = ' + VERSION_INFO.current);
|
1286
1298
|
servePlainText(res, 'Current version is ' + VERSION_INFO.current);
|
1287
1299
|
} else if(cmd === 'update') {
|
1288
1300
|
// Shut down this server silently. When the server was started from
|
@@ -1529,8 +1541,6 @@ function commandLineSettings() {
|
|
1529
1541
|
launch: false,
|
1530
1542
|
port: 5050,
|
1531
1543
|
preferred_solver: '',
|
1532
|
-
solver: '',
|
1533
|
-
solver_path: '',
|
1534
1544
|
user_dir: path.join(WORKING_DIRECTORY, 'user')
|
1535
1545
|
};
|
1536
1546
|
const
|
@@ -1600,142 +1610,36 @@ Possible options are:
|
|
1600
1610
|
}
|
1601
1611
|
}
|
1602
1612
|
}
|
1603
|
-
// Check whether
|
1613
|
+
// Check whether Inkscape has been installed.
|
1604
1614
|
const path_list = process.env.PATH.split(path.delimiter);
|
1605
|
-
let gurobi_path = '',
|
1606
|
-
scip_path = '',
|
1607
|
-
cplex_path = '',
|
1608
|
-
match,
|
1609
|
-
max_v = -1;
|
1610
1615
|
for(let i = 0; i < path_list.length; i++) {
|
1611
|
-
|
1612
|
-
if(match && parseInt(match[1]) > max_v) {
|
1613
|
-
gurobi_path = path_list[i];
|
1614
|
-
max_v = parseInt(match[1]);
|
1615
|
-
}
|
1616
|
-
match = path_list[i].match(/[\/\\]cplex[\/\\]bin/i);
|
1617
|
-
if(match) {
|
1618
|
-
cplex_path = path_list[i];
|
1619
|
-
} else {
|
1620
|
-
// NOTE: CPLEX may create its own environment variable for its paths
|
1621
|
-
match = path_list[i].match(/%(.*cplex.*)%/i);
|
1622
|
-
if(match) {
|
1623
|
-
const cpl = process.env[match[1]].split(path.delimiter);
|
1624
|
-
for(let i = 0; i < cpl.length; i++) {
|
1625
|
-
match = cpl[i].match(/[\/\\]cplex[\/\\]bin/i);
|
1626
|
-
if(match) {
|
1627
|
-
cplex_path = cpl[i];
|
1628
|
-
break;
|
1629
|
-
}
|
1630
|
-
}
|
1631
|
-
}
|
1632
|
-
}
|
1633
|
-
match = path_list[i].match(/[\/\\]scip[^\/\\]+[\/\\]bin/i);
|
1634
|
-
if(match) scip_path = path_list[i];
|
1616
|
+
// Check whether it is an Inkscape path.
|
1635
1617
|
match = path_list[i].match(/inkscape/i);
|
1618
|
+
// If so, use it (so the *last* matching path will be used).
|
1636
1619
|
if(match) settings.inkscape = path_list[i];
|
1637
1620
|
}
|
1638
|
-
|
1639
|
-
console.log('Looking for Gurobi in /usr/local/bin');
|
1640
|
-
try {
|
1641
|
-
// On macOS and Unix, Gurobi is in the user's local binaries
|
1642
|
-
const gp = '/usr/local/bin';
|
1643
|
-
fs.accessSync(gp + '/gurobi_cl');
|
1644
|
-
gurobi_path = gp;
|
1645
|
-
} catch(err) {
|
1646
|
-
// No real error, so no action needed
|
1647
|
-
}
|
1648
|
-
}
|
1649
|
-
if(gurobi_path) {
|
1650
|
-
console.log('Path to Gurobi:', gurobi_path);
|
1651
|
-
// Check if command line version is executable
|
1652
|
-
const sp = path.join(gurobi_path,
|
1653
|
-
'gurobi_cl' + (PLATFORM.startsWith('win') ? '.exe' : ''));
|
1654
|
-
try {
|
1655
|
-
fs.accessSync(sp, fs.constants.X_OK);
|
1656
|
-
if(settings.solver !== 'gurobi')
|
1657
|
-
settings.solver = 'gurobi';
|
1658
|
-
settings.solver_path = sp;
|
1659
|
-
} catch(err) {
|
1660
|
-
console.log(err.message);
|
1661
|
-
console.log(
|
1662
|
-
'WARNING: Failed to access the Gurobi command line application');
|
1663
|
-
}
|
1664
|
-
}
|
1665
|
-
// Check if cplex(.exe) exists in its directory
|
1666
|
-
let sp = path.join(cplex_path, 'cplex' + (PLATFORM.startsWith('win') ? '.exe' : ''));
|
1667
|
-
const need_cplex = !settings.solver || settings.preferred_solver === 'cplex';
|
1668
|
-
try {
|
1669
|
-
fs.accessSync(sp, fs.constants.X_OK);
|
1670
|
-
console.log('Path to CPLEX:', sp);
|
1671
|
-
if(need_cplex) {
|
1672
|
-
settings.solver = 'cplex';
|
1673
|
-
settings.solver_path = sp;
|
1674
|
-
}
|
1675
|
-
} catch(err) {
|
1676
|
-
// Only report error if CPLEX is needed
|
1677
|
-
if(need_cplex) {
|
1678
|
-
console.log(err.message);
|
1679
|
-
console.log('WARNING: CPLEX application not found in', sp);
|
1680
|
-
}
|
1681
|
-
}
|
1682
|
-
// Check if scip(.exe) exists in its directory
|
1683
|
-
sp = path.join(scip_path, 'scip' + (PLATFORM.startsWith('win') ? '.exe' : ''));
|
1684
|
-
const need_scip = !settings.solver || settings.preferred_solver === 'scip';
|
1685
|
-
try {
|
1686
|
-
fs.accessSync(sp, fs.constants.X_OK);
|
1687
|
-
console.log('Path to SCIP:', sp);
|
1688
|
-
if(need_scip) {
|
1689
|
-
settings.solver = 'scip';
|
1690
|
-
settings.solver_path = sp;
|
1691
|
-
}
|
1692
|
-
} catch(err) {
|
1693
|
-
// Only report error if SCIP is needed
|
1694
|
-
if(need_scip) {
|
1695
|
-
console.log(err.message);
|
1696
|
-
console.log('WARNING: SCIP application not found in', sp);
|
1697
|
-
}
|
1698
|
-
}
|
1699
|
-
// Check if lp_solve(.exe) exists in working directory
|
1700
|
-
sp = path.join(WORKING_DIRECTORY,
|
1701
|
-
'lp_solve' + (PLATFORM.startsWith('win') ? '.exe' : ''));
|
1702
|
-
const need_lps = !settings.solver || settings.preferred_solver === 'lp_solve';
|
1703
|
-
try {
|
1704
|
-
fs.accessSync(sp, fs.constants.X_OK);
|
1705
|
-
console.log('Path to LP_solve:', sp);
|
1706
|
-
if(need_lps) {
|
1707
|
-
settings.solver = 'lp_solve';
|
1708
|
-
settings.solver_path = sp;
|
1709
|
-
}
|
1710
|
-
} catch(err) {
|
1711
|
-
// Only report error if LP_solve is needed
|
1712
|
-
if(need_lps) {
|
1713
|
-
console.log(err.message);
|
1714
|
-
console.log('WARNING: LP_solve application not found in', sp);
|
1715
|
-
}
|
1716
|
-
}
|
1717
|
-
// On macOS, Inkscape is not added to the PATH environment variable
|
1621
|
+
// NOTE: On macOS, Inkscape is not added to the PATH environment variable.
|
1718
1622
|
if(!settings.inkscape && PLATFORM === 'darwin') {
|
1719
1623
|
console.log('Looking for Inkscape in Applications...');
|
1720
1624
|
try {
|
1721
|
-
// Look in the default directory
|
1625
|
+
// Look in the default directory.
|
1722
1626
|
const ip = '/Applications/Inkscape.app/Contents/MacOS';
|
1723
1627
|
fs.accessSync(ip);
|
1724
1628
|
settings.inkscape = ip;
|
1725
1629
|
} catch(err) {
|
1726
|
-
// No real error, so no action needed
|
1630
|
+
// No real error, so no action needed.
|
1727
1631
|
}
|
1728
1632
|
}
|
1729
|
-
// Verify that Inkscape is installed
|
1633
|
+
// Verify that Inkscape is installed.
|
1730
1634
|
if(settings.inkscape) {
|
1731
|
-
// NOTE: On Windows, the command line version is a .com file
|
1635
|
+
// NOTE: On Windows, the command line version is a .com file.
|
1732
1636
|
let ip = path.join(settings.inkscape,
|
1733
1637
|
'inkscape' + (PLATFORM.startsWith('win') ? '.com' : ''));
|
1734
1638
|
try {
|
1735
1639
|
fs.accessSync(ip, fs.constants.X_OK);
|
1736
1640
|
} catch(err) {
|
1737
1641
|
// NOTE: As of Inkscape version 1.3, the command line executable
|
1738
|
-
// is called inkscapecom(.com)
|
1642
|
+
// is called inkscapecom(.com).
|
1739
1643
|
ip = path.join(settings.inkscape,
|
1740
1644
|
'inkscapecom' + (PLATFORM.startsWith('win') ? '.com' : ''));
|
1741
1645
|
}
|
@@ -1759,14 +1663,14 @@ Possible options are:
|
|
1759
1663
|
}
|
1760
1664
|
|
1761
1665
|
function createWorkspace() {
|
1762
|
-
//
|
1763
|
-
// paths to sub-directories, and
|
1666
|
+
// Verify that Linny-R has write access to the user workspace, define
|
1667
|
+
// paths to sub-directories, and create them if necessary.
|
1764
1668
|
try {
|
1765
|
-
// See whether the user directory already exists
|
1669
|
+
// See whether the user directory already exists.
|
1766
1670
|
try {
|
1767
1671
|
fs.accessSync(SETTINGS.user_dir, fs.constants.R_OK | fs.constants.W_O);
|
1768
1672
|
} catch(err) {
|
1769
|
-
// If not, try to create it
|
1673
|
+
// If not, try to create it.
|
1770
1674
|
fs.mkdirSync(SETTINGS.user_dir);
|
1771
1675
|
console.log('Created user directory:', SETTINGS.user_dir);
|
1772
1676
|
}
|
@@ -1776,7 +1680,7 @@ function createWorkspace() {
|
|
1776
1680
|
SETTINGS.user_dir);
|
1777
1681
|
process.exit();
|
1778
1682
|
}
|
1779
|
-
// Define the sub-directory paths
|
1683
|
+
// Define the sub-directory paths.
|
1780
1684
|
const ws = {
|
1781
1685
|
autosave: path.join(SETTINGS.user_dir, 'autosave'),
|
1782
1686
|
channel: path.join(SETTINGS.user_dir, 'channel'),
|
@@ -1787,7 +1691,7 @@ function createWorkspace() {
|
|
1787
1691
|
reports: path.join(SETTINGS.user_dir, 'reports'),
|
1788
1692
|
solver_output: path.join(SETTINGS.user_dir, 'solver')
|
1789
1693
|
};
|
1790
|
-
// Create these sub-directories if not aready there
|
1694
|
+
// Create these sub-directories if not aready there.
|
1791
1695
|
try {
|
1792
1696
|
for(let p in ws) if(ws.hasOwnProperty(p)) {
|
1793
1697
|
try {
|
@@ -1801,9 +1705,11 @@ function createWorkspace() {
|
|
1801
1705
|
console.log(err.message);
|
1802
1706
|
console.log('WARNING: No access to workspace directory');
|
1803
1707
|
}
|
1804
|
-
// The file containing name, URL and access token for remote repositories
|
1708
|
+
// The file containing name, URL and access token for remote repositories.
|
1805
1709
|
ws.repositories = path.join(SETTINGS.user_dir, 'repositories.cfg');
|
1806
|
-
//
|
1710
|
+
// For completeness, add path to Linny-R directory.
|
1711
|
+
ws.working_directory = WORKING_DIRECTORY;
|
1712
|
+
// Return the updated workspace object.
|
1807
1713
|
return ws;
|
1808
1714
|
}
|
1809
1715
|
|
package/static/index.html
CHANGED
@@ -162,16 +162,17 @@ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
|
162
162
|
if(info.length > 1) {
|
163
163
|
LINNY_R_VERSION = info[0];
|
164
164
|
const v = 'Version ' + LINNY_R_VERSION;
|
165
|
-
// Update the "home page" of the documentation manager
|
165
|
+
// Update the "home page" of the documentation manager.
|
166
166
|
DOCUMENTATION_MANAGER.about_linny_r =
|
167
167
|
DOCUMENTATION_MANAGER.about_linny_r.replace(
|
168
168
|
'[LINNY_R_VERSION]', v);
|
169
|
-
// Update the version number in the browser's upper left corner
|
169
|
+
// Update the version number in the browser's upper left corner.
|
170
170
|
document.getElementById('linny-r-version-number').innerHTML = v;
|
171
|
-
// NOTE:
|
172
|
-
// not reached; if so, do not suggest a new version exists
|
171
|
+
// NOTE: Server detects "version 0" when npmjs website was
|
172
|
+
// not reached; if so, do not suggest a new version exists.
|
173
173
|
if(info[1] !== 'up-to-date' && info[1] !== '0') {
|
174
|
-
// Inform user that newer version exists
|
174
|
+
// Inform user that newer version exists.
|
175
|
+
UI.newer_version = info[1];
|
175
176
|
UI.check_update_modal.element('msg').innerHTML = [
|
176
177
|
'<a href="', GITHUB_REPOSITORY,
|
177
178
|
'/releases/tag/v', info[1], '" ',
|
@@ -423,6 +424,28 @@ and move the cursor over the status bar">
|
|
423
424
|
</div>
|
424
425
|
</div>
|
425
426
|
|
427
|
+
<!-- the SERVER dialog permits solver selection, update and shut-down -->
|
428
|
+
<div id="server-modal" class="modal">
|
429
|
+
<div id="server-dlg" class="inp-dlg">
|
430
|
+
<div class="dlg-title">
|
431
|
+
Server and solver
|
432
|
+
<img class="cancel-btn" src="images/cancel.png">
|
433
|
+
<img class="ok-btn" src="images/ok.png">
|
434
|
+
</div>
|
435
|
+
<div id="server-host-div" title="Linny-R directory">Server on local host</div>
|
436
|
+
<div id="server-no-solver-div">No solver detected</div>
|
437
|
+
<div id="server-solver-div" hidden="hidden">
|
438
|
+
<div id="server-sel">
|
439
|
+
<label>Default solver:</label>
|
440
|
+
<select id="server-solver">
|
441
|
+
</select>
|
442
|
+
</div>
|
443
|
+
</div>
|
444
|
+
<button id="server-update" hidden="hidden">Update Linny-R to new version</button>
|
445
|
+
<button id="server-shut-down">Shut down server</button>
|
446
|
+
</div>
|
447
|
+
</div>
|
448
|
+
|
426
449
|
<!-- the MODEL dialog prompts for (optional) model name and author name -->
|
427
450
|
<div id="model-modal" class="modal">
|
428
451
|
<div id="model-dlg" class="inp-dlg">
|
@@ -605,6 +628,8 @@ and move the cursor over the status bar">
|
|
605
628
|
<td colspan="2">Solver time limit:
|
606
629
|
<input id="settings-time-limit" type="text" autocomplete="off"
|
607
630
|
style="width:35px;text-align:center"> seconds per block
|
631
|
+
<img id="settings-solver-prefs-btn" class="sbtn" src="images/settings.png"
|
632
|
+
title="Set solver preferences">
|
608
633
|
</td>
|
609
634
|
</tr>
|
610
635
|
<tr>
|
@@ -688,6 +713,40 @@ NOTE: Unit symbols are case-sensitive, so BTU ≠ Btu">
|
|
688
713
|
</div>
|
689
714
|
</div>
|
690
715
|
|
716
|
+
<!-- the SOLVER dialog permits specifying solver preference and tolerances -->
|
717
|
+
<div id="solver-modal" class="modal">
|
718
|
+
<div id="solver-dlg" class="inp-dlg">
|
719
|
+
<div class="dlg-title">
|
720
|
+
Solver preferences
|
721
|
+
<img class="cancel-btn" src="images/cancel.png">
|
722
|
+
<img class="ok-btn" src="images/ok.png">
|
723
|
+
</div>
|
724
|
+
<table style="width: 100%">
|
725
|
+
<tr title="This solver will be used if it is installed">
|
726
|
+
<td>
|
727
|
+
<label>Preferred solver:</label>
|
728
|
+
<select id="solver-preference">
|
729
|
+
</select>
|
730
|
+
</td>
|
731
|
+
</tr>
|
732
|
+
<tr title="Tolerance may range from 1e-9 to 0.1">
|
733
|
+
<td>
|
734
|
+
<label>Integer feasibility tolerance:</label>
|
735
|
+
<input id="solver-int-feasibility" style="width: 55px"
|
736
|
+
placeholder="5e-7" type="text" autocomplete="off">
|
737
|
+
</td>
|
738
|
+
</tr>
|
739
|
+
<tr title="Relative gap may range from 0 to 0.5">
|
740
|
+
<td>
|
741
|
+
<label>Relative MIP gap:</label>
|
742
|
+
<input id="solver-mip-gap" style="width: 55px"
|
743
|
+
placeholder="1e-4" type="text" autocomplete="off">
|
744
|
+
</td>
|
745
|
+
</tr>
|
746
|
+
</table>
|
747
|
+
</div>
|
748
|
+
</div>
|
749
|
+
|
691
750
|
<!-- the REPOSITORY BROWSER dialog allows interaction with repositories -->
|
692
751
|
<div id="repository-dlg" class="inp-dlg">
|
693
752
|
<div id="repository-hdr" class="dragger dlg-title">Repository browser
|
package/static/linny-r.css
CHANGED
@@ -944,6 +944,7 @@ img.inline-cancel-btn {
|
|
944
944
|
height: min-content;
|
945
945
|
}
|
946
946
|
|
947
|
+
#settings-solver-prefs-btn,
|
947
948
|
#settings-scale-units-btn {
|
948
949
|
margin-left: 0px;
|
949
950
|
margin-right: 7px;
|
@@ -952,6 +953,7 @@ img.inline-cancel-btn {
|
|
952
953
|
filter: brightness(85%);
|
953
954
|
}
|
954
955
|
|
956
|
+
#settings-solver-prefs-btn:hover,
|
955
957
|
#settings-scale-units-btn:hover {
|
956
958
|
filter: brightness(200%);
|
957
959
|
}
|
@@ -994,6 +996,16 @@ input.pws-5 {
|
|
994
996
|
background-color: #c0ffc0;
|
995
997
|
}
|
996
998
|
|
999
|
+
/* styles for the SOLVER PREFERENCES dialog */
|
1000
|
+
#solver-modal {
|
1001
|
+
z-index: 110; /* on top of the SETTINGS dialog */
|
1002
|
+
}
|
1003
|
+
|
1004
|
+
#solver-dlg {
|
1005
|
+
width: 212px;
|
1006
|
+
height: min-content;
|
1007
|
+
}
|
1008
|
+
|
997
1009
|
/* styles for the ACTOR DIALOG
|
998
1010
|
NOTE: the z-index is very high for this dialog, as it should appear
|
999
1011
|
on top of the ACTOR LIST DIALOG
|
@@ -1190,6 +1202,35 @@ td.export {
|
|
1190
1202
|
color: #0000b0;
|
1191
1203
|
}
|
1192
1204
|
|
1205
|
+
/* styles for the SERVER dialog */
|
1206
|
+
#server-dlg {
|
1207
|
+
width: 200px;
|
1208
|
+
height: min-content;
|
1209
|
+
}
|
1210
|
+
#server-host-div,
|
1211
|
+
#server-solver-div {
|
1212
|
+
margin: 3px;
|
1213
|
+
}
|
1214
|
+
|
1215
|
+
#server-no-solver-div {
|
1216
|
+
margin: 3px;
|
1217
|
+
padding-top: 2px;
|
1218
|
+
padding-bottom: 2px;
|
1219
|
+
font-style: italic;
|
1220
|
+
margin-bottom: 2px;
|
1221
|
+
}
|
1222
|
+
|
1223
|
+
#server-solver {
|
1224
|
+
font-size: 12px;
|
1225
|
+
margin-bottom: 3px;
|
1226
|
+
}
|
1227
|
+
|
1228
|
+
#server-update,
|
1229
|
+
#server-shut-down {
|
1230
|
+
margin: 3px;
|
1231
|
+
font-size: 12px;
|
1232
|
+
}
|
1233
|
+
|
1193
1234
|
/* styles for the MODEL dialog */
|
1194
1235
|
#model-dlg {
|
1195
1236
|
width: 270px;
|
@@ -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
|
}
|