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 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, and a graphical user interface (GUI) that runs in any modern browser.
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
- You can play with the most recent release of Linny-R on
16
- <a href="https://sysmod.tbm.tudelft.nl/linny-r" target="_blank">this server hosted at TU Delft</a>.
17
- Note that this server imposes restrictions on solver time and the total number of blocks it will solve per run.
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
- Documentation for Linny-R is still scant, but it is growing. You can contribute yourself (in "wiki fashion")
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.10.0.
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.10.0.
36
+ The response should be the version number of Node.js, for example: v18.11.0.
40
37
 
41
- ### Installing Linny-R
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
- ### Configuring the MILP solver
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.8
172
- Node.js version: v18.10.0
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
- #### User workspace
211
-
212
- The user workspace is created when the server is run for the first time.
213
- The sub-directories of this directory `user` are used by Linny-R to store files.
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
- #### Command line options
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
- ### Installing Inkscape
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
- ### Click-start for Linny-R
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
- ### Troubleshooting problems
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 documentatio website:
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 Web Developer Tools option via their application menu.
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "linny-r",
3
- "version": "1.1.8",
3
+ "version": "1.1.10",
4
4
  "description": "Executable graphical language with WYSIWYG editor for MILP models",
5
5
  "main": "server.js",
6
6
  "scripts": {
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 ${SETTINGS.cli_name} window
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>&nbsp;&nbsp;<tt>npm update linny-r</tt><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>&nbsp;&nbsp;<tt>node node_modules\\linny-r\\server</tt></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 $.post('repo', x) where x is a JSON
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 model have a model element as root
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 and release date
61
+ // Version number
62
62
  LINNY_R_VERSION = '0',
63
- VERSION_RELEASE_DATE = 0,
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="https://sysmod.tbm.tudelft.nl/linny-r/docs/?15" ',
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 ', info[2],
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
  })
@@ -705,7 +705,7 @@ img.del-asm-btn:hover {
705
705
 
706
706
  #auto-save-settings {
707
707
  position: absolute;
708
- bottom: 2px;
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: 245px;
4323
- height: 150px;
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: calc(100% - 8px);
4337
+ width: 100%;
4338
4338
  height: 23px;
4339
- font-size: 16px;
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, ' ').replace(/\||\\/g, ' '
281
- ).trim().replace(/\s\s+/g, ' ');
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().replace(/[\s\-]+/g, '-'
523
- ).replace(/[^A-Za-z0-9_\-]/g, '_').trim('-_');
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
- () => AUTO_SAVE.showRestoreDialog());
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.checkForSavedModels());
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.setInterval();
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.setInterval();
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
- // the interaction with the MILP solver via POST requests to the server.
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
- const
6188
- rbtn = document.getElementById('load-autosaved-btn'),
6189
- ml = AUTO_SAVE.savedModelList();
6190
- if(ml.length > 0) {
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' ) <= 0) {
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
- saveToLocalStorage() {
6322
- // Store model identified by an identifier based on author name and
6323
- // model name
6324
- // (1) autosave is skipped while experiment is running, as it may interfere
6325
- // with storing run results
6326
- // (2) action will overwrite earlier auto-saved version of this model
6327
- // unless its name and/or author have been changed in the meantime
6328
- // (3) browser may be configured to prohibit local storage function,
6329
- // and local storage space is limited (by browser settings)
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
- try {
6335
- // Store model XML string using its display name as key
6336
- const n = MODEL.displayName;
6337
- console.log('Autosaving', n);
6338
- window.localStorage.setItem(n, MODEL.asXML);
6339
- // Also store the timestamp for this operation
6340
- window.localStorage.setItem(AUTO_SAVE.time_prefix + n, Date.now());
6341
- // Remove the highlighting of the icon on the status bar
6342
- document.getElementById('autosave-btn').classList.remove('stay-activ');
6343
- } catch(err) {
6344
- UI.alert(`Failed to auto-save model: ${err}`);
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 time
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.purgeSavedModels();
6828
- this.setInterval();
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
- () => AUTO_SAVE.removeSavedModels());
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('Invalid local auto-save settings');
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('No auto-save:', err);
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.saveToLocalStorage(), 300);
6885
+ setTimeout(() => FILE_MANAGER.storeAutoSavedModel(), 300);
6879
6886
  }
6880
6887
 
6881
- setInterval() {
6888
+ setAutoSaveInterval() {
6882
6889
  // Activate the auto-save feature (if interval is configured)
6883
6890
  if(this.timeout_id) clearInterval(this.timeout_id);
6884
- this.getSettings();
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
- purgeSavedModels() {
6893
- // Remove all autosaved models that have been stored beyond the set period
6894
- try {
6895
- for(let key in window.localStorage) {
6896
- if(key.startsWith(this.time_prefix)) {
6897
- const
6898
- name = key.split(this.time_prefix)[1],
6899
- ts = parseInt(window.localStorage.getItem(key)),
6900
- now = Date.now();
6901
- if((now - ts) / 3600000 > this.period) {
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
- } catch(err) {
6910
- console.log('No auto-save:', err);
6911
- }
6912
- }
6913
-
6914
- savedModelList() {
6915
- // Returns autosaved models as array of tuples [model name, time, file size]
6916
- // First purge outdated auto-saved models
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
- // NOTE: sort models in reverse time order (most recent on top)
6941
- list.sort((a, b) => {
6942
- if(a[1] > b[1]) return -1;
6943
- if(a[1] < b[1]) return 1;
6944
- return 0;
6945
- });
6946
- } catch(err) {
6947
- console.log('No auto-save:', err);
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
- const ml = this.savedModelList();
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 < ml.length; i++) {
6986
- const bytes = ml[i][2].split(' ');
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.loadFromLocalStorage(\'',
6989
- ml[i][0],'\');"><td class="restore-name">', ml[i][0], '</td><td>',
6990
- ml[i][1], '</td><td style="text-align: right">',
6991
- bytes[0], '</td><td>', bytes[1],
6992
- '</td><td><img class="del-asm-btn" src="images/delete.png" ',
6993
- 'onclick="AUTO_SAVE.deleteSavedModel(\'',
6994
- ml[i][0], '\')"></td></tr>'].join('');
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
- (45 + 19 * ml.length) + 'px';
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(ml.length) {
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
- ${LINNY_R_VERSION}
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