linny-r 1.1.9 → 1.1.11
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 +63 -73
- package/package.json +1 -1
- package/server.js +187 -27
- package/static/index.html +12 -9
- package/static/linny-r.css +8 -2
- package/static/scripts/linny-r-ctrl.js +7 -4
- package/static/scripts/linny-r-gui.js +125 -165
- package/static/scripts/linny-r-utils.js +1 -1
- package/static/scripts/linny-r-vm.js +14 -4
package/README.md
CHANGED
@@ -10,22 +10,19 @@ The graphical language and WYSIWYG model editor are developed by **Pieter Bots**
|
|
10
10
|
|
11
11
|
Originally implemented in Delphi Pascal, Linny-R is now developed in HTML+CSS+JavaScript
|
12
12
|
so as to be platform-independent and 100% transparent open source (under the MIT license).
|
13
|
-
The software comprises a server that runs on Node.js
|
13
|
+
The software comprises a server that runs on **Node.js**,
|
14
|
+
and a graphical user interface (GUI) that runs in any modern browser.
|
14
15
|
|
15
|
-
|
16
|
-
<a href="https://
|
17
|
-
|
18
|
-
If you install Linny-R on your own machine, no such restrictions apply.
|
16
|
+
User documentation for Linny-R is still scant, but it is growing. You can contribute yourself (in "wiki fashion")
|
17
|
+
via the official user documentation site <a href="https://linny-r.info" target="_blank">https://linny-r.info</a>.
|
18
|
+
Technical documentation will be developed on GitHub: https://github.com/pwgbots/linny-r/wiki
|
19
19
|
|
20
|
-
|
21
|
-
via the official documentation site <a href="https://linny-r.info" target="_blank">https://linny-r.info</a>.
|
22
|
-
|
23
|
-
### Installing Node.js
|
20
|
+
## Installing Node.js
|
24
21
|
|
25
22
|
Linny-R is developed as a JavaScript package, and requires that **Node.js** is installed on your computer.
|
26
23
|
This software can be downloaded from <a href="https://nodejs.org" target="_blank">https://nodejs.org</a>.
|
27
24
|
Make sure that you choose the correct installer for your computer.
|
28
|
-
Linny-R is developed using the _current_ release. Presently (October 2022) this is 18.
|
25
|
+
Linny-R is developed using the _current_ release. Presently (October 2022) this is 18.11.0.
|
29
26
|
|
30
27
|
Run the installer and accept the default settings.
|
31
28
|
There is **no** need to install the optional _Tools for Native Modules_.
|
@@ -36,9 +33,9 @@ Verify the installation by typing:
|
|
36
33
|
|
37
34
|
``node --version``
|
38
35
|
|
39
|
-
The response should be the version number of Node.js, for example: v18.
|
36
|
+
The response should be the version number of Node.js, for example: v18.11.0.
|
40
37
|
|
41
|
-
|
38
|
+
## Installing Linny-R
|
42
39
|
It is advisable to install Linny-R in a directory on your computer, not in a cloud.
|
43
40
|
In this installation guide, the path to this directory is denoted by `WORKING_DIRECTORY`,
|
44
41
|
so in all commands you should replace this with the actual directory path.
|
@@ -57,6 +54,8 @@ and then type at the command line prompt:
|
|
57
54
|
|
58
55
|
``npm install --prefix . linny-r``
|
59
56
|
|
57
|
+
**NOTE:** The spacing around the dot is important.
|
58
|
+
|
60
59
|
After installation has completed, `WORKING_DIRECTORY` should have this directory tree structure:
|
61
60
|
|
62
61
|
<pre>
|
@@ -105,7 +104,7 @@ It should also contain the style sheet `linny-r.css` required by the GUI.
|
|
105
104
|
The sub-directories of `static` contain files that are served to the browser by the script
|
106
105
|
`server.js` when it is running in Node.js.
|
107
106
|
|
108
|
-
|
107
|
+
## Configuring the MILP solver
|
109
108
|
|
110
109
|
Linny-R presently supports two MILP solvers: Gurobi and LP_solve.
|
111
110
|
Gurobi is _considerably_ more powerful than the open source LP_solve solver that has powered Linny-R since 2009,
|
@@ -158,18 +157,17 @@ Then return to Terminal and once more type `./lp_solve -h`.
|
|
158
157
|
The response should then be a listing of all the command line options of LP_solve.
|
159
158
|
If you reach this stage, Linny-R will be able to run LP_solve.
|
160
159
|
|
161
|
-
|
162
|
-
### Running Linny-R
|
160
|
+
## Running Linny-R
|
163
161
|
|
164
162
|
Open the Command Line Interface (CLI) of your computer, change to your `WORKING_DIRECTORY` and type:
|
165
163
|
|
166
|
-
``linny-r``
|
164
|
+
``node node_modules/linny-r/server launch``
|
167
165
|
|
168
166
|
This response should be something similar to:
|
169
167
|
|
170
168
|
<pre>
|
171
|
-
Node.js server for Linny-R version 1.1.
|
172
|
-
Node.js version: v18.
|
169
|
+
Node.js server for Linny-R version 1.1.11
|
170
|
+
Node.js version: v18.11.0
|
173
171
|
... etc.
|
174
172
|
</pre>
|
175
173
|
|
@@ -179,9 +177,9 @@ The Linny-R GUI should show in your browser window,
|
|
179
177
|
while in the CLI you should see a long series of server log messages like:
|
180
178
|
|
181
179
|
<pre>
|
182
|
-
Static file: /index.html
|
183
|
-
Static file: /scripts/iro.min.js
|
184
|
-
Static file: /images/open.png
|
180
|
+
[2022-10-17 14:55:37] Static file: /index.html
|
181
|
+
[2022-10-17 14:55:37] Static file: /scripts/iro.min.js
|
182
|
+
[2022-10-17 14:55:37] Static file: /images/open.png
|
185
183
|
... etc.
|
186
184
|
</pre>
|
187
185
|
|
@@ -207,7 +205,45 @@ Meanwhile, in the CLI, you should see a server log message like:
|
|
207
205
|
Solve block 1 a
|
208
206
|
</pre>
|
209
207
|
|
210
|
-
|
208
|
+
To end a modeling session, you can shut down the server by clickicng on the local host icon
|
209
|
+
in the upper right corner of the Linny-R GUI in your browser, confirm that you want to leave,
|
210
|
+
and then close your browser (tab). If you do not shut down the server from the browser,
|
211
|
+
you can also stop the server by repeatedly pressing ``Ctrl+C`` in the CLI box.
|
212
|
+
|
213
|
+
## Command line options
|
214
|
+
|
215
|
+
Optionally, you can add more arguments to the `node` command:
|
216
|
+
|
217
|
+
<pre>
|
218
|
+
dpi=[number] to overrule the default resolution (300 dpi) for Inkscape
|
219
|
+
launch to automatically launch Linny-R in your default browser
|
220
|
+
port=[number] to overrule the default port number (5050)
|
221
|
+
solver=[name] to overrule the default sequence (Gurobi, LP_solve)
|
222
|
+
workspace=[path] to overrule the default path for the user directory
|
223
|
+
</pre>
|
224
|
+
|
225
|
+
## Click-start for Linny-R
|
226
|
+
|
227
|
+
To facilitate start-up, you can create a shortcut icon for Linny-R on your desktop.
|
228
|
+
|
229
|
+
On a Windows machine, open the _File Explorer_, select your Linny-R folder,
|
230
|
+
right-click on the batch file `linny-r.bat`, and select the _Create shortcut_ option.
|
231
|
+
Then right-click on the shortcut file to edit its properties, and click the _Change Icon_ button.
|
232
|
+
The dialog that then appears will allow you to go to the sub-folder `node_modules\linny-r\static\images`,
|
233
|
+
where you should select the file `linny-r.ico`.
|
234
|
+
Finally, rename the shortcut to `Linny-R` and move or copy it to your desktop.
|
235
|
+
|
236
|
+
On a macOS machine, open _Terminal_ and change to your Linny-R directory, and then type:
|
237
|
+
|
238
|
+
``chmod +x linny-r.command``
|
239
|
+
|
240
|
+
to make the script file executable.
|
241
|
+
To set the icon, open the folder that contains the file `linny-r.command`,
|
242
|
+
click on its icon (which still is plain) and open the _Info dialog_ by pressing ``Cmd+I``.
|
243
|
+
Then open your Linny-R folder in _Finder_, change to the sub-folder `node_modules/linny-r/static/images`,
|
244
|
+
and from there drag/drop the file `linny-r.icns` on the icon shown in the top left corner of the _Info dialog_.
|
245
|
+
|
246
|
+
## User workspace
|
211
247
|
|
212
248
|
The user workspace is created when the server is run for the first time.
|
213
249
|
The sub-directories of this directory `user` are used by Linny-R to store files.
|
@@ -225,19 +261,7 @@ You can overrule this by specifying the path to another directory when you start
|
|
225
261
|
Note that doing this will create a new, empty workspace (the directories listed above)
|
226
262
|
in the specified path. It will **not** affect or duplicate information from existing workspaces.
|
227
263
|
|
228
|
-
|
229
|
-
|
230
|
-
Optionally, you can add more arguments to the `node` command:
|
231
|
-
|
232
|
-
<pre>
|
233
|
-
dpi=[number] to overrule the default resolution (300 dpi) for Inkscape
|
234
|
-
launch to automatically launch Linny-R in your default browser
|
235
|
-
port=[number] to overrule the default port number (5050)
|
236
|
-
solver=[name] to overrule the default sequence (Gurobi, LP_solve)
|
237
|
-
workspace=[path] to overrule the default path for the user directory
|
238
|
-
</pre>
|
239
|
-
|
240
|
-
### Installing Inkscape
|
264
|
+
## Installing Inkscape
|
241
265
|
|
242
266
|
Linny-R creates its diagrams and charts as SVG images.
|
243
267
|
When you download a diagram, it will be saved as a .svg file.
|
@@ -261,41 +285,7 @@ On a macOS computer, Linny-R will look for Inkscape in /Applications/Inkscape.ap
|
|
261
285
|
**NOTE:** The current installation wizard for Inkscape (version 1.2) does **not** add the application to the PATH variable,
|
262
286
|
so you need to do this yourself.
|
263
287
|
|
264
|
-
|
265
|
-
|
266
|
-
To facilitate start-up, you can create a shortcut icon on your desktop.
|
267
|
-
|
268
|
-
On a Windows machine, change to your Linny-R folder, right-click on the batch file `linny-r.bat`,
|
269
|
-
and select the _Create shortcut_ option.
|
270
|
-
Then right-click on the shortcut file to edit its properties, and click the _Change Icon_ button.
|
271
|
-
The dialog that then appears will allow you to go to the sub-folder `node_modules\linny-r\static\images`,
|
272
|
-
where you should select the file `linny-r.ico`.
|
273
|
-
Finally, rename the shortcut to `Linny-R` and move or copy it to your desktop.
|
274
|
-
|
275
|
-
On a macOS machine, open Terminal and change to your Linny-R directory, and then type:
|
276
|
-
|
277
|
-
``chmod +x linny-r.command``
|
278
|
-
|
279
|
-
to make the script file executable.
|
280
|
-
To set the icon, click on the icon of `linny-r.command` (which still is plain) and open the Info dialog by pressing ``Cmd+I``.
|
281
|
-
Then open your Linny-R folder in the Finder, change to the sub-folder `node_modules/linny-r/static/images`,
|
282
|
-
and from there drag/drop the file `linny-r.icns` on the icon shown in the top left corner of the Info dialog.
|
283
|
-
|
284
|
-
|
285
|
-
### Normal use after installation
|
286
|
-
|
287
|
-
If you have not configured a "click-start" icon as described above,
|
288
|
-
you must start a modeling session with Linny-R by opening a CLI box,
|
289
|
-
then change to the Linny-R directory and type `linny-r`.
|
290
|
-
|
291
|
-
To shut down the server, click on the local host icon in the upper right corner of the Linny-R GUI in your browser.
|
292
|
-
Alternatively, you can stop the server by repeatedly pressing ``Ctrl+C`` in the CLI box.
|
293
|
-
|
294
|
-
Pressing ``Ctrl+C`` in the Terminal window on a macOS machine may not stop the process.
|
295
|
-
In that case, you can stop Node.js by stopping the Terminal.
|
296
|
-
|
297
|
-
|
298
|
-
### Using Linny-R console
|
288
|
+
## Using Linny-R console
|
299
289
|
|
300
290
|
The console-only version of Linny-R allows you to run a Linny-R model without a web browser.
|
301
291
|
This may be useful when you want run models from a script (shell script, Python, ...).
|
@@ -307,17 +297,17 @@ you will see the command line options that allow you to run models in various wa
|
|
307
297
|
|
308
298
|
**NOTE: The console-only version is still in development, and does not provide all functions yet.**
|
309
299
|
|
310
|
-
|
300
|
+
## Troubleshooting problems
|
311
301
|
|
312
302
|
If during any of the steps above you encounter problems, please try to diagnose them and resolve them yourself.
|
313
|
-
You can find a lot of useful information on the Linny-R
|
303
|
+
You can find a lot of useful information on the Linny-R user documentation website:
|
314
304
|
<a href="https://linny-r.info" target="_blank">https://linny-r.info</a>.
|
315
305
|
|
316
306
|
To diagnose a problem, always look in the CLI box where Node.js is running,
|
317
307
|
as informative server-side error messages will appear there.
|
318
308
|
|
319
309
|
Then also look at the console window of your browser.
|
320
|
-
Most browsers offer a
|
310
|
+
Most browsers offer a _Web Developer Tools_ option via their application menu.
|
321
311
|
This will allow you to view the browser console, which will display JavaScript errors in red font.
|
322
312
|
|
323
313
|
If you've tried hard, but failed, you can try to contact Pieter Bots at ``p.w.g.bots@tudelft.nl``
|
package/package.json
CHANGED
package/server.js
CHANGED
@@ -162,12 +162,29 @@ if(SETTINGS.launch) {
|
|
162
162
|
const cmd = (PLATFORM.startsWith('win') ? 'start' : 'open');
|
163
163
|
child_process.exec(cmd + ' http://127.0.0.1:' + SETTINGS.port,
|
164
164
|
(error, stdout, stderr) => {
|
165
|
-
|
166
|
-
|
167
|
-
|
165
|
+
if(error) {
|
166
|
+
console.log('NOTICE: Failed to launch GUI in browser');
|
167
|
+
console.log(error);
|
168
|
+
console.log(stdout);
|
169
|
+
console.log(stderr);
|
170
|
+
}
|
168
171
|
});
|
169
172
|
}
|
170
173
|
|
174
|
+
// Server action logging functionality
|
175
|
+
// ===================================
|
176
|
+
// Only actions are logged to the console as with date and time;
|
177
|
+
// error messages are not prefixed, so these are logged directly.
|
178
|
+
|
179
|
+
function logAction(msg) {
|
180
|
+
// Log request processing to console with time-zone-aware date and time
|
181
|
+
const
|
182
|
+
t = new Date(),
|
183
|
+
tzt = new Date(t.getTime() - t.getTimezoneOffset()*60000),
|
184
|
+
dts = tzt.toISOString().substring(0, 19).replace('T', ' ');
|
185
|
+
console.log(`[${dts}] ${msg}`);
|
186
|
+
}
|
187
|
+
|
171
188
|
// Version check functionality
|
172
189
|
// ===========================
|
173
190
|
// This section of code implements server responses to the request made
|
@@ -186,7 +203,21 @@ function autoCheck(res) {
|
|
186
203
|
}
|
187
204
|
|
188
205
|
// HTML page to show then the server is shut down by the user
|
189
|
-
|
206
|
+
// NOTE: on a macOS machine, this is slightly more work
|
207
|
+
const
|
208
|
+
OS_TEXT = (PLATFORM === 'darwin' ? [
|
209
|
+
`<p>You can close the <em>Terminal</em> window that shows
|
210
|
+
<tt>[Process Terminated]</tt> at the bottom.
|
211
|
+
</p>`,
|
212
|
+
`open <em>Terminal</em> again, change to your Linny-R directory by typing:
|
213
|
+
</p>
|
214
|
+
<p><code>cd ${WORKING_DIRECTORY}</code></p>
|
215
|
+
<p>`
|
216
|
+
] : [
|
217
|
+
'',
|
218
|
+
'switch to your <em>Command Prompt</em> window '
|
219
|
+
]),
|
220
|
+
SHUTDOWN_MESSAGE = `<!DOCTYPE html>
|
190
221
|
<html lang="en-US">
|
191
222
|
<head>
|
192
223
|
<meta http-equiv="content-type" content="text/html; charset=UTF-8">
|
@@ -206,15 +237,14 @@ const SHUTDOWN_MESSAGE = `<!DOCTYPE html>
|
|
206
237
|
</style>
|
207
238
|
</head>
|
208
239
|
<body>
|
209
|
-
<h3>Linny-R server (127.0.0.1) is shutting down</h3
|
210
|
-
<p>To restart Linny-R,
|
211
|
-
and there at the prompt` +
|
240
|
+
<h3>Linny-R server (127.0.0.1) is shutting down</h3>` + OS_TEXT[0] + `
|
241
|
+
<p>To restart Linny-R, ` + OS_TEXT[1] + ` and then at the prompt` +
|
212
242
|
(VERSION_INFO.up_to_date ? '' : `
|
213
243
|
first type:</p>
|
214
244
|
<p><code>npm update linny-r</code><p>
|
215
245
|
to upgrade to Linny-R version ${VERSION_INFO.latest}, and then`) +
|
216
246
|
` type:</p>
|
217
|
-
<p><code>node node_modules
|
247
|
+
<p><code>node node_modules${path.sep}linny-r${path.sep}server</code></p>
|
218
248
|
<p>
|
219
249
|
Then switch back to this window, and click this
|
220
250
|
<button type="button"
|
@@ -225,10 +255,145 @@ const SHUTDOWN_MESSAGE = `<!DOCTYPE html>
|
|
225
255
|
</body>
|
226
256
|
</html>`;
|
227
257
|
|
258
|
+
// Auto-save & restore model functionality
|
259
|
+
// =======================================
|
260
|
+
// For auto-save services, the Linny-R JavaScript application communicates with
|
261
|
+
// the server via calls to the server like fetch('autosave/', x) where x is a JSON
|
262
|
+
// object with at least the entry `action`, which can be one of the following:
|
263
|
+
// purge remove all model files older than the set auto-save period
|
264
|
+
// store write the property x.xml to the file with name x.name
|
265
|
+
// load return the XML contents of the specified model file
|
266
|
+
// Each action returns a JSON string that represents the actualized auto-save
|
267
|
+
// settings (interval and perdiod) and list of auto-saved model data objects.
|
268
|
+
// For each model: {name, file_name, size, time_saved}
|
269
|
+
|
270
|
+
function asFileName(s) {
|
271
|
+
// Returns string `s` in lower case with whitespace converted to a single
|
272
|
+
// dash, special characters converted to underscores, and leading and
|
273
|
+
// trailing dashes and underscores removed
|
274
|
+
return s.normalize('NFKD').trim()
|
275
|
+
.replace(/[\s\-]+/g, '-')
|
276
|
+
.replace(/[^A-Za-z0-9_\-]/g, '_')
|
277
|
+
.replace(/^[\-\_]+|[\-\_]+$/g, '');
|
278
|
+
}
|
279
|
+
|
280
|
+
function autoSave(res, sp) {
|
281
|
+
// Processes all auto-save & restore commands
|
282
|
+
const action = sp.get('action').trim();
|
283
|
+
logAction('Auto-save action: ' + action);
|
284
|
+
if(['purge', 'load', 'store'].indexOf(action) < 0) {
|
285
|
+
// Invalid action => report error
|
286
|
+
return servePlainText(res, `ERROR: Invalid auto-save action: "${action}"`);
|
287
|
+
}
|
288
|
+
// Always purge the auto-save files before further action; this returns
|
289
|
+
// the list with model data objects
|
290
|
+
const data = autoSavePurge(res, sp);
|
291
|
+
// NOTE: if string instead of array, this string is an error message
|
292
|
+
if(typeof data === 'string') return servePlainText(res, data);
|
293
|
+
// Perform load or store actions if requested
|
294
|
+
if(action === 'load') return autoSaveLoad(res, sp);
|
295
|
+
if(action === 'store') return autoSaveStore(res, sp);
|
296
|
+
// Otherwise, action was 'purge' => return the auto-saved model list
|
297
|
+
serveJSON(res, data);
|
298
|
+
}
|
299
|
+
|
300
|
+
function autoSavePurge(res, sp) {
|
301
|
+
// Deletes specified file(s) (if any) as well as all expired files,
|
302
|
+
// and returns list with data on remaining files as JSON string
|
303
|
+
const
|
304
|
+
now = new Date(),
|
305
|
+
p = sp.get('period'),
|
306
|
+
period = (p ? parseInt(p) : 24) * 3600000,
|
307
|
+
df = sp.get('to_delete'),
|
308
|
+
all = df === '/*ALL*/';
|
309
|
+
|
310
|
+
// Get list of data on Linny-R models in `autosave` directory
|
311
|
+
data = [];
|
312
|
+
try {
|
313
|
+
const flist = fs.readdirSync(WORKSPACE.autosave);
|
314
|
+
for(let i = 0; i < flist.length; i++) {
|
315
|
+
const
|
316
|
+
pp = path.parse(flist[i]),
|
317
|
+
md = {name: pp.name},
|
318
|
+
fp = path.join(WORKSPACE.autosave, flist[i]);
|
319
|
+
// NOTE: only consider Linny-R model files (extension .lnr)
|
320
|
+
if(pp.ext === '.lnr') {
|
321
|
+
let dodel = all || pp.name === df;
|
322
|
+
if(!dodel) {
|
323
|
+
// Get file properties
|
324
|
+
const fstat = fs.statSync(fp);
|
325
|
+
md.size = fstat.size;
|
326
|
+
md.date = fstat.mtime;
|
327
|
+
// Also delete if file has expired
|
328
|
+
dodel = now - fstat.mtimeMs > period;
|
329
|
+
}
|
330
|
+
if(dodel) {
|
331
|
+
// Delete model file
|
332
|
+
try {
|
333
|
+
fs.unlinkSync(fp);
|
334
|
+
} catch(err) {
|
335
|
+
console.log('WARNING: Failed to delete', fp);
|
336
|
+
console.log(err);
|
337
|
+
}
|
338
|
+
} else {
|
339
|
+
// Add model data to the list
|
340
|
+
data.push(md);
|
341
|
+
}
|
342
|
+
}
|
343
|
+
}
|
344
|
+
} catch(err) {
|
345
|
+
console.log(err);
|
346
|
+
return 'ERROR: Auto-save failed -- ' + err.message;
|
347
|
+
}
|
348
|
+
return data;
|
349
|
+
}
|
350
|
+
|
351
|
+
function autoSaveLoad(res, sp) {
|
352
|
+
// Return XML content of specified file
|
353
|
+
const fn = sp.get('file');
|
354
|
+
if(fn) {
|
355
|
+
const fp = path.join(WORKSPACE.autosave, fn + '.lnr');
|
356
|
+
try {
|
357
|
+
data = fs.readFileSync(fp, 'utf8');
|
358
|
+
} catch(err) {
|
359
|
+
console.log(err);
|
360
|
+
data = 'WARNING: Failed to load auto-saved file: ' + err.message;
|
361
|
+
}
|
362
|
+
} else {
|
363
|
+
data = 'ERROR: No auto-saved file name';
|
364
|
+
}
|
365
|
+
servePlainText(res, data);
|
366
|
+
}
|
367
|
+
|
368
|
+
function autoSaveStore(res, sp) {
|
369
|
+
// Stores XML data under specified file name in the auto-save directory
|
370
|
+
let data = 'OK';
|
371
|
+
const fn = sp.get('file');
|
372
|
+
if(!fn) {
|
373
|
+
data = 'WARNING: No name for file to auto-save';
|
374
|
+
} else {
|
375
|
+
const xml = sp.get('xml');
|
376
|
+
// Validate XML as a Linny-R model
|
377
|
+
try {
|
378
|
+
const
|
379
|
+
parser = new DOMParser(),
|
380
|
+
doc = parser.parseFromString(xml, 'text/xml');
|
381
|
+
root = doc.documentElement;
|
382
|
+
// Linny-R models have a model element as root
|
383
|
+
if(root.nodeName !== 'model') throw 'XML document has no model element';
|
384
|
+
fs.writeFileSync(path.join(WORKSPACE.autosave, fn + '.lnr'), xml);
|
385
|
+
} catch(err) {
|
386
|
+
console.log(err);
|
387
|
+
data = 'ERROR: Not a Linny-R model to auto-save';
|
388
|
+
}
|
389
|
+
}
|
390
|
+
servePlainText(res, data);
|
391
|
+
}
|
392
|
+
|
228
393
|
// Repository functionality
|
229
394
|
// ========================
|
230
395
|
// For repository services, the Linny-R JavaScript application communicates with
|
231
|
-
// the server via calls to the server like
|
396
|
+
// the server via calls to the server like fetch('repo/', x) where x is a JSON
|
232
397
|
// object with at least the entry `action`, which can be one of the following:
|
233
398
|
// id return the repository URL (for this script: 'local host')
|
234
399
|
// list return list with names of repositories available on the server
|
@@ -243,7 +408,7 @@ const SHUTDOWN_MESSAGE = `<!DOCTYPE html>
|
|
243
408
|
function repo(res, sp) {
|
244
409
|
// Processes all repository commands
|
245
410
|
const action = sp.get('action').trim();
|
246
|
-
|
411
|
+
logAction('Repository action: ' + action);
|
247
412
|
if(action === 'id') return repoID(res);
|
248
413
|
if(action === 'list') return repoList(res);
|
249
414
|
if(action === 'add') return repoAdd(res, sp);
|
@@ -257,14 +422,7 @@ function repo(res, sp) {
|
|
257
422
|
if(action === 'store') return repoStore(res, repo, file, sp.get('xml'));
|
258
423
|
if(action === 'delete') return repoDelete(res, repo, file);
|
259
424
|
// Fall-through: report error
|
260
|
-
servePlainText(res, `ERROR: Invalid action: "${action}"`);
|
261
|
-
}
|
262
|
-
|
263
|
-
function asFileName(s) {
|
264
|
-
// Returns string `s` with whitespace converted to a single dash, and special
|
265
|
-
// characters converted to underscores
|
266
|
-
s = s.trim().replace(/[\s\-]+/g, '-');
|
267
|
-
return s.replace(/[^A-Za-z0-9_\-]/g, '_');
|
425
|
+
servePlainText(res, `ERROR: Invalid repository action: "${action}"`);
|
268
426
|
}
|
269
427
|
|
270
428
|
function repositoryByName(name) {
|
@@ -607,7 +765,7 @@ function repoStore(res, rname, mname, mxml) {
|
|
607
765
|
parser = new DOMParser(),
|
608
766
|
doc = parser.parseFromString(mxml, 'text/xml');
|
609
767
|
root = doc.documentElement;
|
610
|
-
// Linny-R
|
768
|
+
// Linny-R models have a model element as root
|
611
769
|
if(root.nodeName !== 'model') throw 'XML document has no model element';
|
612
770
|
valid = true;
|
613
771
|
} catch(err) {
|
@@ -701,7 +859,7 @@ function anyOSpath(p) {
|
|
701
859
|
|
702
860
|
function loadData(res, url) {
|
703
861
|
// Passed parameter is the URL or full path
|
704
|
-
|
862
|
+
logAction('Load data from ' + url);
|
705
863
|
if(!url) servePlainText(res, 'ERROR: No URL or path');
|
706
864
|
if(url.toLowerCase().startsWith('http')) {
|
707
865
|
// URL => validate it, and then try to download its content as text
|
@@ -761,7 +919,7 @@ function receiver(res, sp) {
|
|
761
919
|
}
|
762
920
|
// Get the action from the search parameters
|
763
921
|
const action = sp.get('action');
|
764
|
-
|
922
|
+
logAction(`Receiver action: ${action} ${rpath} ${rfile}`);
|
765
923
|
if(action === 'listen') {
|
766
924
|
rcvrListen(res, rpath);
|
767
925
|
} else if(action === 'abort') {
|
@@ -914,7 +1072,7 @@ function rcvrCallBack(res, rpath, rfile, script) {
|
|
914
1072
|
}
|
915
1073
|
}
|
916
1074
|
if(cpath) {
|
917
|
-
|
1075
|
+
logAction(`Deleting ${file_type} file: ${cpath}`);
|
918
1076
|
try {
|
919
1077
|
fs.unlinkSync(cpath);
|
920
1078
|
} catch(err) {
|
@@ -930,7 +1088,7 @@ function rcvrCallBack(res, rpath, rfile, script) {
|
|
930
1088
|
}
|
931
1089
|
try {
|
932
1090
|
cmd = fs.readFileSync(path.join(WORKSPACE.callback, script), 'utf8');
|
933
|
-
|
1091
|
+
logAction(`Executing callback command "${cmd}"`);
|
934
1092
|
child_process.exec(cmd, (error, stdout, stderr) => {
|
935
1093
|
console.log(stdout);
|
936
1094
|
if(error) {
|
@@ -1025,6 +1183,8 @@ function processRequest(req, res, cmd, data) {
|
|
1025
1183
|
SERVER.close();
|
1026
1184
|
} else if(cmd === '/auto-check') {
|
1027
1185
|
autoCheck(res);
|
1186
|
+
} else if(cmd === '/autosave/') {
|
1187
|
+
autoSave(res, new URLSearchParams(data));
|
1028
1188
|
} else if(cmd === '/repo/') {
|
1029
1189
|
repo(res, new URLSearchParams(data));
|
1030
1190
|
} else if(cmd === '/load-data/') {
|
@@ -1075,11 +1235,11 @@ function serveStaticFile(res, path) {
|
|
1075
1235
|
if(path === '/' || path === '') path = '/index.html';
|
1076
1236
|
if(path.startsWith('/diagrams/')) {
|
1077
1237
|
// Serve diagrams from the (main)/user/diagrams/ sub-directory
|
1078
|
-
|
1238
|
+
logAction('Diagram: ' + path);
|
1079
1239
|
path = '/user' + path;
|
1080
1240
|
} else {
|
1081
1241
|
// Other files from the (main)/static/ subdirectory
|
1082
|
-
|
1242
|
+
logAction('Static file: ' + path);
|
1083
1243
|
path = '/static' + path;
|
1084
1244
|
}
|
1085
1245
|
fs.readFile(MODULE_DIRECTORY + path, (err, data) => {
|
@@ -1107,7 +1267,7 @@ function convertSVGtoPNG(req, res, sp) {
|
|
1107
1267
|
(new Date()).toISOString().slice(0, 19).replace(/[\-\:]/g, ''),
|
1108
1268
|
fp = path.join(WORKSPACE.diagrams, fn);
|
1109
1269
|
// NOTE: use binary encoding for SVG file
|
1110
|
-
|
1270
|
+
logAction('Saving SVG file: ' + fp);
|
1111
1271
|
try {
|
1112
1272
|
fs.writeFileSync(fp + '.svg', svg);
|
1113
1273
|
} catch(error) {
|
@@ -1115,7 +1275,7 @@ function convertSVGtoPNG(req, res, sp) {
|
|
1115
1275
|
}
|
1116
1276
|
// Use Inkscape to convert SVG to the requested format
|
1117
1277
|
if(SETTINGS.inkscape) {
|
1118
|
-
|
1278
|
+
logAction('Rendering image');
|
1119
1279
|
let
|
1120
1280
|
cmd = SETTINGS.inkscape,
|
1121
1281
|
svg = fp + '.svg';
|
package/static/index.html
CHANGED
@@ -176,6 +176,9 @@ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
|
176
176
|
// Invalid server response (should not occur, but just in case)
|
177
177
|
UI.warn('Version check failed: "' + data + '"');
|
178
178
|
}
|
179
|
+
// Schedule a new check 8 hours from now
|
180
|
+
setTimeout(checkForUpdates, 8*3600000);
|
181
|
+
|
179
182
|
})
|
180
183
|
.catch((error) => UI.warn(UI.WARNING.NO_CONNECTION, error));
|
181
184
|
}
|
@@ -206,8 +209,8 @@ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
|
206
209
|
MONITOR = new GUIMonitor();
|
207
210
|
RECEIVER = new GUIReceiver();
|
208
211
|
// Check for software updates only when running on local server
|
209
|
-
// NOTE: do this *after* GUI elements have been created, as the
|
210
|
-
// uses a dialog
|
212
|
+
// NOTE: do this *after* GUI elements have been created, as the
|
213
|
+
// updater uses a dialog
|
211
214
|
if(!SOLVER.user_id) checkForUpdates();
|
212
215
|
// Initialize auto-saving function
|
213
216
|
AUTO_SAVE = new ModelAutoSaver();
|
@@ -470,10 +473,10 @@ and move the cursor over the status bar">
|
|
470
473
|
<input id="auto-save-hours" type="text" autocomplete="off">
|
471
474
|
hours
|
472
475
|
<img id="auto-save-clear-btn" class="btn enab" src="images/reset.png"
|
473
|
-
title="Remove all auto-saved models from
|
476
|
+
title="Remove all auto-saved models from user workspace">
|
474
477
|
</div>
|
475
478
|
<div id="confirm-remove-models">
|
476
|
-
Really remove all auto-saved from
|
479
|
+
Really remove all auto-saved models from user workspace?
|
477
480
|
<strong>
|
478
481
|
<img id="autosave-do-remove" class="inline-ok-btn"
|
479
482
|
src="images/ok.png"> Yes
|
@@ -1064,7 +1067,7 @@ and move the cursor over the status bar">
|
|
1064
1067
|
μ (mean: the average level Σ/(δ+1) )
|
1065
1068
|
</option>
|
1066
1069
|
<option id="link-startup" value="5">
|
1067
|
-
&#
|
1070
|
+
▲ (start-up: 1 if X[t-1] = 0 ∧ X[t] > 0, otherwise 0)
|
1068
1071
|
</option>
|
1069
1072
|
<option id="link-on" value="6">
|
1070
1073
|
+ (positive: 1 if X[t] > 0, otherwise 0)
|
@@ -1073,7 +1076,7 @@ and move the cursor over the status bar">
|
|
1073
1076
|
0 (zero: 1 if X[t] = 0, otherwise 0)
|
1074
1077
|
</option>
|
1075
1078
|
<option id="link-shutdown" value="10">
|
1076
|
-
&#
|
1079
|
+
▼ (shut-down: 1 if X[t-1] > 0 ∧ X[t] = 0, otherwise 0)
|
1077
1080
|
</option>
|
1078
1081
|
<option id="link-spinning" value="8">
|
1079
1082
|
⤴ (spinning reserve: UB - X[t] if X[t] > 0, otherwise 0)
|
@@ -2422,12 +2425,12 @@ where X can be one or several of these letters: ABCDELPQ">
|
|
2422
2425
|
<div class="docu-sym" id="docu-link">→</div>
|
2423
2426
|
<div class="docu-sym" id="docu-constraint">⤑</div>
|
2424
2427
|
<div class="docu-sym" id="docu-bi-constraint">↔</div>
|
2425
|
-
<div class="docu-sym" id="docu-throughput">&#
|
2428
|
+
<div class="docu-sym" id="docu-throughput">⇉</div>
|
2426
2429
|
<div class="docu-sym" id="docu-change">Δ</div>
|
2427
2430
|
<div class="docu-sym" id="docu-sum">Σ</div>
|
2428
2431
|
<div class="docu-sym" id="docu-mean">μ</div>
|
2429
|
-
<div class="docu-sym" id="docu-startup">&#
|
2430
|
-
<div class="docu-sym" id="docu-shutdown">&#
|
2432
|
+
<div class="docu-sym" id="docu-startup">▲</div>
|
2433
|
+
<div class="docu-sym" id="docu-shutdown">▼</div>
|
2431
2434
|
<div class="docu-sym" id="docu-spinning-reserve">⤴</div>
|
2432
2435
|
<div class="docu-sym" id="docu-first-commit">✲</div>
|
2433
2436
|
<div class="docu-sym" id="docu-infinity">∞</div>
|
package/static/linny-r.css
CHANGED
@@ -37,6 +37,8 @@ body {
|
|
37
37
|
|
38
38
|
body.waiting * { cursor: wait; }
|
39
39
|
|
40
|
+
@charset UTF-8;
|
41
|
+
|
40
42
|
@font-face {
|
41
43
|
font-family: Lato;
|
42
44
|
src: url(fonts/Lato-Regular.ttf);
|
@@ -705,7 +707,7 @@ img.del-asm-btn:hover {
|
|
705
707
|
|
706
708
|
#auto-save-settings {
|
707
709
|
position: absolute;
|
708
|
-
bottom:
|
710
|
+
bottom: 3px;
|
709
711
|
left: 2px;
|
710
712
|
width: calc(100% - 4px);
|
711
713
|
}
|
@@ -3804,6 +3806,11 @@ select.i-param {
|
|
3804
3806
|
font-family: monospace;
|
3805
3807
|
}
|
3806
3808
|
|
3809
|
+
#confirm-delete-from-repo-msg {
|
3810
|
+
height: calc(100% - 55px);
|
3811
|
+
}
|
3812
|
+
|
3813
|
+
|
3807
3814
|
/* the FINDER DIALOG allows lookup of occurrences of entities */
|
3808
3815
|
#finder-dlg {
|
3809
3816
|
display: none;
|
@@ -4326,7 +4333,6 @@ div.call-stack-expr {
|
|
4326
4333
|
#confirm-delete-from-repo-msg,
|
4327
4334
|
#check-update-msg {
|
4328
4335
|
width: calc(100% - 8px);
|
4329
|
-
height: calc(100% - 55px);
|
4330
4336
|
margin: 3px;
|
4331
4337
|
overflow: auto;
|
4332
4338
|
}
|
@@ -277,8 +277,9 @@ class Controller {
|
|
277
277
|
// Returns `name` without the object-attribute separator |, backslashes,
|
278
278
|
// and leading and trailing whitespace, and with all internal whitespace
|
279
279
|
// reduced to a single space.
|
280
|
-
return name.replace(this.OA_SEPARATOR, ' ')
|
281
|
-
|
280
|
+
return name.replace(this.OA_SEPARATOR, ' ')
|
281
|
+
.replace(/\||\\/g, ' ').trim()
|
282
|
+
.replace(/\s\s+/g, ' ');
|
282
283
|
}
|
283
284
|
|
284
285
|
validName(name) {
|
@@ -519,8 +520,10 @@ class RepositoryBrowser {
|
|
519
520
|
asFileName(s) {
|
520
521
|
// Returns string `s` with whitespace converted to a single dash, and
|
521
522
|
// special characters converted to underscores
|
522
|
-
return s.normalize('NFKD').trim()
|
523
|
-
|
523
|
+
return s.normalize('NFKD').trim()
|
524
|
+
.replace(/[\s\-]+/g, '-')
|
525
|
+
.replace(/[^A-Za-z0-9_\-]/g, '_')
|
526
|
+
.replace(/^[\-\_]+|[\-\_]+$/g, '');
|
524
527
|
}
|
525
528
|
|
526
529
|
loadModuleAsModel() {
|
@@ -3054,9 +3054,10 @@ class GUIController extends Controller {
|
|
3054
3054
|
// Recall button toggles the documentation dialog
|
3055
3055
|
() => UI.buttons.documentation.dispatchEvent(new Event('click')));
|
3056
3056
|
this.buttons.autosave.addEventListener('click',
|
3057
|
-
|
3057
|
+
// NOTE: TRUE indicates "show dialog after obtaining the model list"
|
3058
|
+
() => AUTO_SAVE.getAutoSavedModels(true));
|
3058
3059
|
this.buttons.autosave.addEventListener('mouseover',
|
3059
|
-
() => AUTO_SAVE.
|
3060
|
+
() => AUTO_SAVE.getAutoSavedModels());
|
3060
3061
|
|
3061
3062
|
// Make "stay active" buttons respond to Shift-click
|
3062
3063
|
const
|
@@ -3282,7 +3283,7 @@ class GUIController extends Controller {
|
|
3282
3283
|
// Undoable operations no longer apply!
|
3283
3284
|
UNDO_STACK.clear();
|
3284
3285
|
// Autosaving should start anew
|
3285
|
-
AUTO_SAVE.
|
3286
|
+
AUTO_SAVE.setAutoSaveInterval();
|
3286
3287
|
// Signal success or failure
|
3287
3288
|
return loaded;
|
3288
3289
|
}
|
@@ -4732,7 +4733,9 @@ class GUIController extends Controller {
|
|
4732
4733
|
// Update global variable (and force display) only for "real" messages
|
4733
4734
|
this.time_last_message = t;
|
4734
4735
|
dt = this.message_display_time;
|
4735
|
-
SOUNDS[type].play()
|
4736
|
+
SOUNDS[type].play().catch(() => {
|
4737
|
+
console.log('NOTICE: Sounds will only play after first user action');
|
4738
|
+
});
|
4736
4739
|
const
|
4737
4740
|
now = [d.getHours(), d.getMinutes().toString().padStart(2, '0'),
|
4738
4741
|
d.getSeconds().toString().padStart(2, '0')].join(':'),
|
@@ -4823,7 +4826,7 @@ class GUIController extends Controller {
|
|
4823
4826
|
UNDO_STACK.clear();
|
4824
4827
|
VM.reset();
|
4825
4828
|
this.updateButtons();
|
4826
|
-
AUTO_SAVE.
|
4829
|
+
AUTO_SAVE.setAutoSaveInterval();
|
4827
4830
|
}
|
4828
4831
|
|
4829
4832
|
addNode(type) {
|
@@ -6059,8 +6062,8 @@ class GUIMonitor {
|
|
6059
6062
|
|
6060
6063
|
|
6061
6064
|
// CLASS GUIFileManager provides the GUI for loading and saving models and
|
6062
|
-
// diagrams and handles
|
6063
|
-
//
|
6065
|
+
// diagrams and handles the interaction with the MILP solver via POST requests
|
6066
|
+
// to the server.
|
6064
6067
|
// NOTE: because the console-only monitor requires Node.js modules, this
|
6065
6068
|
// GUI class does NOT extend its console-only counterpart
|
6066
6069
|
class GUIFileManager {
|
@@ -6184,15 +6187,10 @@ class GUIFileManager {
|
|
6184
6187
|
// Show "Load model" modal
|
6185
6188
|
// @@TO DO: warn user if unsaved changes to current model
|
6186
6189
|
UI.hideStayOnTopDialogs();
|
6187
|
-
|
6188
|
-
|
6189
|
-
|
6190
|
-
|
6191
|
-
rbtn.title = pluralS(ml.length, 'auto-saved model');
|
6192
|
-
rbtn.style.display = 'block';
|
6193
|
-
} else {
|
6194
|
-
rbtn.style.display = 'none';
|
6195
|
-
}
|
6190
|
+
// Update auto-saved model list; if not empty, this will display the
|
6191
|
+
// "restore autosaved files" button
|
6192
|
+
AUTO_SAVE.getAutoSavedModels();
|
6193
|
+
// Show the "Load model" dialog
|
6196
6194
|
UI.modals.load.show();
|
6197
6195
|
}
|
6198
6196
|
|
@@ -6282,7 +6280,7 @@ class GUIFileManager {
|
|
6282
6280
|
console.log('Encoded file size:', el.href.length);
|
6283
6281
|
el.download = 'model.lnr';
|
6284
6282
|
if(el.href.length > 25*1024*1024 &&
|
6285
|
-
navigator.userAgent.search('Chrome'
|
6283
|
+
navigator.userAgent.search('Chrome') <= 0) {
|
6286
6284
|
UI.notify('Model file size exceeds 25 MB. ' +
|
6287
6285
|
'If it does not download, store it in a repository');
|
6288
6286
|
}
|
@@ -6317,47 +6315,55 @@ class GUIFileManager {
|
|
6317
6315
|
console.log(err);
|
6318
6316
|
});
|
6319
6317
|
}
|
6320
|
-
|
6321
|
-
|
6322
|
-
|
6323
|
-
|
6324
|
-
|
6325
|
-
|
6326
|
-
|
6327
|
-
|
6328
|
-
|
6329
|
-
|
6318
|
+
|
6319
|
+
loadAutoSavedModel(name) {
|
6320
|
+
fetch('autosave/', postData({
|
6321
|
+
action: 'load',
|
6322
|
+
file: name
|
6323
|
+
}))
|
6324
|
+
.then((response) => {
|
6325
|
+
if(!response.ok) {
|
6326
|
+
UI.alert(`ERROR ${response.status}: ${response.statusText}`);
|
6327
|
+
}
|
6328
|
+
return response.text();
|
6329
|
+
})
|
6330
|
+
.then((data) => {
|
6331
|
+
if(UI.postResponseOK(data)) UI.loadModelFromXML(data);
|
6332
|
+
})
|
6333
|
+
.catch((err) => UI.warn(UI.WARNING.NO_CONNECTION, err));
|
6334
|
+
}
|
6335
|
+
|
6336
|
+
storeAutoSavedModel() {
|
6337
|
+
// Stores the current model in the local auto-save directory
|
6338
|
+
const bcl = document.getElementById('autosave-btn').classList;
|
6330
6339
|
if(MODEL.running_experiment) {
|
6331
6340
|
console.log('No autosaving while running an experiment');
|
6341
|
+
bcl.remove('stay-activ');
|
6332
6342
|
return;
|
6333
6343
|
}
|
6334
|
-
|
6335
|
-
|
6336
|
-
|
6337
|
-
|
6338
|
-
|
6339
|
-
|
6340
|
-
|
6341
|
-
|
6342
|
-
|
6343
|
-
|
6344
|
-
|
6345
|
-
|
6344
|
+
fetch('autosave/', postData({
|
6345
|
+
action: 'store',
|
6346
|
+
file: REPOSITORY_BROWSER.asFileName(
|
6347
|
+
(MODEL.name || 'no-name') + '_by_' +
|
6348
|
+
(MODEL.author || 'no-author')),
|
6349
|
+
xml: MODEL.asXML
|
6350
|
+
}))
|
6351
|
+
.then((response) => {
|
6352
|
+
if(!response.ok) {
|
6353
|
+
UI.alert(`ERROR ${response.status}: ${response.statusText}`);
|
6354
|
+
}
|
6355
|
+
return response.text();
|
6356
|
+
})
|
6357
|
+
.then((data) => {
|
6358
|
+
UI.postResponseOK(data);
|
6359
|
+
bcl.remove('stay-activ');
|
6360
|
+
})
|
6361
|
+
.catch((err) => {
|
6362
|
+
UI.warn(UI.WARNING.NO_CONNECTION, err);
|
6363
|
+
bcl.remove('stay-activ');
|
6364
|
+
});
|
6346
6365
|
}
|
6347
6366
|
|
6348
|
-
loadFromLocalStorage(key) {
|
6349
|
-
// Retrieve auto-saved model identified by `key`
|
6350
|
-
// NOTE: browser may be configured to prohibit local storage function
|
6351
|
-
try {
|
6352
|
-
const
|
6353
|
-
ls = window.localStorage,
|
6354
|
-
xml = ls.getItem(key);
|
6355
|
-
if(xml) UI.loadModelFromXML(xml);
|
6356
|
-
} catch(err) {
|
6357
|
-
UI.alert(`Failed to restore auto-saved model ${key}: ${err}`);
|
6358
|
-
}
|
6359
|
-
}
|
6360
|
-
|
6361
6367
|
renderDiagramAsPNG() {
|
6362
6368
|
localStorage.removeItem('png-url');
|
6363
6369
|
UI.paper.fitToSize();
|
@@ -6811,27 +6817,28 @@ NOTE: Grouping groups results in a single group, e.g., (1;2);(3;4;5) evaluates a
|
|
6811
6817
|
} // END of class ExpressionEditor
|
6812
6818
|
|
6813
6819
|
|
6814
|
-
// CLASS ModelAutoSaver automatically saves the current model at regular
|
6815
|
-
// intervals
|
6816
|
-
// NOTE: it seemed to be a good idea to do this in the browser's local storage,
|
6817
|
-
// but this breaks for large model files
|
6818
|
-
// @@TO DO: re-implement via POST requests to server
|
6820
|
+
// CLASS ModelAutoSaver automatically saves the current model at regular
|
6821
|
+
// time intervals in the user's `autosave` directory
|
6819
6822
|
class ModelAutoSaver {
|
6820
6823
|
constructor() {
|
6821
6824
|
// Keep track of time-out interval of auto-saving feature
|
6822
6825
|
this.timeout_id = 0;
|
6823
|
-
this.time_prefix = '_A_S_T_$';
|
6824
6826
|
this.interval = 10; // auto-save every 10 minutes
|
6825
6827
|
this.period = 24; // delete models older than 24 hours
|
6828
|
+
this.model_list = [];
|
6826
6829
|
// Overwite defaults if settings still in local storage of browser
|
6827
|
-
this.
|
6828
|
-
|
6830
|
+
this.getSettings();
|
6831
|
+
// Purge files that have "expired"
|
6832
|
+
this.getAutoSavedModels();
|
6833
|
+
// Start the interval timer
|
6834
|
+
this.setAutoSaveInterval();
|
6829
6835
|
// Add listeners to GUI elements
|
6830
6836
|
this.confirm_dialog = document.getElementById('confirm-remove-models');
|
6831
6837
|
document.getElementById('auto-save-clear-btn').addEventListener('click',
|
6832
6838
|
() => AUTO_SAVE.confirm_dialog.style.display = 'block');
|
6833
6839
|
document.getElementById('autosave-do-remove').addEventListener('click',
|
6834
|
-
|
6840
|
+
// NOTE: file name parameter /*ALL*/ indicates: delete all
|
6841
|
+
() => AUTO_SAVE.getAutoSavedModels(true, '/*ALL*/'));
|
6835
6842
|
document.getElementById('autosave-cancel').addEventListener('click',
|
6836
6843
|
() => AUTO_SAVE.confirm_dialog.style.display = 'none');
|
6837
6844
|
document.getElementById('restore-cancel').addEventListener('click',
|
@@ -6850,14 +6857,14 @@ class ModelAutoSaver {
|
|
6850
6857
|
m = parseFloat(mh[0]),
|
6851
6858
|
h = parseFloat(mh[1]);
|
6852
6859
|
if(isNaN(m) || isNaN(h)) {
|
6853
|
-
UI.warn('
|
6860
|
+
UI.warn('Ignored invalid local auto-save settings');
|
6854
6861
|
} else {
|
6855
6862
|
this.interval = m;
|
6856
6863
|
this.period = h;
|
6857
6864
|
}
|
6858
6865
|
}
|
6859
6866
|
} catch(err) {
|
6860
|
-
console.log('
|
6867
|
+
console.log('Local storage failed:', err);
|
6861
6868
|
}
|
6862
6869
|
}
|
6863
6870
|
|
@@ -6866,8 +6873,10 @@ class ModelAutoSaver {
|
|
6866
6873
|
try {
|
6867
6874
|
window.localStorage.setItem('Linny-R-autosave',
|
6868
6875
|
this.interval + '|' + this.period);
|
6876
|
+
UI.notify('New auto-save settings stored in browser');
|
6869
6877
|
} catch(err) {
|
6870
6878
|
UI.warn('Failed to write auto-save settings to local storage');
|
6879
|
+
console.log(err);
|
6871
6880
|
}
|
6872
6881
|
}
|
6873
6882
|
|
@@ -6875,128 +6884,80 @@ class ModelAutoSaver {
|
|
6875
6884
|
document.getElementById('autosave-btn').classList.add('stay-activ');
|
6876
6885
|
// Use setTimeout to let browser always briefly show the active color
|
6877
6886
|
// even when the model file is small and storing hardly takes time
|
6878
|
-
setTimeout(() => FILE_MANAGER.
|
6887
|
+
setTimeout(() => FILE_MANAGER.storeAutoSavedModel(), 300);
|
6879
6888
|
}
|
6880
6889
|
|
6881
|
-
|
6890
|
+
setAutoSaveInterval() {
|
6882
6891
|
// Activate the auto-save feature (if interval is configured)
|
6883
6892
|
if(this.timeout_id) clearInterval(this.timeout_id);
|
6884
|
-
|
6893
|
+
// NOTE: interval = 0 indicates "do not auto-save"
|
6885
6894
|
if(this.interval) {
|
6886
6895
|
// Interval is in minutes, so multiply by 60 thousand to get msec
|
6887
6896
|
this.timeout_id = setInterval(
|
6888
6897
|
() => AUTO_SAVE.saveModel(), this.interval * 60000);
|
6889
6898
|
}
|
6890
6899
|
}
|
6891
|
-
|
6892
|
-
|
6893
|
-
//
|
6894
|
-
|
6895
|
-
|
6896
|
-
|
6897
|
-
|
6898
|
-
|
6899
|
-
|
6900
|
-
|
6901
|
-
|
6902
|
-
window.localStorage.removeItem(name);
|
6903
|
-
console.log('Purged model', name, 'from local storage');
|
6904
|
-
// Also remove the timestamp item
|
6905
|
-
window.localStorage.removeItem(key);
|
6900
|
+
|
6901
|
+
getAutoSavedModels(show_dialog=false, file_to_delete='') {
|
6902
|
+
// Get list of auto-saved models from server (after deleting those that
|
6903
|
+
// have been stored beyond the set period AND the specified file to
|
6904
|
+
// delete (where /*ALL*/ indicates "delete all auto-saved files")
|
6905
|
+
const pd = {action: 'purge', period: this.period};
|
6906
|
+
if(file_to_delete) pd.to_delete = file_to_delete;
|
6907
|
+
fetch('autosave/', postData(pd))
|
6908
|
+
.then((response) => {
|
6909
|
+
if(!response.ok) {
|
6910
|
+
UI.alert(`ERROR ${response.status}: ${response.statusText}`);
|
6906
6911
|
}
|
6907
|
-
|
6908
|
-
|
6909
|
-
|
6910
|
-
|
6911
|
-
|
6912
|
-
|
6913
|
-
|
6914
|
-
|
6915
|
-
|
6916
|
-
|
6917
|
-
this.purgeSavedModels();
|
6918
|
-
const list = [];
|
6919
|
-
try {
|
6920
|
-
for(let key in window.localStorage) {
|
6921
|
-
if(key.startsWith(this.time_prefix)) {
|
6922
|
-
const
|
6923
|
-
name = key.split(this.time_prefix)[1],
|
6924
|
-
ts = parseInt(window.localStorage.getItem(key)),
|
6925
|
-
// Retrieve the item to make sure it exists
|
6926
|
-
xml = window.localStorage.getItem(name);
|
6927
|
-
if(xml && xml.length > 100) {
|
6928
|
-
let mdate = new Date();
|
6929
|
-
mdate.setTime(ts);
|
6930
|
-
const offset = mdate.getTimezoneOffset();
|
6931
|
-
mdate = new Date(mdate.getTime() - (offset * 60 * 1000));
|
6932
|
-
mdate = mdate.toISOString().split(':');
|
6933
|
-
mdate = mdate[0].replace('T', ' ') + ':' + mdate[1];
|
6934
|
-
list.push([name, mdate, UI.sizeInBytes(xml.length)]);
|
6935
|
-
} else {
|
6936
|
-
console.log('Autosaved model not found or invalid:', xml);
|
6912
|
+
return response.text();
|
6913
|
+
})
|
6914
|
+
.then((data) => {
|
6915
|
+
if(UI.postResponseOK(data)) {
|
6916
|
+
try {
|
6917
|
+
AUTO_SAVE.model_list = JSON.parse(data);
|
6918
|
+
} catch(err) {
|
6919
|
+
AUTO_SAVE.model_list = [];
|
6920
|
+
UI.warn('Data on auto-saved models is not valid');
|
6921
|
+
}
|
6937
6922
|
}
|
6938
|
-
|
6939
|
-
|
6940
|
-
|
6941
|
-
|
6942
|
-
|
6943
|
-
|
6944
|
-
|
6945
|
-
|
6946
|
-
|
6947
|
-
|
6948
|
-
|
6949
|
-
return list;
|
6950
|
-
}
|
6951
|
-
|
6952
|
-
checkForSavedModels() {
|
6953
|
-
const ml = this.savedModelList();
|
6954
|
-
document.getElementById('autosave-btn').title =
|
6955
|
-
pluralS(ml.length, 'auto-saved model');
|
6956
|
-
}
|
6957
|
-
|
6958
|
-
removeSavedModels() {
|
6959
|
-
const ml = this.savedModelList();
|
6960
|
-
for(let i = 0; i < ml.length; i++) {
|
6961
|
-
const n = ml[i][0];
|
6962
|
-
window.localStorage.removeItem(n);
|
6963
|
-
window.localStorage.removeItem(this.time_prefix + n);
|
6964
|
-
}
|
6965
|
-
this.hideRestoreDialog(true);
|
6966
|
-
}
|
6967
|
-
|
6968
|
-
deleteSavedModel(n) {
|
6969
|
-
window.localStorage.removeItem(n);
|
6970
|
-
window.localStorage.removeItem(this.time_prefix + n);
|
6971
|
-
const ml = this.savedModelList();
|
6972
|
-
if(ml.length > 0) {
|
6973
|
-
this.showRestoreDialog();
|
6974
|
-
} else {
|
6975
|
-
this.hideRestoreDialog(true);
|
6976
|
-
}
|
6923
|
+
// Update auto-save-related dialog elements
|
6924
|
+
const
|
6925
|
+
n = this.model_list.length,
|
6926
|
+
ttl = pluralS(n, 'auto-saved model'),
|
6927
|
+
rbtn = document.getElementById('load-autosaved-btn');
|
6928
|
+
document.getElementById('autosave-btn').title = ttl;
|
6929
|
+
rbtn.title = ttl;
|
6930
|
+
rbtn.style.display = (n > 0 ? 'block' : 'none');
|
6931
|
+
if(show_dialog) AUTO_SAVE.showRestoreDialog();
|
6932
|
+
})
|
6933
|
+
.catch((err) => {console.log(err); UI.warn(UI.WARNING.NO_CONNECTION, err);});
|
6977
6934
|
}
|
6978
|
-
|
6935
|
+
|
6979
6936
|
showRestoreDialog() {
|
6980
6937
|
// Shows list of auto-saved models; clicking on one will load it
|
6981
|
-
|
6938
|
+
// NOTE: hide "Load model" dialog in case it was showing
|
6982
6939
|
document.getElementById('load-modal').style.display = 'none';
|
6983
6940
|
// Contruct the table to select from
|
6984
6941
|
let html = '';
|
6985
|
-
for(let i = 0; i <
|
6986
|
-
const
|
6942
|
+
for(let i = 0; i < this.model_list.length; i++) {
|
6943
|
+
const
|
6944
|
+
m = this.model_list[i],
|
6945
|
+
bytes = UI.sizeInBytes(m.size).split(' ');
|
6987
6946
|
html += ['<tr class="dataset" style="color: gray" ',
|
6988
|
-
'onclick="FILE_MANAGER.
|
6989
|
-
|
6990
|
-
|
6991
|
-
|
6992
|
-
'</td><td><
|
6993
|
-
'
|
6994
|
-
|
6947
|
+
'onclick="FILE_MANAGER.loadAutoSavedModel(\'',
|
6948
|
+
m.name,'\');"><td class="restore-name">', m.name, '</td><td>',
|
6949
|
+
m.date.substring(1, 16).replace('T', ' '),
|
6950
|
+
'</td><td style="text-align: right">',
|
6951
|
+
bytes[0], '</td><td>', bytes[1], '</td><td style="width:15px">',
|
6952
|
+
'<img class="del-asm-btn" src="images/delete.png" ',
|
6953
|
+
'onclick="event.stopPropagation(); ',
|
6954
|
+
'AUTO_SAVE.getAutoSavedModels(true, \'', m.name,
|
6955
|
+
'\')"></td></tr>'].join('');
|
6995
6956
|
}
|
6996
6957
|
document.getElementById('restore-table').innerHTML = html;
|
6997
6958
|
// Adjust dialog height (max-height will limit list to 10 lines)
|
6998
6959
|
document.getElementById('restore-dlg').style.height =
|
6999
|
-
(
|
6960
|
+
(48 + 19 * this.model_list.length) + 'px';
|
7000
6961
|
document.getElementById('confirm-remove-models').style.display = 'none';
|
7001
6962
|
// Fill text input fields with present settings
|
7002
6963
|
document.getElementById('auto-save-minutes').value = this.interval;
|
@@ -7006,7 +6967,7 @@ class ModelAutoSaver {
|
|
7006
6967
|
ttl = document.getElementById('restore-dlg-title'),
|
7007
6968
|
sa = document.getElementById('restore-scroll-area'),
|
7008
6969
|
btn = document.getElementById('auto-save-clear-btn');
|
7009
|
-
if(
|
6970
|
+
if(this.model_list.length) {
|
7010
6971
|
ttl.innerHTML = 'Restore auto-saved model';
|
7011
6972
|
sa.style.display = 'block';
|
7012
6973
|
btn.style.display = 'block';
|
@@ -7037,10 +6998,10 @@ class ModelAutoSaver {
|
|
7037
6998
|
if(!isNaN(h)) {
|
7038
6999
|
// If valid, store in local storage of browser
|
7039
7000
|
if(m !== this.interval || h !== this.period) {
|
7040
|
-
UI.notify('New auto-save settings stored in browser');
|
7041
7001
|
this.interval = m;
|
7042
7002
|
this.period = h;
|
7043
7003
|
this.setSettings();
|
7004
|
+
this.setAutoSaveInterval();
|
7044
7005
|
}
|
7045
7006
|
document.getElementById('restore-modal').style.display = 'none';
|
7046
7007
|
return;
|
@@ -8188,7 +8149,6 @@ class Repository {
|
|
8188
8149
|
|
8189
8150
|
} // END of class Repository
|
8190
8151
|
|
8191
|
-
|
8192
8152
|
//
|
8193
8153
|
// Draggable & resizable dialogs
|
8194
8154
|
//
|
@@ -112,7 +112,7 @@ function rangeToList(str, max=0) {
|
|
112
112
|
function dateToString(d) {
|
113
113
|
// Returns date-time `d` in UTC format, accounting for time zone
|
114
114
|
const offset = d.getTimezoneOffset();
|
115
|
-
d = new Date(d.getTime() - offset*60000);
|
115
|
+
d = new Date(d.getTime() - offset * 60000);
|
116
116
|
return d.toISOString().split('T')[0];
|
117
117
|
}
|
118
118
|
|
@@ -1451,8 +1451,8 @@ class VirtualMachine {
|
|
1451
1451
|
this.LM_PEAK_INC = 11; // Symbol: plus inside triangle ("peak-plus")
|
1452
1452
|
// List of link multipliers that require binary ON/OFF variables
|
1453
1453
|
this.LM_NEEDING_ON_OFF = [5, 6, 7, 8, 9, 10];
|
1454
|
-
this.LM_SYMBOLS = ['', '\u21C9', '\u0394', '\u03A3', '\u03BC', '\
|
1455
|
-
'+', '0', '\u2934', '\u2732', '\
|
1454
|
+
this.LM_SYMBOLS = ['', '\u21C9', '\u0394', '\u03A3', '\u03BC', '\u25B2',
|
1455
|
+
'+', '0', '\u2934', '\u2732', '\u25BC', '\u2A39'];
|
1456
1456
|
|
1457
1457
|
// VM max. expression stack size
|
1458
1458
|
this.MAX_STACK = 200;
|
@@ -1631,6 +1631,8 @@ class VirtualMachine {
|
|
1631
1631
|
// Initialize error counters (error count will be reset to 0 for each block)
|
1632
1632
|
this.error_count = 0;
|
1633
1633
|
this.block_issues = 0;
|
1634
|
+
// NOTE: special tracking of potential solver license errors
|
1635
|
+
this.license_expired = 0;
|
1634
1636
|
// Reset solver result arrays
|
1635
1637
|
this.round_times.length = 0;
|
1636
1638
|
this.solver_times.length = 0;
|
@@ -1708,7 +1710,8 @@ class VirtualMachine {
|
|
1708
1710
|
// Other special values are very big POSITIVE numbers, so start
|
1709
1711
|
// comparing `n` with the highest value
|
1710
1712
|
if(n >= this.COMPUTING) return [true, '\u25A6']; // Checkered square
|
1711
|
-
|
1713
|
+
// NOTE: prettier circled bold X 2BBF does not display on macOS !!
|
1714
|
+
if(n >= this.NOT_COMPUTED) return [true, '\u2297']; // Circled X
|
1712
1715
|
if(n >= this.UNDEFINED) return [true, '\u2047']; // Double question mark ??
|
1713
1716
|
if(n >= this.PLUS_INFINITY) return [true, '\u221E'];
|
1714
1717
|
return [false, n];
|
@@ -4526,7 +4529,7 @@ class VirtualMachine {
|
|
4526
4529
|
checkLicense() {
|
4527
4530
|
// Compares license expiry date (if set) with current time, and notifies
|
4528
4531
|
// when three days or less remain
|
4529
|
-
if(this.license_expires) {
|
4532
|
+
if(this.license_expires && this.license_expires.length) {
|
4530
4533
|
// NOTE: expiry date has YYYY-MM-DD format
|
4531
4534
|
const
|
4532
4535
|
xds = this.license_expires[0].slice(-10).split('-'),
|
@@ -4582,6 +4585,9 @@ Solver status = ${json.status}`);
|
|
4582
4585
|
}
|
4583
4586
|
if(json.error) {
|
4584
4587
|
const errmsg = 'Solver error: ' + json.error;
|
4588
|
+
if(errmsg.indexOf('license') >= 0 && errmsg.indexOf('expired') >= 0) {
|
4589
|
+
this.license_expired += 1;
|
4590
|
+
}
|
4585
4591
|
this.logMessage(bnr, errmsg);
|
4586
4592
|
UI.alert(errmsg);
|
4587
4593
|
}
|
@@ -4633,6 +4639,10 @@ Solver status = ${json.status}`);
|
|
4633
4639
|
if(this.block_issues) UI.warn('Issues occurred in ' +
|
4634
4640
|
pluralS(this.block_issues, 'block') +
|
4635
4641
|
' -- check messages in monitor');
|
4642
|
+
if(this.license_expired > 0) {
|
4643
|
+
// Special message to draw attention to this critical error
|
4644
|
+
UI.alert('SOLVER LICENSE EXPIRED: Please check!');
|
4645
|
+
}
|
4636
4646
|
// Call back to the console (if callback hook has been set)
|
4637
4647
|
if(this.callback) this.callback(this);
|
4638
4648
|
return;
|