linny-r 1.4.3 → 1.4.5

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.
Files changed (49) hide show
  1. package/README.md +102 -48
  2. package/package.json +1 -1
  3. package/server.js +31 -6
  4. package/static/images/check-off-not-same-changed.png +0 -0
  5. package/static/images/check-off-not-same-not-changed.png +0 -0
  6. package/static/images/check-off-same-changed.png +0 -0
  7. package/static/images/check-off-same-not-changed.png +0 -0
  8. package/static/images/check-on-not-same-changed.png +0 -0
  9. package/static/images/check-on-not-same-not-changed.png +0 -0
  10. package/static/images/check-on-same-changed.png +0 -0
  11. package/static/images/check-on-same-not-changed.png +0 -0
  12. package/static/images/eq-not-same-changed.png +0 -0
  13. package/static/images/eq-not-same-not-changed.png +0 -0
  14. package/static/images/eq-same-changed.png +0 -0
  15. package/static/images/eq-same-not-changed.png +0 -0
  16. package/static/images/ne-not-same-changed.png +0 -0
  17. package/static/images/ne-not-same-not-changed.png +0 -0
  18. package/static/images/ne-same-changed.png +0 -0
  19. package/static/images/ne-same-not-changed.png +0 -0
  20. package/static/images/sort-asc-lead.png +0 -0
  21. package/static/images/sort-asc.png +0 -0
  22. package/static/images/sort-desc-lead.png +0 -0
  23. package/static/images/sort-desc.png +0 -0
  24. package/static/images/sort-not.png +0 -0
  25. package/static/index.html +51 -35
  26. package/static/linny-r.css +167 -53
  27. package/static/scripts/linny-r-gui-actor-manager.js +340 -0
  28. package/static/scripts/linny-r-gui-chart-manager.js +944 -0
  29. package/static/scripts/linny-r-gui-constraint-editor.js +681 -0
  30. package/static/scripts/linny-r-gui-controller.js +4005 -0
  31. package/static/scripts/linny-r-gui-dataset-manager.js +1176 -0
  32. package/static/scripts/linny-r-gui-documentation-manager.js +739 -0
  33. package/static/scripts/linny-r-gui-equation-manager.js +307 -0
  34. package/static/scripts/linny-r-gui-experiment-manager.js +1944 -0
  35. package/static/scripts/linny-r-gui-expression-editor.js +450 -0
  36. package/static/scripts/linny-r-gui-file-manager.js +392 -0
  37. package/static/scripts/linny-r-gui-finder.js +727 -0
  38. package/static/scripts/linny-r-gui-model-autosaver.js +230 -0
  39. package/static/scripts/linny-r-gui-monitor.js +448 -0
  40. package/static/scripts/linny-r-gui-paper.js +2789 -0
  41. package/static/scripts/linny-r-gui-receiver.js +323 -0
  42. package/static/scripts/linny-r-gui-repository-browser.js +819 -0
  43. package/static/scripts/linny-r-gui-scale-unit-manager.js +244 -0
  44. package/static/scripts/linny-r-gui-sensitivity-analysis.js +778 -0
  45. package/static/scripts/linny-r-gui-undo-redo.js +560 -0
  46. package/static/scripts/linny-r-model.js +34 -15
  47. package/static/scripts/linny-r-utils.js +11 -1
  48. package/static/scripts/linny-r-vm.js +21 -12
  49. package/static/scripts/linny-r-gui.js +0 -16908
@@ -0,0 +1,392 @@
1
+ /*
2
+ Linny-R is an executable graphical specification language for (mixed integer)
3
+ linear programming (MILP) problems, especially unit commitment problems (UCP).
4
+ The Linny-R language and tool have been developed by Pieter Bots at Delft
5
+ University of Technology, starting in 2009. The project to develop a browser-
6
+ based version started in 2017. See https://linny-r.org for more information.
7
+
8
+ This JavaScript file (linny-r-gui-file-manager.js) provides the GUI
9
+ functionality for the Linny-R File Manager.
10
+
11
+ */
12
+
13
+ /*
14
+ Copyright (c) 2017-2023 Delft University of Technology
15
+
16
+ Permission is hereby granted, free of charge, to any person obtaining a copy
17
+ of this software and associated documentation files (the "Software"), to deal
18
+ in the Software without restriction, including without limitation the rights to
19
+ use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
20
+ of the Software, and to permit persons to whom the Software is furnished to do
21
+ so, subject to the following conditions:
22
+
23
+ The above copyright notice and this permission notice shall be included in
24
+ all copies or substantial portions of the Software.
25
+
26
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
27
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
28
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
29
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
30
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
31
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
32
+ SOFTWARE.
33
+ */
34
+
35
+ // CLASS GUIFileManager provides the GUI for loading and saving models and
36
+ // diagrams and handles the interaction with the MILP solver via POST requests
37
+ // to the server.
38
+ // NOTE: Because the console-only monitor requires Node.js modules, this
39
+ // GUI class does NOT extend its console-only counterpart.
40
+
41
+
42
+ // CLASS GUIFileManager
43
+ class GUIFileManager {
44
+
45
+ // NOTE: The modal dialogs related to loading and saving a model file
46
+ // are properties of the GUIController because they are activated by
47
+ // buttons on the top menu.
48
+
49
+ getRemoteData(dataset, url) {
50
+ // Gets data from a URL, or from a file on the local host
51
+ if(url === '') return;
52
+ // NOTE: add this dataset to the "loading" list...
53
+ addDistinct(dataset, MODEL.loading_datasets);
54
+ // ... and allow for 3 more seconds (6 times 500 ms) to complete
55
+ MODEL.max_time_to_load += 6;
56
+ // Send the "load data" request to the server
57
+ fetch('load-data/', postData({'url': url}))
58
+ .then((response) => {
59
+ if(!response.ok) {
60
+ UI.alert(`ERROR ${response.status}: ${response.statusText}`);
61
+ }
62
+ return response.text();
63
+ })
64
+ .then((data) => {
65
+ if(data !== '' && UI.postResponseOK(data)) {
66
+ // Server must return either semicolon-separated or
67
+ // newline-separated string of numbers
68
+ if(data.indexOf(';') < 0) {
69
+ // If no semicolon found, replace newlines by semicolons
70
+ data = data.trim().split('\n').join(';');
71
+ }
72
+ // Remove all white space
73
+ data = data.replace(/\s+/g, '');
74
+ // Show data in text area when the SERIES dialog is visible
75
+ if(!UI.hidden('series-modal')) {
76
+ DATASET_MANAGER.series_data.value = data.split(';').join('\n');
77
+ } else {
78
+ dataset.unpackDataString(data);
79
+ }
80
+ // NOTE: remove dataset from the "loading" list
81
+ const i = MODEL.loading_datasets.indexOf(dataset);
82
+ if(i >= 0) MODEL.loading_datasets.splice(i, 1);
83
+ }
84
+ })
85
+ .catch((err) => UI.warn(UI.WARNING.NO_CONNECTION, err));
86
+ }
87
+
88
+ decryptIfNeeded(event, action) {
89
+ // Checks whether XML is encrypted; if not, processes data "as is", otherwise
90
+ // prompts for password
91
+ const data = event.target.result;
92
+ if(data.indexOf('model latch="') < 0) return action(data);
93
+ const
94
+ xml = parseXML(data),
95
+ md = UI.modals.password;
96
+ md.encrypted_msg = {
97
+ encryption: nodeContentByTag(xml, 'content'),
98
+ latch: nodeParameterValue(xml, 'latch')
99
+ };
100
+ md.post_decrypt_action = action;
101
+ md.element('action').innerHTML = 'Enter';
102
+ md.ok = UI.removeListeners(md.ok);
103
+ md.ok.addEventListener('click', () => FILE_MANAGER.startToDecrypt());
104
+ this.updateStrength();
105
+ md.show('code');
106
+ }
107
+
108
+ startToDecrypt() {
109
+ // Wrapper function to permit DOM events to occur first
110
+ const
111
+ md = UI.modals.password,
112
+ encr_msg = md.encrypted_msg,
113
+ code = md.element('code'),
114
+ password = code.value;
115
+ // NOTE: immediately clear password field
116
+ code.value = '';
117
+ md.hide();
118
+ UI.waitingCursor();
119
+ UI.setMessage('Decrypting...');
120
+ // NOTE: asynchronous function tryToDecrypt is defined in linny-r-utils.js
121
+ setTimeout((msg, pwd, ok, err) => tryToDecrypt(msg, pwd, ok, err), 5,
122
+ encr_msg, password,
123
+ // The on_ok function
124
+ (data) => {
125
+ UI.normalCursor();
126
+ const md = UI.modals.password;
127
+ if(data) md.post_decrypt_action(data);
128
+ md.encrypted_msg = null;
129
+ md.post_decrypt_action = null;
130
+ },
131
+ // The on_error function
132
+ (err) => {
133
+ console.log(err);
134
+ UI.warn('Failed to load encrypted model');
135
+ const md = UI.modals.password;
136
+ md.encrypted_msg = null;
137
+ md.post_decrypt_action = null;
138
+ });
139
+ }
140
+
141
+ readModel(event) {
142
+ // Read XML string from input file, decrypt if necessary, and then parse it
143
+ this.decryptIfNeeded(event, (data) => UI.loadModelFromXML(data));
144
+ }
145
+
146
+ loadModel() {
147
+ // Get the XML of the file selected in the Load dialog
148
+ const md = UI.modals.load;
149
+ md.hide();
150
+ try {
151
+ const file = md.element('xml-file').files[0];
152
+ if(!file) return;
153
+ if(file.name.split('.').pop() != 'lnr') {
154
+ UI.warn('Linny-R files should have extension .lnr');
155
+ }
156
+ const reader = new FileReader();
157
+ reader.onload = (event) => FILE_MANAGER.readModel(event);
158
+ reader.readAsText(file);
159
+ } catch(err) {
160
+ UI.alert('Error while reading file: ' + err);
161
+ }
162
+ }
163
+
164
+ promptToLoad() {
165
+ // Show "Load model" modal
166
+ // @@TO DO: warn user if unsaved changes to current model
167
+ UI.hideStayOnTopDialogs();
168
+ // Update auto-saved model list; if not empty, this will display the
169
+ // "restore autosaved files" button
170
+ AUTO_SAVE.getAutoSavedModels();
171
+ // Show the "Load model" dialog
172
+ UI.modals.load.show();
173
+ }
174
+
175
+ readModelToCompare(event) {
176
+ // Read model-to-compare from input file, decrypting if necessary
177
+ this.decryptIfNeeded(event,
178
+ (data) => DOCUMENTATION_MANAGER.compareModels(data));
179
+ }
180
+
181
+ loadModelToCompare() {
182
+ document.getElementById('comparison-modal').style.display = 'none';
183
+ try {
184
+ const file = document.getElementById('comparison-xml-file').files[0];
185
+ if(!file) return;
186
+ if(file.name.split('.').pop() != 'lnr') {
187
+ UI.warn('Linny-R files should have extension .lnr');
188
+ }
189
+ const reader = new FileReader();
190
+ reader.onload = (event) => FILE_MANAGER.readModelToCompare(event);
191
+ reader.readAsText(file);
192
+ } catch(err) {
193
+ UI.alert('Error while reading file: ' + err);
194
+ }
195
+ }
196
+
197
+ passwordStrength(pwd) {
198
+ if(pwd.length < CONFIGURATION.min_password_length) return 0;
199
+ let score = 1;
200
+ if(pwd.match(/[a-z]/) && pwd.match(/[A-Z]/)) score++;
201
+ if(pwd.match(/\d+/)) score++;
202
+ if(pwd.match(/.[!,@,#,$,%,^,&,*,?,_,~,-,(,)]/)) score++;
203
+ if(pwd.length > CONFIGURATION.min_password_length + 4) score++;
204
+ return score;
205
+ }
206
+
207
+ updateStrength() {
208
+ // Relects password strength in password field colors
209
+ const code = document.getElementById('password-code');
210
+ if(document.getElementById('password-action').innerHTML === 'Set') {
211
+ code.className = 'pws-' + this.passwordStrength(code.value);
212
+ } else {
213
+ code.className = '';
214
+ }
215
+ }
216
+
217
+ confirmPassword() {
218
+ const
219
+ md = UI.modals.password,
220
+ code = md.element('code');
221
+ md.encryption_code = code.value;
222
+ // NOTE: immediately clear password field
223
+ code.value = '';
224
+ if(md.encryption_code.length < CONFIGURATION.min_password_length) {
225
+ UI.warn('Password must be at least '+ CONFIGURATION.min_password_length +
226
+ ' characters long');
227
+ md.encryption_code = '';
228
+ code.focus();
229
+ return;
230
+ }
231
+ md.element('action').innerHTML = 'Confirm';
232
+ md.ok = UI.removeListeners(md.ok);
233
+ md.ok.addEventListener('click', () => FILE_MANAGER.encryptModel());
234
+ this.updateStrength();
235
+ code.focus();
236
+ }
237
+
238
+ saveModel() {
239
+ MODEL.clearSelection();
240
+ if(MODEL.encrypt) {
241
+ const md = UI.modals.password;
242
+ md.encryption_code = '';
243
+ md.element('action').innerHTML = 'Set';
244
+ md.ok = UI.removeListeners(md.ok);
245
+ md.ok.addEventListener('click', () => FILE_MANAGER.confirmPassword());
246
+ this.updateStrength();
247
+ md.show('code');
248
+ return;
249
+ }
250
+ // NOTE: Encode hashtags, or they will break the URI.
251
+ this.pushModelToBrowser(MODEL.asXML.replace(/#/g, '%23'));
252
+ }
253
+
254
+ pushModelToBrowser(xml) {
255
+ UI.setMessage('Model file size: ' + UI.sizeInBytes(xml.length));
256
+ const el = document.getElementById('xml-saver');
257
+ el.href = 'data:attachment/text,' + encodeURI(xml);
258
+ console.log('Encoded file size:', el.href.length);
259
+ el.download = 'model.lnr';
260
+ if(el.href.length > 25*1024*1024 &&
261
+ navigator.userAgent.search('Chrome') <= 0) {
262
+ UI.notify('Model file size exceeds 25 MB. ' +
263
+ 'If it does not download, store it in a repository');
264
+ }
265
+ el.click();
266
+ UI.normalCursor();
267
+ }
268
+
269
+ encryptModel() {
270
+ const
271
+ md = UI.modals.password,
272
+ code = md.element('code'),
273
+ pwd = code.value;
274
+ // NOTE: immediately clear password field
275
+ code.value = '';
276
+ md.hide();
277
+ if(pwd !== md.encryption_code) {
278
+ UI.warn('Encryption passwords did not match');
279
+ return;
280
+ }
281
+ UI.setMessage('Encrypting...');
282
+ UI.waitingCursor();
283
+ // Wait for key (NOTE: asynchronous functions defined in linny-r.js)
284
+ encryptionKey(pwd)
285
+ .then((key) => encryptMessage(MODEL.asXML.replace(/#/g, '%23'), key)
286
+ .then((enc) => this.pushModelToBrowser(MODEL.asEncryptedXML(enc)))
287
+ .catch((err) => {
288
+ UI.alert('Encryption failed');
289
+ console.log(err);
290
+ }))
291
+ .catch((err) => {
292
+ UI.alert('Failed to get encryption key');
293
+ console.log(err);
294
+ });
295
+ }
296
+
297
+ loadAutoSavedModel(name) {
298
+ fetch('autosave/', postData({
299
+ action: 'load',
300
+ file: name
301
+ }))
302
+ .then((response) => {
303
+ if(!response.ok) {
304
+ UI.alert(`ERROR ${response.status}: ${response.statusText}`);
305
+ }
306
+ return response.text();
307
+ })
308
+ .then((data) => {
309
+ if(UI.postResponseOK(data)) UI.loadModelFromXML(data);
310
+ })
311
+ .catch((err) => UI.warn(UI.WARNING.NO_CONNECTION, err));
312
+ }
313
+
314
+ storeAutoSavedModel() {
315
+ // Stores the current model in the local auto-save directory
316
+ const bcl = document.getElementById('autosave-btn').classList;
317
+ if(MODEL.running_experiment) {
318
+ console.log('No autosaving while running an experiment');
319
+ bcl.remove('stay-activ');
320
+ return;
321
+ }
322
+ fetch('autosave/', postData({
323
+ action: 'store',
324
+ file: REPOSITORY_BROWSER.asFileName(
325
+ (MODEL.name || 'no-name') + '_by_' +
326
+ (MODEL.author || 'no-author')),
327
+ xml: MODEL.asXML
328
+ }))
329
+ .then((response) => {
330
+ if(!response.ok) {
331
+ UI.alert(`ERROR ${response.status}: ${response.statusText}`);
332
+ }
333
+ return response.text();
334
+ })
335
+ .then((data) => {
336
+ UI.postResponseOK(data);
337
+ bcl.remove('stay-activ');
338
+ })
339
+ .catch((err) => {
340
+ UI.warn(UI.WARNING.NO_CONNECTION, err);
341
+ bcl.remove('stay-activ');
342
+ });
343
+ }
344
+
345
+ renderDiagramAsPNG() {
346
+ window.localStorage.removeItem('png-url');
347
+ UI.paper.fitToSize();
348
+ MODEL.alignToGrid();
349
+ this.renderSVGAsPNG(UI.paper.svg.outerHTML);
350
+ }
351
+
352
+ renderSVGAsPNG(svg) {
353
+ // Sends SVG to the server, which will convert it to PNG using Inkscape;
354
+ // if successful, the server will return the URL to the PNG file location;
355
+ // this URL is passed via the browser's local storage to the newly opened
356
+ // browser tab that awaits this URL and then loads it
357
+ const form = {
358
+ action: 'png',
359
+ user: VM.solver_user,
360
+ token: VM.solver_token,
361
+ data: btoa(encodeURI(svg))
362
+ };
363
+ fetch('solver/', postData(form))
364
+ .then((response) => {
365
+ if(!response.ok) {
366
+ UI.alert(`ERROR ${response.status}: ${response.statusText}`);
367
+ }
368
+ return response.text();
369
+ })
370
+ .then((data) => {
371
+ // Pass URL of image to the newly opened browser window
372
+ window.localStorage.setItem('png-url', data);
373
+ })
374
+ .catch((err) => UI.warn(UI.WARNING.NO_CONNECTION, err));
375
+ }
376
+
377
+ saveDiagramAsSVG() {
378
+ UI.paper.fitToSize();
379
+ MODEL.alignToGrid();
380
+ this.pushOutSVG(UI.paper.outerHTML);
381
+ }
382
+
383
+ pushOutSVG(svg) {
384
+ const blob = new Blob([svg], {'type': 'image/svg+xml'});
385
+ const e = document.getElementById('svg-saver');
386
+ e.download = 'model.svg';
387
+ e.type = 'image/svg+xml';
388
+ e.href = (window.URL || webkitURL).createObjectURL(blob);
389
+ e.click();
390
+ }
391
+
392
+ } // END of class GUIFileManager