linny-r 1.1.8 → 1.1.10
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 +65 -71
- package/package.json +1 -1
- package/server.js +152 -15
- package/static/index.html +17 -4
- package/static/linny-r.css +9 -8
- package/static/scripts/linny-r-ctrl.js +7 -4
- package/static/scripts/linny-r-gui.js +123 -165
- package/static/scripts/linny-r-utils.js +1 -1
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.9
|
170
|
+
Node.js version: v18.11.0
|
173
171
|
... etc.
|
174
172
|
</pre>
|
175
173
|
|
@@ -207,21 +205,12 @@ Meanwhile, in the CLI, you should see a server log message like:
|
|
207
205
|
Solve block 1 a
|
208
206
|
</pre>
|
209
207
|
|
210
|
-
|
211
|
-
|
212
|
-
|
213
|
-
|
214
|
-
|
215
|
-
* `channel` and `callback` will be used to interact with Linny-R via its _Receiver_
|
216
|
-
* `diagrams` will be used to render Scalable Vector Graphics (SVG) files as
|
217
|
-
Portable Network Graphics (PNG) using Inkscape (if installed)
|
218
|
-
* `modules` will contain models stored in the `local host` _repository_
|
219
|
-
* `solver` will contain the files that are exchanged with the Mixed Integer Linear Programming (MILP) solver
|
220
|
-
(the names of the files that will appear in this directory may vary, depending on the MILP-solver you use)
|
221
|
-
|
222
|
-
|
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.
|
223
212
|
|
224
|
-
|
213
|
+
## Command line options
|
225
214
|
|
226
215
|
Optionally, you can add more arguments to the `node` command:
|
227
216
|
|
@@ -233,7 +222,46 @@ solver=[name] to overrule the default sequence (Gurobi, LP_solve)
|
|
233
222
|
workspace=[path] to overrule the default path for the user directory
|
234
223
|
</pre>
|
235
224
|
|
236
|
-
|
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
|
247
|
+
|
248
|
+
The user workspace is created when the server is run for the first time.
|
249
|
+
The sub-directories of this directory `user` are used by Linny-R to store files.
|
250
|
+
|
251
|
+
* `autosave` will contain models that have been _auto-saved_
|
252
|
+
* `channel` and `callback` will be used to interact with Linny-R via its _Receiver_
|
253
|
+
* `diagrams` will be used to render Scalable Vector Graphics (SVG) files as
|
254
|
+
Portable Network Graphics (PNG) using Inkscape (if installed)
|
255
|
+
* `modules` will contain models stored in the `local host` _repository_
|
256
|
+
* `solver` will contain the files that are exchanged with the Mixed Integer Linear Programming (MILP) solver
|
257
|
+
(the names of the files that will appear in this directory may vary, depending on the MILP-solver you use)
|
258
|
+
|
259
|
+
By default, the `user` directory is created in your `WORKING_DIRECTORY`.
|
260
|
+
You can overrule this by specifying the path to another directory when you start the server.
|
261
|
+
Note that doing this will create a new, empty workspace (the directories listed above)
|
262
|
+
in the specified path. It will **not** affect or duplicate information from existing workspaces.
|
263
|
+
|
264
|
+
## Installing Inkscape
|
237
265
|
|
238
266
|
Linny-R creates its diagrams and charts as SVG images.
|
239
267
|
When you download a diagram, it will be saved as a .svg file.
|
@@ -257,41 +285,7 @@ On a macOS computer, Linny-R will look for Inkscape in /Applications/Inkscape.ap
|
|
257
285
|
**NOTE:** The current installation wizard for Inkscape (version 1.2) does **not** add the application to the PATH variable,
|
258
286
|
so you need to do this yourself.
|
259
287
|
|
260
|
-
|
261
|
-
|
262
|
-
To facilitate start-up, you can create a shortcut icon on your desktop.
|
263
|
-
|
264
|
-
On a Windows machine, change to your Linny-R folder, right-click on the batch file `linny-r.bat`,
|
265
|
-
and select the _Create shortcut_ option.
|
266
|
-
Then right-click on the shortcut file to edit its properties, and click the _Change Icon_ button.
|
267
|
-
The dialog that then appears will allow you to go to the sub-folder `node_modules\linny-r\static\images`,
|
268
|
-
where you should select the file `linny-r.ico`.
|
269
|
-
Finally, rename the shortcut to `Linny-R` and move or copy it to your desktop.
|
270
|
-
|
271
|
-
On a macOS machine, open Terminal and change to your Linny-R directory, and then type:
|
272
|
-
|
273
|
-
``chmod +x linny-r.command``
|
274
|
-
|
275
|
-
to make the script file executable.
|
276
|
-
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``.
|
277
|
-
Then open your Linny-R folder in the Finder, change to the sub-folder `node_modules/linny-r/static/images`,
|
278
|
-
and from there drag/drop the file `linny-r.icns` on the icon shown in the top left corner of the Info dialog.
|
279
|
-
|
280
|
-
|
281
|
-
### Normal use after installation
|
282
|
-
|
283
|
-
If you have not configured a "click-start" icon as described above,
|
284
|
-
you must start a modeling session with Linny-R by opening a CLI box,
|
285
|
-
then change to the Linny-R directory and type `linny-r`.
|
286
|
-
|
287
|
-
To shut down the server, click on the local host icon in the upper right corner of the Linny-R GUI in your browser.
|
288
|
-
Alternatively, you can stop the server by repeatedly pressing ``Ctrl+C`` in the CLI box.
|
289
|
-
|
290
|
-
Pressing ``Ctrl+C`` in the Terminal window on a macOS machine may not stop the process.
|
291
|
-
In that case, you can stop Node.js by stopping the Terminal.
|
292
|
-
|
293
|
-
|
294
|
-
### Using Linny-R console
|
288
|
+
## Using Linny-R console
|
295
289
|
|
296
290
|
The console-only version of Linny-R allows you to run a Linny-R model without a web browser.
|
297
291
|
This may be useful when you want run models from a script (shell script, Python, ...).
|
@@ -303,17 +297,17 @@ you will see the command line options that allow you to run models in various wa
|
|
303
297
|
|
304
298
|
**NOTE: The console-only version is still in development, and does not provide all functions yet.**
|
305
299
|
|
306
|
-
|
300
|
+
## Troubleshooting problems
|
307
301
|
|
308
302
|
If during any of the steps above you encounter problems, please try to diagnose them and resolve them yourself.
|
309
|
-
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 documentation website:
|
310
304
|
<a href="https://linny-r.info" target="_blank">https://linny-r.info</a>.
|
311
305
|
|
312
306
|
To diagnose a problem, always look in the CLI box where Node.js is running,
|
313
307
|
as informative server-side error messages will appear there.
|
314
308
|
|
315
309
|
Then also look at the console window of your browser.
|
316
|
-
Most browsers offer a
|
310
|
+
Most browsers offer a _Web Developer Tools_ option via their application menu.
|
317
311
|
This will allow you to view the browser console, which will display JavaScript errors in red font.
|
318
312
|
|
319
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
@@ -197,32 +197,173 @@ const SHUTDOWN_MESSAGE = `<!DOCTYPE html>
|
|
197
197
|
font-family: sans-serif;
|
198
198
|
font-size: 15px;
|
199
199
|
}
|
200
|
+
code {
|
201
|
+
background-color: black;
|
202
|
+
color: white;
|
203
|
+
padding: 2px;
|
204
|
+
border-radius: 5px;
|
205
|
+
}
|
200
206
|
</style>
|
201
207
|
</head>
|
202
208
|
<body>
|
203
209
|
<h3>Linny-R server (127.0.0.1) is shutting down</h3>
|
204
|
-
<p>To restart Linny-R, switch to your
|
210
|
+
<p>To restart Linny-R, switch to your <em>${SETTINGS.cli_name}</em> window
|
205
211
|
and there at the prompt` +
|
206
212
|
(VERSION_INFO.up_to_date ? '' : `
|
207
213
|
first type:</p>
|
208
|
-
<p
|
214
|
+
<p><code>npm update linny-r</code><p>
|
209
215
|
to upgrade to Linny-R version ${VERSION_INFO.latest}, and then`) +
|
210
216
|
` type:</p>
|
211
|
-
<p
|
217
|
+
<p><code>node node_modules${path.sep}linny-r${path.sep}server</code></p>
|
212
218
|
<p>
|
213
|
-
Then switch back to this window, and click
|
219
|
+
Then switch back to this window, and click this
|
214
220
|
<button type="button"
|
215
221
|
onclick="window.location.href = 'http://127.0.0.1:${SETTINGS.port}';">
|
216
222
|
Restart
|
217
|
-
</button>
|
223
|
+
</button> button.
|
218
224
|
</p>
|
219
225
|
</body>
|
220
226
|
</html>`;
|
221
227
|
|
228
|
+
// Auto-save & restore model functionality
|
229
|
+
// =======================================
|
230
|
+
// For auto-save services, the Linny-R JavaScript application communicates with
|
231
|
+
// the server via calls to the server like fetch('autosave/', x) where x is a JSON
|
232
|
+
// object with at least the entry `action`, which can be one of the following:
|
233
|
+
// purge remove all model files older than the set auto-save period
|
234
|
+
// store write the property x.xml to the file with name x.name
|
235
|
+
// load return the XML contents of the specified model file
|
236
|
+
// Each action returns a JSON string that represents the actualized auto-save
|
237
|
+
// settings (interval and perdiod) and list of auto-saved model data objects.
|
238
|
+
// For each model: {name, file_name, size, time_saved}
|
239
|
+
|
240
|
+
function asFileName(s) {
|
241
|
+
// Returns string `s` in lower case with whitespace converted to a single
|
242
|
+
// dash, special characters converted to underscores, and leading and
|
243
|
+
// trailing dashes and underscores removed
|
244
|
+
return s.normalize('NFKD').trim()
|
245
|
+
.replace(/[\s\-]+/g, '-')
|
246
|
+
.replace(/[^A-Za-z0-9_\-]/g, '_')
|
247
|
+
.replace(/^[\-\_]+|[\-\_]+$/g, '');
|
248
|
+
}
|
249
|
+
|
250
|
+
function autoSave(res, sp) {
|
251
|
+
// Processes all auto-save & restore commands
|
252
|
+
const action = sp.get('action').trim();
|
253
|
+
console.log('Auto-save action:', action);
|
254
|
+
if(['purge', 'load', 'store'].indexOf(action) < 0) {
|
255
|
+
// Invalid action => report error
|
256
|
+
return servePlainText(res, `ERROR: Invalid auto-save action: "${action}"`);
|
257
|
+
}
|
258
|
+
// Always purge the auto-save files before further action; this returns
|
259
|
+
// the list with model data objects
|
260
|
+
const data = autoSavePurge(res, sp);
|
261
|
+
// NOTE: if string instead of array, this string is an error message
|
262
|
+
if(typeof data === 'string') return servePlainText(res, data);
|
263
|
+
// Perform load or store actions if requested
|
264
|
+
if(action === 'load') return autoSaveLoad(res, sp);
|
265
|
+
if(action === 'store') return autoSaveStore(res, sp);
|
266
|
+
// Otherwise, action was 'purge' => return the auto-saved model list
|
267
|
+
serveJSON(res, data);
|
268
|
+
}
|
269
|
+
|
270
|
+
function autoSavePurge(res, sp) {
|
271
|
+
// Deletes specified file(s) (if any) as well as all expired files,
|
272
|
+
// and returns list with data on remaining files as JSON string
|
273
|
+
const
|
274
|
+
now = new Date(),
|
275
|
+
p = sp.get('period'),
|
276
|
+
period = (p ? parseInt(p) : 24) * 3600000,
|
277
|
+
df = sp.get('to_delete'),
|
278
|
+
all = df === '/*ALL*/';
|
279
|
+
|
280
|
+
// Get list of data on Linny-R models in `autosave` directory
|
281
|
+
data = [];
|
282
|
+
try {
|
283
|
+
const flist = fs.readdirSync(WORKSPACE.autosave);
|
284
|
+
for(let i = 0; i < flist.length; i++) {
|
285
|
+
const
|
286
|
+
pp = path.parse(flist[i]),
|
287
|
+
md = {name: pp.name},
|
288
|
+
fp = path.join(WORKSPACE.autosave, flist[i]);
|
289
|
+
// NOTE: only consider Linny-R model files (extension .lnr)
|
290
|
+
if(pp.ext === '.lnr') {
|
291
|
+
let dodel = all || pp.name === df;
|
292
|
+
if(!dodel) {
|
293
|
+
// Get file properties
|
294
|
+
const fstat = fs.statSync(fp);
|
295
|
+
md.size = fstat.size;
|
296
|
+
md.date = fstat.mtime;
|
297
|
+
// Also delete if file has expired
|
298
|
+
dodel = now - fstat.mtimeMs > period;
|
299
|
+
}
|
300
|
+
if(dodel) {
|
301
|
+
// Delete model file
|
302
|
+
try {
|
303
|
+
fs.unlinkSync(fp);
|
304
|
+
} catch(err) {
|
305
|
+
console.log('WARNING: Failed to delete', fp);
|
306
|
+
console.log(err);
|
307
|
+
}
|
308
|
+
} else {
|
309
|
+
// Add model data to the list
|
310
|
+
data.push(md);
|
311
|
+
}
|
312
|
+
}
|
313
|
+
}
|
314
|
+
} catch(err) {
|
315
|
+
console.log(err);
|
316
|
+
return 'ERROR: Auto-save failed -- ' + err.message;
|
317
|
+
}
|
318
|
+
return data;
|
319
|
+
}
|
320
|
+
|
321
|
+
function autoSaveLoad(res, sp) {
|
322
|
+
// Return XML content of specified file
|
323
|
+
const fn = sp.get('file');
|
324
|
+
if(fn) {
|
325
|
+
const fp = path.join(WORKSPACE.autosave, fn + '.lnr');
|
326
|
+
try {
|
327
|
+
data = fs.readFileSync(fp, 'utf8');
|
328
|
+
} catch(err) {
|
329
|
+
console.log(err);
|
330
|
+
data = 'WARNING: Failed to load auto-saved file: ' + err.message;
|
331
|
+
}
|
332
|
+
} else {
|
333
|
+
data = 'ERROR: No auto-saved file name';
|
334
|
+
}
|
335
|
+
servePlainText(res, data);
|
336
|
+
}
|
337
|
+
|
338
|
+
function autoSaveStore(res, sp) {
|
339
|
+
// Stores XML data under specified file name in the auto-save directory
|
340
|
+
let data = 'OK';
|
341
|
+
const fn = sp.get('file');
|
342
|
+
if(!fn) {
|
343
|
+
data = 'WARNING: No name for file to auto-save';
|
344
|
+
} else {
|
345
|
+
const xml = sp.get('xml');
|
346
|
+
// Validate XML as a Linny-R model
|
347
|
+
try {
|
348
|
+
const
|
349
|
+
parser = new DOMParser(),
|
350
|
+
doc = parser.parseFromString(xml, 'text/xml');
|
351
|
+
root = doc.documentElement;
|
352
|
+
// Linny-R models have a model element as root
|
353
|
+
if(root.nodeName !== 'model') throw 'XML document has no model element';
|
354
|
+
fs.writeFileSync(path.join(WORKSPACE.autosave, fn + '.lnr'), xml);
|
355
|
+
} catch(err) {
|
356
|
+
console.log(err);
|
357
|
+
data = 'ERROR: Not a Linny-R model to auto-save';
|
358
|
+
}
|
359
|
+
}
|
360
|
+
servePlainText(res, data);
|
361
|
+
}
|
362
|
+
|
222
363
|
// Repository functionality
|
223
364
|
// ========================
|
224
365
|
// For repository services, the Linny-R JavaScript application communicates with
|
225
|
-
// the server via calls to the server like
|
366
|
+
// the server via calls to the server like fetch('repo/', x) where x is a JSON
|
226
367
|
// object with at least the entry `action`, which can be one of the following:
|
227
368
|
// id return the repository URL (for this script: 'local host')
|
228
369
|
// list return list with names of repositories available on the server
|
@@ -251,14 +392,7 @@ function repo(res, sp) {
|
|
251
392
|
if(action === 'store') return repoStore(res, repo, file, sp.get('xml'));
|
252
393
|
if(action === 'delete') return repoDelete(res, repo, file);
|
253
394
|
// Fall-through: report error
|
254
|
-
servePlainText(res, `ERROR: Invalid action: "${action}"`);
|
255
|
-
}
|
256
|
-
|
257
|
-
function asFileName(s) {
|
258
|
-
// Returns string `s` with whitespace converted to a single dash, and special
|
259
|
-
// characters converted to underscores
|
260
|
-
s = s.trim().replace(/[\s\-]+/g, '-');
|
261
|
-
return s.replace(/[^A-Za-z0-9_\-]/g, '_');
|
395
|
+
servePlainText(res, `ERROR: Invalid repository action: "${action}"`);
|
262
396
|
}
|
263
397
|
|
264
398
|
function repositoryByName(name) {
|
@@ -601,7 +735,7 @@ function repoStore(res, rname, mname, mxml) {
|
|
601
735
|
parser = new DOMParser(),
|
602
736
|
doc = parser.parseFromString(mxml, 'text/xml');
|
603
737
|
root = doc.documentElement;
|
604
|
-
// Linny-R
|
738
|
+
// Linny-R models have a model element as root
|
605
739
|
if(root.nodeName !== 'model') throw 'XML document has no model element';
|
606
740
|
valid = true;
|
607
741
|
} catch(err) {
|
@@ -1019,6 +1153,8 @@ function processRequest(req, res, cmd, data) {
|
|
1019
1153
|
SERVER.close();
|
1020
1154
|
} else if(cmd === '/auto-check') {
|
1021
1155
|
autoCheck(res);
|
1156
|
+
} else if(cmd === '/autosave/') {
|
1157
|
+
autoSave(res, new URLSearchParams(data));
|
1022
1158
|
} else if(cmd === '/repo/') {
|
1023
1159
|
repo(res, new URLSearchParams(data));
|
1024
1160
|
} else if(cmd === '/load-data/') {
|
@@ -1389,6 +1525,7 @@ function createWorkspace() {
|
|
1389
1525
|
}
|
1390
1526
|
// Define the sub-directory paths
|
1391
1527
|
const ws = {
|
1528
|
+
autosave: path.join(SETTINGS.user_dir, 'autosave'),
|
1392
1529
|
channel: path.join(SETTINGS.user_dir, 'channel'),
|
1393
1530
|
callback: path.join(SETTINGS.user_dir, 'callback'),
|
1394
1531
|
diagrams: path.join(SETTINGS.user_dir, 'diagrams'),
|
package/static/index.html
CHANGED
@@ -58,9 +58,10 @@ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
|
58
58
|
var
|
59
59
|
// NODE = false indicates that modules need not export their properties
|
60
60
|
NODE = false,
|
61
|
-
// Version number
|
61
|
+
// Version number
|
62
62
|
LINNY_R_VERSION = '0',
|
63
|
-
|
63
|
+
// GitHub repository
|
64
|
+
GITHUB_REPOSITORY = 'https://github.com/pwgbots/linny-r',
|
64
65
|
// Linny-R server hosting public channels
|
65
66
|
PUBLIC_LINNY_R_URL = 'https://sysmod.tbm.tudelft.nl/linny-r',
|
66
67
|
// Create the XML parser
|
@@ -151,16 +152,28 @@ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
|
151
152
|
const info = data.split('|');
|
152
153
|
if(info.length > 1) {
|
153
154
|
LINNY_R_VERSION = info[0];
|
155
|
+
const v = 'Version ' + LINNY_R_VERSION;
|
156
|
+
// Update the "home page" of the documentation manager
|
157
|
+
DOCUMENTATION_MANAGER.about_linny_r =
|
158
|
+
DOCUMENTATION_MANAGER.about_linny_r.replace(
|
159
|
+
'[LINNY_R_VERSION]', v);
|
160
|
+
// Update the version number in the browser's upper left corner
|
161
|
+
document.getElementById('linny-r-version-number').innerHTML = v;
|
154
162
|
if(info[1] !== 'up-to-date') {
|
163
|
+
// Inform user that newer version exists
|
155
164
|
UI.check_update_modal.element('msg').innerHTML = [
|
156
|
-
'<a href="
|
165
|
+
'<a href="', GITHUB_REPOSITORY,
|
166
|
+
'/wiki/Linny-R-version-history" ',
|
167
|
+
'title="Click to view version release notes" ',
|
157
168
|
'target="_blank">Version <strong>',
|
158
|
-
info[1], '</strong></a> released on ',
|
169
|
+
info[1], '</strong></a> released on ',
|
170
|
+
info[2].substring(0, 21),
|
159
171
|
' can be installed.'].join('');
|
160
172
|
UI.check_update_modal.show();
|
161
173
|
UI.check_update_modal.element('buttons').style.display = 'block';
|
162
174
|
}
|
163
175
|
} else {
|
176
|
+
// Invalid server response (should not occur, but just in case)
|
164
177
|
UI.warn('Version check failed: "' + data + '"');
|
165
178
|
}
|
166
179
|
})
|
package/static/linny-r.css
CHANGED
@@ -705,7 +705,7 @@ img.del-asm-btn:hover {
|
|
705
705
|
|
706
706
|
#auto-save-settings {
|
707
707
|
position: absolute;
|
708
|
-
bottom:
|
708
|
+
bottom: 3px;
|
709
709
|
left: 2px;
|
710
710
|
width: calc(100% - 4px);
|
711
711
|
}
|
@@ -4319,8 +4319,8 @@ div.call-stack-expr {
|
|
4319
4319
|
|
4320
4320
|
/* the CHECK UPDATE modal asks for permission to update the software */
|
4321
4321
|
#check-update-dlg {
|
4322
|
-
width:
|
4323
|
-
height:
|
4322
|
+
width: 500px;
|
4323
|
+
height: min-content;
|
4324
4324
|
}
|
4325
4325
|
|
4326
4326
|
#confirm-delete-from-repo-msg,
|
@@ -4334,20 +4334,21 @@ div.call-stack-expr {
|
|
4334
4334
|
#confirm-move-buttons,
|
4335
4335
|
#confirm-delete-from-repo-buttons,
|
4336
4336
|
#check-update-buttons {
|
4337
|
-
width:
|
4337
|
+
width: 100%;
|
4338
4338
|
height: 23px;
|
4339
|
-
|
4339
|
+
padding-top: 3px;
|
4340
|
+
font-size: 14px;
|
4341
|
+
white-space: nowrap;
|
4340
4342
|
}
|
4341
4343
|
|
4342
4344
|
#confirm-move-buttons > img,
|
4343
4345
|
#confirm-delete-from-repo-buttons > img,
|
4344
|
-
#check-update-buttons > img
|
4345
|
-
#check-update-restart-btn > img,
|
4346
|
-
#check-update-reload-btn > img {
|
4346
|
+
#check-update-buttons > img {
|
4347
4347
|
float: none;
|
4348
4348
|
height: 23px;
|
4349
4349
|
width: 23px;
|
4350
4350
|
vertical-align: middle;
|
4351
|
+
padding-bottom: 3px;
|
4351
4352
|
}
|
4352
4353
|
|
4353
4354
|
/* Linny-R logo icon animation in 60 frames */
|
@@ -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
|
}
|
@@ -4823,7 +4824,7 @@ class GUIController extends Controller {
|
|
4823
4824
|
UNDO_STACK.clear();
|
4824
4825
|
VM.reset();
|
4825
4826
|
this.updateButtons();
|
4826
|
-
AUTO_SAVE.
|
4827
|
+
AUTO_SAVE.setAutoSaveInterval();
|
4827
4828
|
}
|
4828
4829
|
|
4829
4830
|
addNode(type) {
|
@@ -6059,8 +6060,8 @@ class GUIMonitor {
|
|
6059
6060
|
|
6060
6061
|
|
6061
6062
|
// CLASS GUIFileManager provides the GUI for loading and saving models and
|
6062
|
-
// diagrams and handles
|
6063
|
-
//
|
6063
|
+
// diagrams and handles the interaction with the MILP solver via POST requests
|
6064
|
+
// to the server.
|
6064
6065
|
// NOTE: because the console-only monitor requires Node.js modules, this
|
6065
6066
|
// GUI class does NOT extend its console-only counterpart
|
6066
6067
|
class GUIFileManager {
|
@@ -6184,15 +6185,10 @@ class GUIFileManager {
|
|
6184
6185
|
// Show "Load model" modal
|
6185
6186
|
// @@TO DO: warn user if unsaved changes to current model
|
6186
6187
|
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
|
-
}
|
6188
|
+
// Update auto-saved model list; if not empty, this will display the
|
6189
|
+
// "restore autosaved files" button
|
6190
|
+
AUTO_SAVE.getAutoSavedModels();
|
6191
|
+
// Show the "Load model" dialog
|
6196
6192
|
UI.modals.load.show();
|
6197
6193
|
}
|
6198
6194
|
|
@@ -6282,7 +6278,7 @@ class GUIFileManager {
|
|
6282
6278
|
console.log('Encoded file size:', el.href.length);
|
6283
6279
|
el.download = 'model.lnr';
|
6284
6280
|
if(el.href.length > 25*1024*1024 &&
|
6285
|
-
navigator.userAgent.search('Chrome'
|
6281
|
+
navigator.userAgent.search('Chrome') <= 0) {
|
6286
6282
|
UI.notify('Model file size exceeds 25 MB. ' +
|
6287
6283
|
'If it does not download, store it in a repository');
|
6288
6284
|
}
|
@@ -6317,47 +6313,55 @@ class GUIFileManager {
|
|
6317
6313
|
console.log(err);
|
6318
6314
|
});
|
6319
6315
|
}
|
6320
|
-
|
6321
|
-
|
6322
|
-
|
6323
|
-
|
6324
|
-
|
6325
|
-
|
6326
|
-
|
6327
|
-
|
6328
|
-
|
6329
|
-
|
6316
|
+
|
6317
|
+
loadAutoSavedModel(name) {
|
6318
|
+
fetch('autosave/', postData({
|
6319
|
+
action: 'load',
|
6320
|
+
file: name
|
6321
|
+
}))
|
6322
|
+
.then((response) => {
|
6323
|
+
if(!response.ok) {
|
6324
|
+
UI.alert(`ERROR ${response.status}: ${response.statusText}`);
|
6325
|
+
}
|
6326
|
+
return response.text();
|
6327
|
+
})
|
6328
|
+
.then((data) => {
|
6329
|
+
if(UI.postResponseOK(data)) UI.loadModelFromXML(data);
|
6330
|
+
})
|
6331
|
+
.catch((err) => UI.warn(UI.WARNING.NO_CONNECTION, err));
|
6332
|
+
}
|
6333
|
+
|
6334
|
+
storeAutoSavedModel() {
|
6335
|
+
// Stores the current model in the local auto-save directory
|
6336
|
+
const bcl = document.getElementById('autosave-btn').classList;
|
6330
6337
|
if(MODEL.running_experiment) {
|
6331
6338
|
console.log('No autosaving while running an experiment');
|
6339
|
+
bcl.remove('stay-activ');
|
6332
6340
|
return;
|
6333
6341
|
}
|
6334
|
-
|
6335
|
-
|
6336
|
-
|
6337
|
-
|
6338
|
-
|
6339
|
-
|
6340
|
-
|
6341
|
-
|
6342
|
-
|
6343
|
-
|
6344
|
-
|
6345
|
-
|
6342
|
+
fetch('autosave/', postData({
|
6343
|
+
action: 'store',
|
6344
|
+
file: REPOSITORY_BROWSER.asFileName(
|
6345
|
+
(MODEL.name || 'no-name') + '_by_' +
|
6346
|
+
(MODEL.author || 'no-author')),
|
6347
|
+
xml: MODEL.asXML
|
6348
|
+
}))
|
6349
|
+
.then((response) => {
|
6350
|
+
if(!response.ok) {
|
6351
|
+
UI.alert(`ERROR ${response.status}: ${response.statusText}`);
|
6352
|
+
}
|
6353
|
+
return response.text();
|
6354
|
+
})
|
6355
|
+
.then((data) => {
|
6356
|
+
UI.postResponseOK(data);
|
6357
|
+
bcl.remove('stay-activ');
|
6358
|
+
})
|
6359
|
+
.catch((err) => {
|
6360
|
+
UI.warn(UI.WARNING.NO_CONNECTION, err);
|
6361
|
+
bcl.remove('stay-activ');
|
6362
|
+
});
|
6346
6363
|
}
|
6347
6364
|
|
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
6365
|
renderDiagramAsPNG() {
|
6362
6366
|
localStorage.removeItem('png-url');
|
6363
6367
|
UI.paper.fitToSize();
|
@@ -6811,27 +6815,28 @@ NOTE: Grouping groups results in a single group, e.g., (1;2);(3;4;5) evaluates a
|
|
6811
6815
|
} // END of class ExpressionEditor
|
6812
6816
|
|
6813
6817
|
|
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
|
6818
|
+
// CLASS ModelAutoSaver automatically saves the current model at regular
|
6819
|
+
// time intervals in the user's `autosave` directory
|
6819
6820
|
class ModelAutoSaver {
|
6820
6821
|
constructor() {
|
6821
6822
|
// Keep track of time-out interval of auto-saving feature
|
6822
6823
|
this.timeout_id = 0;
|
6823
|
-
this.time_prefix = '_A_S_T_$';
|
6824
6824
|
this.interval = 10; // auto-save every 10 minutes
|
6825
6825
|
this.period = 24; // delete models older than 24 hours
|
6826
|
+
this.model_list = [];
|
6826
6827
|
// Overwite defaults if settings still in local storage of browser
|
6827
|
-
this.
|
6828
|
-
|
6828
|
+
this.getSettings();
|
6829
|
+
// Purge files that have "expired"
|
6830
|
+
this.getAutoSavedModels();
|
6831
|
+
// Start the interval timer
|
6832
|
+
this.setAutoSaveInterval();
|
6829
6833
|
// Add listeners to GUI elements
|
6830
6834
|
this.confirm_dialog = document.getElementById('confirm-remove-models');
|
6831
6835
|
document.getElementById('auto-save-clear-btn').addEventListener('click',
|
6832
6836
|
() => AUTO_SAVE.confirm_dialog.style.display = 'block');
|
6833
6837
|
document.getElementById('autosave-do-remove').addEventListener('click',
|
6834
|
-
|
6838
|
+
// NOTE: file name parameter /*ALL*/ indicates: delete all
|
6839
|
+
() => AUTO_SAVE.getAutoSavedModels(true, '/*ALL*/'));
|
6835
6840
|
document.getElementById('autosave-cancel').addEventListener('click',
|
6836
6841
|
() => AUTO_SAVE.confirm_dialog.style.display = 'none');
|
6837
6842
|
document.getElementById('restore-cancel').addEventListener('click',
|
@@ -6850,14 +6855,14 @@ class ModelAutoSaver {
|
|
6850
6855
|
m = parseFloat(mh[0]),
|
6851
6856
|
h = parseFloat(mh[1]);
|
6852
6857
|
if(isNaN(m) || isNaN(h)) {
|
6853
|
-
UI.warn('
|
6858
|
+
UI.warn('Ignored invalid local auto-save settings');
|
6854
6859
|
} else {
|
6855
6860
|
this.interval = m;
|
6856
6861
|
this.period = h;
|
6857
6862
|
}
|
6858
6863
|
}
|
6859
6864
|
} catch(err) {
|
6860
|
-
console.log('
|
6865
|
+
console.log('Local storage failed:', err);
|
6861
6866
|
}
|
6862
6867
|
}
|
6863
6868
|
|
@@ -6866,8 +6871,10 @@ class ModelAutoSaver {
|
|
6866
6871
|
try {
|
6867
6872
|
window.localStorage.setItem('Linny-R-autosave',
|
6868
6873
|
this.interval + '|' + this.period);
|
6874
|
+
UI.notify('New auto-save settings stored in browser');
|
6869
6875
|
} catch(err) {
|
6870
6876
|
UI.warn('Failed to write auto-save settings to local storage');
|
6877
|
+
console.log(err);
|
6871
6878
|
}
|
6872
6879
|
}
|
6873
6880
|
|
@@ -6875,128 +6882,80 @@ class ModelAutoSaver {
|
|
6875
6882
|
document.getElementById('autosave-btn').classList.add('stay-activ');
|
6876
6883
|
// Use setTimeout to let browser always briefly show the active color
|
6877
6884
|
// even when the model file is small and storing hardly takes time
|
6878
|
-
setTimeout(() => FILE_MANAGER.
|
6885
|
+
setTimeout(() => FILE_MANAGER.storeAutoSavedModel(), 300);
|
6879
6886
|
}
|
6880
6887
|
|
6881
|
-
|
6888
|
+
setAutoSaveInterval() {
|
6882
6889
|
// Activate the auto-save feature (if interval is configured)
|
6883
6890
|
if(this.timeout_id) clearInterval(this.timeout_id);
|
6884
|
-
|
6891
|
+
// NOTE: interval = 0 indicates "do not auto-save"
|
6885
6892
|
if(this.interval) {
|
6886
6893
|
// Interval is in minutes, so multiply by 60 thousand to get msec
|
6887
6894
|
this.timeout_id = setInterval(
|
6888
6895
|
() => AUTO_SAVE.saveModel(), this.interval * 60000);
|
6889
6896
|
}
|
6890
6897
|
}
|
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);
|
6898
|
+
|
6899
|
+
getAutoSavedModels(show_dialog=false, file_to_delete='') {
|
6900
|
+
// Get list of auto-saved models from server (after deleting those that
|
6901
|
+
// have been stored beyond the set period AND the specified file to
|
6902
|
+
// delete (where /*ALL*/ indicates "delete all auto-saved files")
|
6903
|
+
const pd = {action: 'purge', period: this.period};
|
6904
|
+
if(file_to_delete) pd.to_delete = file_to_delete;
|
6905
|
+
fetch('autosave/', postData(pd))
|
6906
|
+
.then((response) => {
|
6907
|
+
if(!response.ok) {
|
6908
|
+
UI.alert(`ERROR ${response.status}: ${response.statusText}`);
|
6906
6909
|
}
|
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);
|
6910
|
+
return response.text();
|
6911
|
+
})
|
6912
|
+
.then((data) => {
|
6913
|
+
if(UI.postResponseOK(data)) {
|
6914
|
+
try {
|
6915
|
+
AUTO_SAVE.model_list = JSON.parse(data);
|
6916
|
+
} catch(err) {
|
6917
|
+
AUTO_SAVE.model_list = [];
|
6918
|
+
UI.warn('Data on auto-saved models is not valid');
|
6919
|
+
}
|
6937
6920
|
}
|
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
|
-
}
|
6921
|
+
// Update auto-save-related dialog elements
|
6922
|
+
const
|
6923
|
+
n = this.model_list.length,
|
6924
|
+
ttl = pluralS(n, 'auto-saved model'),
|
6925
|
+
rbtn = document.getElementById('load-autosaved-btn');
|
6926
|
+
document.getElementById('autosave-btn').title = ttl;
|
6927
|
+
rbtn.title = ttl;
|
6928
|
+
rbtn.style.display = (n > 0 ? 'block' : 'none');
|
6929
|
+
if(show_dialog) AUTO_SAVE.showRestoreDialog();
|
6930
|
+
})
|
6931
|
+
.catch((err) => {console.log(err); UI.warn(UI.WARNING.NO_CONNECTION, err);});
|
6977
6932
|
}
|
6978
|
-
|
6933
|
+
|
6979
6934
|
showRestoreDialog() {
|
6980
6935
|
// Shows list of auto-saved models; clicking on one will load it
|
6981
|
-
|
6936
|
+
// NOTE: hide "Load model" dialog in case it was showing
|
6982
6937
|
document.getElementById('load-modal').style.display = 'none';
|
6983
6938
|
// Contruct the table to select from
|
6984
6939
|
let html = '';
|
6985
|
-
for(let i = 0; i <
|
6986
|
-
const
|
6940
|
+
for(let i = 0; i < this.model_list.length; i++) {
|
6941
|
+
const
|
6942
|
+
m = this.model_list[i],
|
6943
|
+
bytes = UI.sizeInBytes(m.size).split(' ');
|
6987
6944
|
html += ['<tr class="dataset" style="color: gray" ',
|
6988
|
-
'onclick="FILE_MANAGER.
|
6989
|
-
|
6990
|
-
|
6991
|
-
|
6992
|
-
'</td><td><
|
6993
|
-
'
|
6994
|
-
|
6945
|
+
'onclick="FILE_MANAGER.loadAutoSavedModel(\'',
|
6946
|
+
m.name,'\');"><td class="restore-name">', m.name, '</td><td>',
|
6947
|
+
m.date.substring(1, 16).replace('T', ' '),
|
6948
|
+
'</td><td style="text-align: right">',
|
6949
|
+
bytes[0], '</td><td>', bytes[1], '</td><td style="width:15px">',
|
6950
|
+
'<img class="del-asm-btn" src="images/delete.png" ',
|
6951
|
+
'onclick="event.stopPropagation(); ',
|
6952
|
+
'AUTO_SAVE.getAutoSavedModels(true, \'', m.name,
|
6953
|
+
'\')"></td></tr>'].join('');
|
6995
6954
|
}
|
6996
6955
|
document.getElementById('restore-table').innerHTML = html;
|
6997
6956
|
// Adjust dialog height (max-height will limit list to 10 lines)
|
6998
6957
|
document.getElementById('restore-dlg').style.height =
|
6999
|
-
(
|
6958
|
+
(48 + 19 * this.model_list.length) + 'px';
|
7000
6959
|
document.getElementById('confirm-remove-models').style.display = 'none';
|
7001
6960
|
// Fill text input fields with present settings
|
7002
6961
|
document.getElementById('auto-save-minutes').value = this.interval;
|
@@ -7006,7 +6965,7 @@ class ModelAutoSaver {
|
|
7006
6965
|
ttl = document.getElementById('restore-dlg-title'),
|
7007
6966
|
sa = document.getElementById('restore-scroll-area'),
|
7008
6967
|
btn = document.getElementById('auto-save-clear-btn');
|
7009
|
-
if(
|
6968
|
+
if(this.model_list.length) {
|
7010
6969
|
ttl.innerHTML = 'Restore auto-saved model';
|
7011
6970
|
sa.style.display = 'block';
|
7012
6971
|
btn.style.display = 'block';
|
@@ -7037,10 +6996,10 @@ class ModelAutoSaver {
|
|
7037
6996
|
if(!isNaN(h)) {
|
7038
6997
|
// If valid, store in local storage of browser
|
7039
6998
|
if(m !== this.interval || h !== this.period) {
|
7040
|
-
UI.notify('New auto-save settings stored in browser');
|
7041
6999
|
this.interval = m;
|
7042
7000
|
this.period = h;
|
7043
7001
|
this.setSettings();
|
7002
|
+
this.setAutoSaveInterval();
|
7044
7003
|
}
|
7045
7004
|
document.getElementById('restore-modal').style.display = 'none';
|
7046
7005
|
return;
|
@@ -8188,7 +8147,6 @@ class Repository {
|
|
8188
8147
|
|
8189
8148
|
} // END of class Repository
|
8190
8149
|
|
8191
|
-
|
8192
8150
|
//
|
8193
8151
|
// Draggable & resizable dialogs
|
8194
8152
|
//
|
@@ -12873,7 +12831,7 @@ class DocumentationManager {
|
|
12873
12831
|
<img src="images/logo.png" style="height:25px; margin-right: 8px">
|
12874
12832
|
<div style="display: inline-block; min-height: 20px;
|
12875
12833
|
vertical-align: top; padding-top: 8px">
|
12876
|
-
|
12834
|
+
[LINNY_R_VERSION]
|
12877
12835
|
</div>
|
12878
12836
|
</div>
|
12879
12837
|
<div style="font-family: serif; font-size: 12px">
|
@@ -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
|
|