linny-r 1.4.3 → 1.4.4

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 +449 -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 +24 -11
  47. package/static/scripts/linny-r-utils.js +10 -0
  48. package/static/scripts/linny-r-vm.js +21 -12
  49. package/static/scripts/linny-r-gui.js +0 -16908
@@ -0,0 +1,230 @@
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.js) provides the GUI functionality
9
+ for the Linny-R model editor: buttons on the main tool bars, the associated
10
+ dialogs, the main drawing canvas, and event handler functions.
11
+
12
+ */
13
+
14
+ /*
15
+ Copyright (c) 2017-2023 Delft University of Technology
16
+
17
+ Permission is hereby granted, free of charge, to any person obtaining a copy
18
+ of this software and associated documentation files (the "Software"), to deal
19
+ in the Software without restriction, including without limitation the rights to
20
+ use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
21
+ of the Software, and to permit persons to whom the Software is furnished to do
22
+ so, subject to the following conditions:
23
+
24
+ The above copyright notice and this permission notice shall be included in
25
+ all copies or substantial portions of the Software.
26
+
27
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
28
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
29
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
30
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
31
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
32
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
33
+ SOFTWARE.
34
+ */
35
+
36
+ // CLASS ModelAutoSaver automatically saves the current model at regular
37
+ // time intervals in the user's `autosave` directory.
38
+ class ModelAutoSaver {
39
+ constructor() {
40
+ // Keep track of time-out interval of auto-saving feature.
41
+ this.timeout_id = 0;
42
+ this.interval = 10; // auto-save every 10 minutes
43
+ this.period = 24; // delete models older than 24 hours
44
+ this.model_list = [];
45
+ // Overwite defaults if settings still in local storage of browser.
46
+ this.getSettings();
47
+ // Purge files that have "expired".
48
+ this.getAutoSavedModels();
49
+ // Start the interval timer.
50
+ this.setAutoSaveInterval();
51
+ // Add listeners to GUI elements.
52
+ this.confirm_dialog = document.getElementById('confirm-remove-models');
53
+ document.getElementById('auto-save-clear-btn').addEventListener('click',
54
+ () => AUTO_SAVE.confirm_dialog.style.display = 'block');
55
+ document.getElementById('autosave-do-remove').addEventListener('click',
56
+ // NOTE: File name parameter /*ALL*/ indicates: delete all.
57
+ () => AUTO_SAVE.getAutoSavedModels(true, '/*ALL*/'));
58
+ document.getElementById('autosave-cancel').addEventListener('click',
59
+ () => AUTO_SAVE.confirm_dialog.style.display = 'none');
60
+ document.getElementById('restore-cancel').addEventListener('click',
61
+ () => AUTO_SAVE.hideRestoreDialog(false));
62
+ document.getElementById('restore-confirm').addEventListener('click',
63
+ () => AUTO_SAVE.hideRestoreDialog(true));
64
+ }
65
+
66
+ getSettings() {
67
+ // Reads custom auto-save settings from local storage.
68
+ try {
69
+ const item = window.localStorage.getItem('Linny-R-autosave');
70
+ if(item) {
71
+ const
72
+ mh = item.split('|'),
73
+ m = parseFloat(mh[0]),
74
+ h = parseFloat(mh[1]);
75
+ if(isNaN(m) || isNaN(h)) {
76
+ UI.warn('Ignored invalid local auto-save settings');
77
+ } else {
78
+ this.interval = m;
79
+ this.period = h;
80
+ }
81
+ }
82
+ } catch(err) {
83
+ console.log('Local storage failed:', err);
84
+ }
85
+ }
86
+
87
+ setSettings() {
88
+ // Writes custom auto-save settings to local storage.
89
+ try {
90
+ window.localStorage.setItem('Linny-R-autosave',
91
+ this.interval + '|' + this.period);
92
+ UI.notify('New auto-save settings stored in browser');
93
+ } catch(err) {
94
+ UI.warn('Failed to write auto-save settings to local storage');
95
+ console.log(err);
96
+ }
97
+ }
98
+
99
+ saveModel() {
100
+ document.getElementById('autosave-btn').classList.add('stay-activ');
101
+ // Use setTimeout to let browser always briefly show the active color
102
+ // even when the model file is small and storing hardly takes time.
103
+ setTimeout(() => FILE_MANAGER.storeAutoSavedModel(), 300);
104
+ }
105
+
106
+ setAutoSaveInterval() {
107
+ // Activate the auto-save feature (if interval is configured).
108
+ if(this.timeout_id) clearInterval(this.timeout_id);
109
+ // NOTE: Interval = 0 indicates "do not auto-save".
110
+ if(this.interval) {
111
+ // Interval is in minutes, so multiply by 60 thousand to get msec.
112
+ this.timeout_id = setInterval(
113
+ () => AUTO_SAVE.saveModel(), this.interval * 60000);
114
+ }
115
+ }
116
+
117
+ getAutoSavedModels(show_dialog=false, file_to_delete='') {
118
+ // Get list of auto-saved models from server (after deleting those that
119
+ // have been stored beyond the set period AND the specified file to
120
+ // delete (where /*ALL*/ indicates "delete all auto-saved files").
121
+ const pd = {action: 'purge', period: this.period};
122
+ if(file_to_delete) pd.to_delete = file_to_delete;
123
+ fetch('autosave/', postData(pd))
124
+ .then((response) => {
125
+ if(!response.ok) {
126
+ UI.alert(`ERROR ${response.status}: ${response.statusText}`);
127
+ }
128
+ return response.text();
129
+ })
130
+ .then((data) => {
131
+ if(UI.postResponseOK(data)) {
132
+ try {
133
+ AUTO_SAVE.model_list = JSON.parse(data);
134
+ } catch(err) {
135
+ AUTO_SAVE.model_list = [];
136
+ UI.warn('Data on auto-saved models is not valid');
137
+ }
138
+ }
139
+ // Update auto-save-related dialog elements.
140
+ const
141
+ n = this.model_list.length,
142
+ ttl = pluralS(n, 'auto-saved model'),
143
+ rbtn = document.getElementById('load-autosaved-btn');
144
+ document.getElementById('autosave-btn').title = ttl;
145
+ rbtn.title = ttl;
146
+ rbtn.style.display = (n > 0 ? 'block' : 'none');
147
+ if(show_dialog) AUTO_SAVE.showRestoreDialog();
148
+ })
149
+ .catch((err) => {console.log(err); UI.warn(UI.WARNING.NO_CONNECTION, err);});
150
+ }
151
+
152
+ showRestoreDialog() {
153
+ // Shows list of auto-saved models; clicking on one will load it
154
+ // NOTE: hide "Load model" dialog in case it was showing.
155
+ document.getElementById('load-modal').style.display = 'none';
156
+ // Contruct the table to select from.
157
+ let html = '';
158
+ for(let i = 0; i < this.model_list.length; i++) {
159
+ const
160
+ m = this.model_list[i],
161
+ bytes = UI.sizeInBytes(m.size).split(' ');
162
+ html += ['<tr class="dataset" style="color: gray" ',
163
+ 'onclick="FILE_MANAGER.loadAutoSavedModel(\'',
164
+ m.name,'\');"><td class="restore-name">', m.name, '</td><td>',
165
+ m.date.substring(1, 16).replace('T', ' '),
166
+ '</td><td style="text-align: right">',
167
+ bytes[0], '</td><td>', bytes[1], '</td><td style="width:15px">',
168
+ '<img class="del-asm-btn" src="images/delete.png" ',
169
+ 'onclick="event.stopPropagation(); ',
170
+ 'AUTO_SAVE.getAutoSavedModels(true, \'', m.name,
171
+ '\')"></td></tr>'].join('');
172
+ }
173
+ document.getElementById('restore-table').innerHTML = html;
174
+ // Adjust dialog height (max-height will limit list to 10 lines).
175
+ document.getElementById('restore-dlg').style.height =
176
+ (48 + 19 * this.model_list.length) + 'px';
177
+ document.getElementById('confirm-remove-models').style.display = 'none';
178
+ // Fill text input fields with present settings.
179
+ document.getElementById('auto-save-minutes').value = this.interval;
180
+ document.getElementById('auto-save-hours').value = this.period;
181
+ // Show remove button only if restorable files exits.
182
+ const
183
+ ttl = document.getElementById('restore-dlg-title'),
184
+ sa = document.getElementById('restore-scroll-area'),
185
+ btn = document.getElementById('auto-save-clear-btn');
186
+ if(this.model_list.length) {
187
+ ttl.innerHTML = 'Restore auto-saved model';
188
+ sa.style.display = 'block';
189
+ btn.style.display = 'block';
190
+ } else {
191
+ ttl.innerHTML = 'Auto-save settings (for this browser)';
192
+ sa.style.display = 'none';
193
+ btn.style.display = 'none';
194
+ }
195
+ document.getElementById('restore-modal').style.display = 'block';
196
+ }
197
+
198
+ hideRestoreDialog(save=true) {
199
+ // Close the restore auto-save model dialog.
200
+ document.getElementById('confirm-remove-models').style.display = 'none';
201
+ // NOTE: Cancel button or ESC will pass `cancel` as FALSE => do not save.
202
+ if(!save) {
203
+ document.getElementById('restore-modal').style.display = 'none';
204
+ return;
205
+ }
206
+ // Validate settings.
207
+ let m = this.interval,
208
+ h = this.period,
209
+ e = document.getElementById('auto-save-minutes');
210
+ m = parseInt(e.value);
211
+ if(!isNaN(m)) {
212
+ e = document.getElementById('auto-save-hours');
213
+ h = parseInt(e.value);
214
+ if(!isNaN(h)) {
215
+ // If valid, store in local storage of browser.
216
+ if(m !== this.interval || h !== this.period) {
217
+ this.interval = m;
218
+ this.period = h;
219
+ this.setSettings();
220
+ this.setAutoSaveInterval();
221
+ }
222
+ document.getElementById('restore-modal').style.display = 'none';
223
+ return;
224
+ }
225
+ }
226
+ UI.warn('Invalid auto-save settings');
227
+ e.focus();
228
+ }
229
+
230
+ } // END of class ModelAutoSaver
@@ -0,0 +1,448 @@
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-monitor.js) provides the GUI functionality
9
+ for the Linny-R Monitor dialog.
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 GUIMonitor provides the GUI for the Virtual Machine, and handles
36
+ // the interaction with the MILP solver via POST requests to the server.
37
+ // NOTE: because the console-only monitor requires Node.js modules, this
38
+ // GUI class does NOT extend its console-only counterpart
39
+ class GUIMonitor {
40
+ constructor() {
41
+ this.console = false;
42
+ this.visible = false;
43
+ // The "shown" flag is used to prevent re-display of the call stack
44
+ this.call_stack_shown = false;
45
+ // Initialize related DOM elements
46
+ this.dialog = UI.draggableDialog('monitor');
47
+ UI.resizableDialog('monitor', 'MONITOR');
48
+ this.close_btn = document.getElementById('monitor-close-btn');
49
+ this.timer = document.getElementById('monitor-timer');
50
+ this.messages_tab = document.getElementById('monitor-msg-tab');
51
+ this.messages_text = document.getElementById('monitor-msg');
52
+ this.variables_tab = document.getElementById('monitor-vbl-tab');
53
+ this.variables_text = document.getElementById('monitor-vbl');
54
+ this.equations_tab = document.getElementById('monitor-eqs-tab');
55
+ this.equations_text = document.getElementById('monitor-eqs');
56
+ this.progress_bar = document.getElementById('monitor-progress-bar');
57
+
58
+ // Make toolbar buttons responsive
59
+ this.close_btn.addEventListener(
60
+ 'click', (event) => UI.toggleDialog(event));
61
+ this.messages_tab.addEventListener(
62
+ 'click', () => MONITOR.updateContent('msg'));
63
+ this.variables_tab.addEventListener(
64
+ 'click', () => MONITOR.updateContent('vbl'));
65
+ this.equations_tab.addEventListener(
66
+ 'click', () => MONITOR.updateContent('eqs'));
67
+
68
+ // Make close button of call stack dialog responsive
69
+ document.getElementById('call-stack-close-btn').addEventListener(
70
+ 'click', () => MONITOR.hideCallStack());
71
+
72
+ this.shown_block = 0;
73
+ // Initially show the messages textarea
74
+ this.tab = 'vbl';
75
+ this.updateContent('msg');
76
+ }
77
+
78
+ reset() {
79
+ this.shown_block = 0;
80
+ this.last_message_block = 0;
81
+ // Clear monitor's text areas
82
+ this.messages_text.value = '';
83
+ this.variables_text.value = '';
84
+ this.equations_text.value = '';
85
+ // Clear the progress bar
86
+ while(this.progress_bar.firstChild) {
87
+ this.progress_bar.removeChild(this.progress_bar.lastChild);
88
+ }
89
+ this.updateContent('msg');
90
+ }
91
+
92
+ updateMonitorTime() {
93
+ // Displays the elapsed time since last reset as (hrs:)mins:secs
94
+ let td = (new Date().getTime() - VM.reset_time) / 1000,
95
+ hrs = Math.floor(td / 3600);
96
+ if(hrs > 0) {
97
+ td -= hrs * 3600;
98
+ hrs += ':';
99
+ } else {
100
+ hrs = '';
101
+ }
102
+ const
103
+ min = Math.floor(td / 60),
104
+ sec = Math.round(td - 60*min),
105
+ t = ('0' + min).slice(-2) + ':' + ('0' + sec).slice(-2);
106
+ this.timer.textContent = hrs + t;
107
+ }
108
+
109
+ updateBlockNumber(bwr) {
110
+ // Display progres as block number (with round) / number of blocks
111
+ document.getElementById('monitor-blocks').innerText =
112
+ bwr + '/' + VM.nr_of_blocks;
113
+ }
114
+
115
+ clearProgressBar() {
116
+ // Clear the progress bar
117
+ while(this.progress_bar.firstChild) {
118
+ this.progress_bar.removeChild(this.progress_bar.lastChild);
119
+ }
120
+ }
121
+
122
+ addProgressBlock(b, err, time) {
123
+ // Adds a block to the progress bar, and updates the relative block lengths
124
+ let total_time = 0;
125
+ for(let i = 0; i < b; i++) {
126
+ total_time += VM.solver_times[i];
127
+ }
128
+ const n = document.createElement('div');
129
+ n.classList.add('progress-block');
130
+ if(err) n.classList.add('error-pb');
131
+ if(b % 2 == 0) n.classList.add('even-pb');
132
+ n.setAttribute('title',
133
+ `Block #${b} took ${time.toPrecision(3)} seconds
134
+ (solver: ${VM.solver_secs[b - 1]} seconds)`);
135
+ n.setAttribute('data-blk', b);
136
+ n.addEventListener('click',
137
+ (event) => {
138
+ const el = event.target;
139
+ el.classList.add('sel-pb');
140
+ MONITOR.showBlock(el.dataset.blk);
141
+ },
142
+ false);
143
+ this.progress_bar.appendChild(n);
144
+ this.progress_bar.style.width =
145
+ Math.floor(100 * b / VM.nr_of_blocks) + '%';
146
+ const cn = this.progress_bar.childNodes;
147
+ if(cn && this.shown_block > 0 && this.shown_block <= cn.length) {
148
+ cn[this.shown_block - 1].classList.add('sel-pb');
149
+ }
150
+ for(let i = 0; i < cn.length; i++) {
151
+ cn[i].style.width =
152
+ (Math.floor(10000 * VM.solver_times[i] / total_time) / 100) + '%';
153
+ }
154
+ }
155
+
156
+ showBlock(b) {
157
+ this.shown_block = b;
158
+ const cn = this.progress_bar.childNodes;
159
+ for(let i = 0; i < cn.length; i++) {
160
+ cn[i].classList.remove('sel-pb');
161
+ }
162
+ cn[b - 1].classList.add('sel-pb');
163
+ this.updateContent(this.tab);
164
+ }
165
+
166
+ updateDialog() {
167
+ // Implements default behavior for a draggable/resizable dialog
168
+ this.updateContent(this.tab);
169
+ }
170
+
171
+ updateContent(tab) {
172
+ // Get the block being computed
173
+ this.block_count = VM.block_count;
174
+ // Shows the appropriate text in the monitor's textarea
175
+ let b = this.shown_block;
176
+ // By default, show information on the block being calculated
177
+ if(b === 0) b = this.block_count;
178
+ if(this.block_count === 0) {
179
+ this.messages_text.value = VM.no_messages;
180
+ this.equations_text.value = VM.no_equations;
181
+ } else if(b <= VM.messages.length) {
182
+ this.messages_text.value = VM.messages[b - 1];
183
+ this.equations_text.value = VM.equations[b - 1];
184
+ }
185
+ // Legend to variables is not block-dependent
186
+ this.variables_text.value = VM.variablesLegend(b);
187
+ // Show the text area for the selected tab
188
+ if(this.tab !== tab) {
189
+ let mt = 'monitor-' + this.tab;
190
+ document.getElementById(mt).style.display = 'none';
191
+ document.getElementById(mt + '-tab').classList.remove('sel-tab');
192
+ this.tab = tab;
193
+ mt = 'monitor-' + this.tab;
194
+ document.getElementById(mt).style.display = 'block';
195
+ document.getElementById(mt + '-tab').classList.add('sel-tab');
196
+ }
197
+ }
198
+
199
+ showCallStack(t) {
200
+ // Show the error message in the dialog header
201
+ // NOTE: prevent showing again when VM detects multiple errors
202
+ if(this.call_stack_shown) return;
203
+ const
204
+ csl = VM.call_stack.length,
205
+ top = VM.call_stack[csl - 1],
206
+ err = top.vector[t],
207
+ // Make separate lists of variable names and their expressions
208
+ vlist = [],
209
+ xlist = [];
210
+ document.getElementById('call-stack-error').innerHTML =
211
+ `ERROR at t=${t}: ` + VM.errorMessage(err);
212
+ for(let i = 0; i < csl; i++) {
213
+ const
214
+ x = VM.call_stack[i],
215
+ // For equations, only show the attribute
216
+ ons = (x.object === MODEL.equations_dataset ? '' :
217
+ x.object.displayName + '|');
218
+ vlist.push(ons + x.attribute);
219
+ // Trim spaces around all object-attribute separators in the expression
220
+ xlist.push(x.text.replace(/\s*\|\s*/g, '|'));
221
+ }
222
+ // Highlight variables where they are used in the expressions
223
+ const vcc = UI.chart_colors.length;
224
+ for(let i = 0; i < xlist.length; i++) {
225
+ for(let j = 0; j < vlist.length; j++) {
226
+ // Ignore selectors, as these may be different per experiment
227
+ const
228
+ vnl = vlist[j].split('|'),
229
+ sel = (vnl.length > 1 ? vnl.pop() : ''),
230
+ attr = (VM.attribute_names[sel] ? '|' + sel : ''),
231
+ vn = vnl.join() + attr,
232
+ vnc = '<span style="font-weight: 600; color: ' +
233
+ `${UI.chart_colors[j % vcc]}">${vn}</span>`;
234
+ xlist[i] = xlist[i].split(vn).join(vnc);
235
+ }
236
+ }
237
+ // Then also color the variables
238
+ for(let i = 0; i < vlist.length; i++) {
239
+ vlist[i] = '<span style="font-weight: 600; color: ' +
240
+ `${UI.chart_colors[i % vcc]}">${vlist[i]}</span>`;
241
+ }
242
+ // Start without indentation
243
+ let pad = 0;
244
+ // First show the variable being computed
245
+ const tbl = ['<div>', vlist[0], '</div>'];
246
+ // Then iterate upwards over the call stack
247
+ for(let i = 0; i < vlist.length - 1; i++) {
248
+ // Show the expression, followed by the next computed variable
249
+ tbl.push(['<div class="call-stack-row" style="padding-left: ',
250
+ pad, 'px"><div class="call-stack-expr">', xlist[i],
251
+ '</div><div class="call-stack-vbl">&nbsp;\u2937', vlist[i+1],
252
+ '</div></div>'].join(''));
253
+ // Increase indentation
254
+ pad += 8;
255
+ }
256
+ // Show the last expression, highlighting the array-out-of-bounds (if any)
257
+ let last_x = xlist[xlist.length - 1],
258
+ anc = '';
259
+ if(VM.out_of_bounds_array) {
260
+ anc = '<span style="font-weight: 600; color: red">' +
261
+ VM.out_of_bounds_array + '</span>';
262
+ last_x = last_x.split(VM.out_of_bounds_array).join(anc);
263
+ }
264
+ tbl.push('<div class="call-stack-expr" style="padding-left: ' +
265
+ `${pad}px">${last_x}</div>`);
266
+ // Add index-out-of-bounds message if appropriate
267
+ if(anc) {
268
+ tbl.push('<div style="color: gray; margin-top: 8px; font-size: 10px">',
269
+ VM.out_of_bounds_msg.replace(VM.out_of_bounds_array, anc), '</div>');
270
+ }
271
+ // Dump the code for the last expression to the console
272
+ console.log('Code for', top.text, top.code);
273
+ // Show the call stack dialog
274
+ document.getElementById('call-stack-table').innerHTML = tbl.join('');
275
+ document.getElementById('call-stack-modal').style.display = 'block';
276
+ this.call_stack_shown = true;
277
+ }
278
+
279
+ hideCallStack() {
280
+ document.getElementById('call-stack-modal').style.display = 'none';
281
+ }
282
+
283
+ logMessage(block, msg) {
284
+ // Appends a solver message to the monitor's messages textarea
285
+ if(this.messages_text.value === VM.no_messages) {
286
+ // Erase the "(no messages)" if still showing
287
+ this.messages_text.value = '';
288
+ }
289
+ if(this.shown_block === 0 && block !== this.last_message_block) {
290
+ // Clear text area when starting with new block while no block selected
291
+ this.last_message_block = block;
292
+ this.messages_text.value = '';
293
+ }
294
+ // NOTE: `msg` is appended only if no block has been selected by
295
+ // clicking on the progress bar, or if the message belongs to the
296
+ // selected block
297
+ if(this.shown_block === 0 || this.shown_block === block) {
298
+ this.messages_text.value += msg + '\n';
299
+ }
300
+ }
301
+
302
+ logOnToServer(usr, pwd) {
303
+ VM.solver_user = usr;
304
+ fetch('solver/', postData({action: 'logon', user: usr, password: pwd}))
305
+ .then((response) => {
306
+ if(!response.ok) {
307
+ UI.alert(`ERROR ${response.status}: ${response.statusText}`);
308
+ }
309
+ return response.text();
310
+ })
311
+ .then((data) => {
312
+ let jsr;
313
+ try {
314
+ jsr = JSON.parse(data);
315
+ } catch(err) {
316
+ console.log('ERROR while parsing JSON:', err);
317
+ UI.alert('ERROR: Unexpected data from server: ' +
318
+ ellipsedText(data));
319
+ return;
320
+ }
321
+ if(jsr.error) {
322
+ UI.alert(jsr.error);
323
+ } else if(jsr.server) {
324
+ VM.solver_token = jsr.token;
325
+ VM.solver_name = jsr.solver;
326
+ // Remote solver may indicate user-specific solver time limit
327
+ let utl = '';
328
+ if(jsr.time_limit) {
329
+ VM.max_solver_time = jsr.time_limit;
330
+ utl = ` -- ${VM.solver_name} solver: ` +
331
+ `max. ${VM.max_solver_time} seconds per block`;
332
+ // If user has a set time limit, no restrictions on tableau size
333
+ VM.max_tableau_size = 0;
334
+ }
335
+ UI.notify('Logged on to ' + jsr.server + utl);
336
+ } else {
337
+ UI.warn('Authentication failed -- NOT logged on to server -- ' +
338
+ 'Click <a href="solver/?action=password">' +
339
+ '<strong>here</strong></a> to change password');
340
+ }
341
+ })
342
+ .catch((err) => UI.warn(UI.WARNING.NO_CONNECTION, err));
343
+ }
344
+
345
+ connectToServer() {
346
+ // Prompts for credentials if not connected yet
347
+ // NOTE: no authentication prompt if SOLVER.user_id in `linny-r-config.js`
348
+ // is left blank
349
+ if(!VM.solver_user) {
350
+ VM.solver_token = 'local host';
351
+ fetch('solver/', postData({action: 'logon'}))
352
+ .then((response) => {
353
+ if(!response.ok) {
354
+ UI.alert(`ERROR ${response.status}: ${response.statusText}`);
355
+ }
356
+ return response.text();
357
+ })
358
+ .then((data) => {
359
+ try {
360
+ const
361
+ jsr = JSON.parse(data),
362
+ svr = `Solver on ${jsr.server} is ${jsr.solver}`;
363
+ if(jsr.solver !== VM.solver_name) UI.notify(svr);
364
+ VM.solver_name = jsr.solver;
365
+ document.getElementById('host-logo').title = svr;
366
+ } catch(err) {
367
+ console.log(err, data);
368
+ UI.alert('ERROR: Unexpected data from server: ' +
369
+ ellipsedText(data));
370
+ return;
371
+ }
372
+ })
373
+ .catch((err) => UI.warn(UI.WARNING.NO_CONNECTION, err));
374
+ }
375
+ if(VM.solver_token) return true;
376
+ UI.loginPrompt();
377
+ return false;
378
+ }
379
+
380
+ submitBlockToSolver() {
381
+ let top = MODEL.timeout_period;
382
+ if(VM.max_solver_time && top > VM.max_solver_time) {
383
+ top = VM.max_solver_time;
384
+ UI.notify('Solver time limit for this server is ' +
385
+ VM.max_solver_time + ' seconds');
386
+ }
387
+ UI.logHeapSize(`BEFORE creating post data`);
388
+ const
389
+ bwr = VM.blockWithRound,
390
+ pd = postData({
391
+ action: 'solve',
392
+ user: VM.solver_user,
393
+ token: VM.solver_token,
394
+ block: VM.block_count,
395
+ round: VM.round_sequence[VM.current_round],
396
+ columns: VM.columnsInBlock,
397
+ data: VM.lines,
398
+ timeout: top
399
+ });
400
+ UI.logHeapSize(`AFTER creating post data`);
401
+ // Immediately free the memory taken up by VM.lines
402
+ VM.lines = '';
403
+ UI.logHeapSize(`BEFORE submitting block #${bwr} to solver`);
404
+ fetch('solver/', pd)
405
+ .then((response) => {
406
+ if(!response.ok) {
407
+ const msg = `ERROR ${response.status}: ${response.statusText}`;
408
+ VM.logMessage(VM.block_count, msg);
409
+ UI.alert(msg);
410
+ }
411
+ return response.text();
412
+ })
413
+ .then((data) => {
414
+ try {
415
+ VM.processServerResponse(JSON.parse(data));
416
+ UI.logHeapSize('After processing results for block #' + this.block_count);
417
+ // If no errors, solve next block (if any)
418
+ // NOTE: use setTimeout so that this calling function returns,
419
+ // and browser can update its DOM to display progress
420
+ setTimeout(() => VM.solveBlocks(), 1);
421
+ } catch(err) {
422
+ // Log details on the console
423
+ console.log('ERROR while parsing JSON:', err);
424
+ console.log(data);
425
+ // Pass summary on to the browser
426
+ const msg = 'ERROR: Unexpected data from server: ' +
427
+ ellipsedText(data);
428
+ this.logMessage(this.block_count, msg);
429
+ UI.alert(msg);
430
+ VM.stopSolving();
431
+ return;
432
+ }
433
+ })
434
+ .catch((err) => {
435
+ console.log('ERROR on POST:', err);
436
+ const msg = 'SERVER ERROR: ' + ellipsedText(err.toString());
437
+ VM.logMessage(VM.block_count, msg);
438
+ UI.alert(msg);
439
+ VM.stopSolving();
440
+ });
441
+ pd.body = '';
442
+ UI.logHeapSize(`after calling FETCH and clearing POST data body`);
443
+ VM.logMessage(VM.block_count,
444
+ `POSTing block #${bwr} took ${VM.elapsedTime} seconds.`);
445
+ UI.logHeapSize(`AFTER posting block #${bwr} to solver`);
446
+ }
447
+
448
+ } // END of class GUIMonitor