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.
- package/README.md +102 -48
- package/package.json +1 -1
- package/server.js +31 -6
- package/static/images/check-off-not-same-changed.png +0 -0
- package/static/images/check-off-not-same-not-changed.png +0 -0
- package/static/images/check-off-same-changed.png +0 -0
- package/static/images/check-off-same-not-changed.png +0 -0
- package/static/images/check-on-not-same-changed.png +0 -0
- package/static/images/check-on-not-same-not-changed.png +0 -0
- package/static/images/check-on-same-changed.png +0 -0
- package/static/images/check-on-same-not-changed.png +0 -0
- package/static/images/eq-not-same-changed.png +0 -0
- package/static/images/eq-not-same-not-changed.png +0 -0
- package/static/images/eq-same-changed.png +0 -0
- package/static/images/eq-same-not-changed.png +0 -0
- package/static/images/ne-not-same-changed.png +0 -0
- package/static/images/ne-not-same-not-changed.png +0 -0
- package/static/images/ne-same-changed.png +0 -0
- package/static/images/ne-same-not-changed.png +0 -0
- package/static/images/sort-asc-lead.png +0 -0
- package/static/images/sort-asc.png +0 -0
- package/static/images/sort-desc-lead.png +0 -0
- package/static/images/sort-desc.png +0 -0
- package/static/images/sort-not.png +0 -0
- package/static/index.html +51 -35
- package/static/linny-r.css +167 -53
- package/static/scripts/linny-r-gui-actor-manager.js +340 -0
- package/static/scripts/linny-r-gui-chart-manager.js +944 -0
- package/static/scripts/linny-r-gui-constraint-editor.js +681 -0
- package/static/scripts/linny-r-gui-controller.js +4005 -0
- package/static/scripts/linny-r-gui-dataset-manager.js +1176 -0
- package/static/scripts/linny-r-gui-documentation-manager.js +739 -0
- package/static/scripts/linny-r-gui-equation-manager.js +307 -0
- package/static/scripts/linny-r-gui-experiment-manager.js +1944 -0
- package/static/scripts/linny-r-gui-expression-editor.js +450 -0
- package/static/scripts/linny-r-gui-file-manager.js +392 -0
- package/static/scripts/linny-r-gui-finder.js +727 -0
- package/static/scripts/linny-r-gui-model-autosaver.js +230 -0
- package/static/scripts/linny-r-gui-monitor.js +448 -0
- package/static/scripts/linny-r-gui-paper.js +2789 -0
- package/static/scripts/linny-r-gui-receiver.js +323 -0
- package/static/scripts/linny-r-gui-repository-browser.js +819 -0
- package/static/scripts/linny-r-gui-scale-unit-manager.js +244 -0
- package/static/scripts/linny-r-gui-sensitivity-analysis.js +778 -0
- package/static/scripts/linny-r-gui-undo-redo.js +560 -0
- package/static/scripts/linny-r-model.js +34 -15
- package/static/scripts/linny-r-utils.js +11 -1
- package/static/scripts/linny-r-vm.js +21 -12
- 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
|