linny-r 1.4.2 → 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.
- package/README.md +162 -74
- package/package.json +1 -1
- package/server.js +145 -49
- 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/octaeder.svg +993 -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 +72 -647
- package/static/linny-r.css +199 -417
- 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 +449 -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 +27 -11
- package/static/scripts/linny-r-utils.js +17 -2
- package/static/scripts/linny-r-vm.js +31 -12
- package/static/scripts/linny-r-gui.js +0 -16761
@@ -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"> \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
|