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,819 @@
|
|
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-repository.js) provides the GUI functionality
|
9
|
+
for the Linny-R Repository Browser dialog (classes Module, Repository, and
|
10
|
+
GUIRepositoryBrowser).
|
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
|
+
|
37
|
+
// CLASS Module
|
38
|
+
// NOTE: a module is not a model component; merely a wrapper for the name and
|
39
|
+
// comments properties of a model stored in a repository so that it responds
|
40
|
+
// as expected by the documentation manager
|
41
|
+
class Module {
|
42
|
+
constructor(file_name) {
|
43
|
+
this.file_name = file_name;
|
44
|
+
this.comments = '';
|
45
|
+
}
|
46
|
+
|
47
|
+
get type() {
|
48
|
+
return 'Module';
|
49
|
+
}
|
50
|
+
|
51
|
+
get displayName() {
|
52
|
+
// NOTE: module names are file names, and hence displayed in monospaced font
|
53
|
+
return `<tt>${this.file_name}<tt>`;
|
54
|
+
}
|
55
|
+
|
56
|
+
} // END of class Module
|
57
|
+
|
58
|
+
|
59
|
+
// CLASS Repository
|
60
|
+
class Repository {
|
61
|
+
constructor(name, aut=false) {
|
62
|
+
this.name = name;
|
63
|
+
// Authorized to store models if local host, or registered with a valid token
|
64
|
+
this.authorized = aut;
|
65
|
+
// NOTE: URL of repository is stored on server => not used in application
|
66
|
+
this.module_names = [];
|
67
|
+
}
|
68
|
+
|
69
|
+
getModuleList() {
|
70
|
+
// Obtains the list of modules in this repository from the server
|
71
|
+
this.module_names.length = 0;
|
72
|
+
fetch('repo/', postData({action: 'dir', repo: this.name}))
|
73
|
+
.then((response) => {
|
74
|
+
if(!response.ok) {
|
75
|
+
UI.alert(`ERROR ${response.status}: ${response.statusText}`);
|
76
|
+
}
|
77
|
+
return response.text();
|
78
|
+
})
|
79
|
+
.then((data) => {
|
80
|
+
if(UI.postResponseOK(data)) {
|
81
|
+
// NOTE: `this` refers to this instance of class Repository
|
82
|
+
const repo = REPOSITORY_BROWSER.repositoryByName(this.name);
|
83
|
+
if(!repo) throw 'Repository not found';
|
84
|
+
// Server returns newline-separated string of formal module names
|
85
|
+
// NOTE: these include version number as -nn
|
86
|
+
repo.module_names = data.split('\n');
|
87
|
+
REPOSITORY_BROWSER.updateDialog();
|
88
|
+
}
|
89
|
+
})
|
90
|
+
.catch((err) => UI.warn(UI.WARNING.NO_CONNECTION, err));
|
91
|
+
}
|
92
|
+
|
93
|
+
getModuleInfo(n, m) {
|
94
|
+
// Gets the documentation (<notes>) of Linny-R model with index `n` from
|
95
|
+
// this repository as `comments` property of module `m`
|
96
|
+
fetch('repo/', postData({
|
97
|
+
action: 'info',
|
98
|
+
repo: this.name,
|
99
|
+
file: this.module_names[n]
|
100
|
+
}))
|
101
|
+
.then((response) => {
|
102
|
+
if(!response.ok) {
|
103
|
+
UI.alert(`ERROR ${response.status}: ${response.statusText}`);
|
104
|
+
}
|
105
|
+
return response.text();
|
106
|
+
})
|
107
|
+
.then((data) => {
|
108
|
+
if(UI.postResponseOK(data)) {
|
109
|
+
// Server returns the "markdown" text
|
110
|
+
m.comments = data;
|
111
|
+
// Completely update the documentation manager dialog
|
112
|
+
DOCUMENTATION_MANAGER.update(m, true);
|
113
|
+
}
|
114
|
+
})
|
115
|
+
.catch((err) => UI.warn(UI.WARNING.NO_CONNECTION, err));
|
116
|
+
}
|
117
|
+
|
118
|
+
loadModule(n, include=false) {
|
119
|
+
// Loads Linny-R model with index `n` from this repository
|
120
|
+
// NOTES:
|
121
|
+
// (1) when `include` is FALSE, this function behaves as the `loadModel`
|
122
|
+
// method of FileManager; when `include` is TRUE, the module is included
|
123
|
+
// as a cluster (with parameterization via an IO context)
|
124
|
+
// (2) loading a module requires no authentication
|
125
|
+
fetch('repo/', postData({
|
126
|
+
action: 'load',
|
127
|
+
repo: this.name,
|
128
|
+
file: this.module_names[n]
|
129
|
+
}))
|
130
|
+
.then((response) => {
|
131
|
+
if(!response.ok) {
|
132
|
+
UI.alert(`ERROR ${response.status}: ${response.statusText}`);
|
133
|
+
}
|
134
|
+
return response.text();
|
135
|
+
})
|
136
|
+
.then((data) => {
|
137
|
+
if(data !== '' && UI.postResponseOK(data)) {
|
138
|
+
// Server returns Linny-R model file
|
139
|
+
if(include) {
|
140
|
+
// Include module into current model
|
141
|
+
REPOSITORY_BROWSER.promptForInclusion(
|
142
|
+
this.name, this.module_names[n],
|
143
|
+
parseXML(data.replace(/%23/g, '#')));
|
144
|
+
} else {
|
145
|
+
if(UI.loadModelFromXML(data)) {
|
146
|
+
UI.notify(`Model <tt>${this.module_names[n]}</tt> ` +
|
147
|
+
`loaded from <strong>${this.name}</strong>`);
|
148
|
+
}
|
149
|
+
}
|
150
|
+
}
|
151
|
+
})
|
152
|
+
.catch((err) => UI.warn(UI.WARNING.NO_CONNECTION, err));
|
153
|
+
}
|
154
|
+
|
155
|
+
storeModelAsModule(name, black_box=false) {
|
156
|
+
// Stores the current model in this repository
|
157
|
+
// NOTE: this requires authentication
|
158
|
+
UI.waitingCursor();
|
159
|
+
fetch('repo/', postData({
|
160
|
+
action: 'store',
|
161
|
+
repo: this.name,
|
162
|
+
file: name,
|
163
|
+
xml: (black_box ? MODEL.asBlackBoxXML : MODEL.asXML)
|
164
|
+
}))
|
165
|
+
.then((response) => {
|
166
|
+
if(!response.ok) {
|
167
|
+
UI.alert(`ERROR ${response.status}: ${response.statusText}`);
|
168
|
+
}
|
169
|
+
return response.text();
|
170
|
+
})
|
171
|
+
.then((data) => {
|
172
|
+
// Always display server message on the information line
|
173
|
+
UI.postResponseOK(data, true);
|
174
|
+
// Deselect any module in the list
|
175
|
+
REPOSITORY_BROWSER.module_index = -1;
|
176
|
+
const r = REPOSITORY_BROWSER.repositoryByName(this.name);
|
177
|
+
if(r) {
|
178
|
+
r.getModuleList();
|
179
|
+
} else {
|
180
|
+
console.log(`ERROR: Failed to return to repository "${this.name}"`);
|
181
|
+
}
|
182
|
+
UI.normalCursor();
|
183
|
+
})
|
184
|
+
.catch((err) => UI.warn(UI.WARNING.NO_CONNECTION, err));
|
185
|
+
}
|
186
|
+
|
187
|
+
deleteModule(n) {
|
188
|
+
// Deletes the n-th module from the module list of this repository
|
189
|
+
// NOTE: this should be accepted only for the local host
|
190
|
+
if(this.name !== 'local host') {
|
191
|
+
UI.warn('Deletion is restricted to the local host');
|
192
|
+
return;
|
193
|
+
}
|
194
|
+
// Check if `n` is a valid module index
|
195
|
+
if(n < 0 || n >= this.module_names.length) {
|
196
|
+
UI.alert('Invalid module index: ' + n);
|
197
|
+
return;
|
198
|
+
}
|
199
|
+
// Send the delete request to the server
|
200
|
+
fetch('repo/', postData({
|
201
|
+
action: 'delete',
|
202
|
+
repo: this.name,
|
203
|
+
file: this.module_names[n]
|
204
|
+
}))
|
205
|
+
.then((response) => {
|
206
|
+
if(!response.ok) {
|
207
|
+
UI.alert(`ERROR ${response.status}: ${response.statusText}`);
|
208
|
+
}
|
209
|
+
return response.text();
|
210
|
+
})
|
211
|
+
.then((data) => {
|
212
|
+
// Always display server message on the information line
|
213
|
+
UI.postResponseOK(data, true);
|
214
|
+
// Deselect any module in the list
|
215
|
+
REPOSITORY_BROWSER.module_index = -1;
|
216
|
+
const r = REPOSITORY_BROWSER.repositoryByName(this.name);
|
217
|
+
if(r) {
|
218
|
+
r.getModuleList();
|
219
|
+
} else {
|
220
|
+
console.log(`ERROR: Failed to return to repository "${this.name}"`);
|
221
|
+
}
|
222
|
+
})
|
223
|
+
.catch((err) => UI.warn(UI.WARNING.NO_CONNECTION, err));
|
224
|
+
}
|
225
|
+
|
226
|
+
} // END of class Repository
|
227
|
+
|
228
|
+
|
229
|
+
// CLASS GUIRepositoryBrowser
|
230
|
+
class GUIRepositoryBrowser extends RepositoryBrowser {
|
231
|
+
constructor() {
|
232
|
+
super();
|
233
|
+
this.dialog = UI.draggableDialog('repository');
|
234
|
+
UI.resizableDialog('repository', 'REPOSITORY_BROWSER');
|
235
|
+
this.close_btn = document.getElementById('repository-close-btn');
|
236
|
+
this.close_btn.addEventListener(
|
237
|
+
'click', (event) => UI.toggleDialog(event));
|
238
|
+
// Make toolbar buttons responsive
|
239
|
+
document.getElementById('repo-add-btn').addEventListener(
|
240
|
+
'click', () => REPOSITORY_BROWSER.promptForRepository());
|
241
|
+
document.getElementById('repo-remove-btn').addEventListener(
|
242
|
+
'click', () => REPOSITORY_BROWSER.removeRepository());
|
243
|
+
document.getElementById('repo-access-btn').addEventListener(
|
244
|
+
'click', () => REPOSITORY_BROWSER.promptForAccess());
|
245
|
+
document.getElementById('repo-include-btn').addEventListener(
|
246
|
+
'click', () => REPOSITORY_BROWSER.includeModule());
|
247
|
+
document.getElementById('repo-load-btn').addEventListener(
|
248
|
+
'click', () => REPOSITORY_BROWSER.confirmLoadModuleAsModel());
|
249
|
+
document.getElementById('repo-store-btn').addEventListener(
|
250
|
+
'click', () => REPOSITORY_BROWSER.promptForStoring());
|
251
|
+
document.getElementById('repo-black-box-btn').addEventListener(
|
252
|
+
'click', () => REPOSITORY_BROWSER.promptForBlackBoxing());
|
253
|
+
document.getElementById('repo-delete-btn').addEventListener(
|
254
|
+
'click', () => REPOSITORY_BROWSER.confirmDeleteFromRepository());
|
255
|
+
// Other dialog controls
|
256
|
+
this.repository_selector = document.getElementById('repository-selector');
|
257
|
+
this.repository_selector.addEventListener(
|
258
|
+
'change', () => REPOSITORY_BROWSER.selectRepository());
|
259
|
+
this.modules_table = document.getElementById('modules-table');
|
260
|
+
this.modules_count = document.getElementById('modules-count');
|
261
|
+
|
262
|
+
// Initialize the associated modals
|
263
|
+
this.add_modal = new ModalDialog('add-repository');
|
264
|
+
this.add_modal.ok.addEventListener(
|
265
|
+
'click', () => REPOSITORY_BROWSER.registerRepository());
|
266
|
+
this.add_modal.cancel.addEventListener(
|
267
|
+
'click', () => REPOSITORY_BROWSER.add_modal.hide());
|
268
|
+
|
269
|
+
this.access_modal = new ModalDialog('access-repository');
|
270
|
+
this.access_modal.ok.addEventListener(
|
271
|
+
'click', () => REPOSITORY_BROWSER.accessRepository());
|
272
|
+
this.access_modal.cancel.addEventListener(
|
273
|
+
'click', () => REPOSITORY_BROWSER.access_modal.hide());
|
274
|
+
|
275
|
+
this.store_modal = new ModalDialog('store-in-repository');
|
276
|
+
this.store_modal.ok.addEventListener(
|
277
|
+
'click', () => REPOSITORY_BROWSER.storeModel());
|
278
|
+
this.store_modal.cancel.addEventListener(
|
279
|
+
'click', () => REPOSITORY_BROWSER.store_modal.hide());
|
280
|
+
|
281
|
+
this.store_bb_modal = new ModalDialog('store-bb-in-repository');
|
282
|
+
this.store_bb_modal.ok.addEventListener(
|
283
|
+
'click', () => REPOSITORY_BROWSER.storeBlackBoxModel());
|
284
|
+
this.store_bb_modal.cancel.addEventListener(
|
285
|
+
'click', () => REPOSITORY_BROWSER.store_bb_modal.hide());
|
286
|
+
|
287
|
+
this.include_modal = new ModalDialog('include');
|
288
|
+
this.include_modal.ok.addEventListener(
|
289
|
+
'click', () => REPOSITORY_BROWSER.performInclusion());
|
290
|
+
this.include_modal.cancel.addEventListener(
|
291
|
+
'click', () => REPOSITORY_BROWSER.cancelInclusion());
|
292
|
+
this.include_modal.element('actor').addEventListener(
|
293
|
+
'blur', () => REPOSITORY_BROWSER.updateActors());
|
294
|
+
|
295
|
+
this.confirm_load_modal = new ModalDialog('confirm-load-from-repo');
|
296
|
+
this.confirm_load_modal.ok.addEventListener(
|
297
|
+
'click', () => REPOSITORY_BROWSER.loadModuleAsModel());
|
298
|
+
this.confirm_load_modal.cancel.addEventListener(
|
299
|
+
'click', () => REPOSITORY_BROWSER.confirm_load_modal.hide());
|
300
|
+
|
301
|
+
this.confirm_delete_modal = new ModalDialog('confirm-delete-from-repo');
|
302
|
+
this.confirm_delete_modal.ok.addEventListener(
|
303
|
+
'click', () => REPOSITORY_BROWSER.deleteFromRepository());
|
304
|
+
this.confirm_delete_modal.cancel.addEventListener(
|
305
|
+
'click', () => REPOSITORY_BROWSER.confirm_delete_modal.hide());
|
306
|
+
}
|
307
|
+
|
308
|
+
reset() {
|
309
|
+
super.reset();
|
310
|
+
this.last_time_selected = 0;
|
311
|
+
}
|
312
|
+
|
313
|
+
enterKey() {
|
314
|
+
// Open "edit properties" dialog for the selected entity
|
315
|
+
const srl = this.modules_table.getElementsByClassName('sel-set');
|
316
|
+
if(srl.length > 0) {
|
317
|
+
const r = this.modules_table.rows[srl[0].rowIndex];
|
318
|
+
if(r) {
|
319
|
+
// Ensure that click will be interpreted as double-click
|
320
|
+
this.last_time_selected = Date.now();
|
321
|
+
r.dispatchEvent(new Event('click'));
|
322
|
+
}
|
323
|
+
}
|
324
|
+
}
|
325
|
+
|
326
|
+
upDownKey(dir) {
|
327
|
+
// Select row above or below the selected one (if possible)
|
328
|
+
const srl = this.modules_table.getElementsByClassName('sel-set');
|
329
|
+
if(srl.length > 0) {
|
330
|
+
const r = this.modules_table.rows[srl[0].rowIndex + dir];
|
331
|
+
if(r) {
|
332
|
+
UI.scrollIntoView(r);
|
333
|
+
r.dispatchEvent(new Event('click'));
|
334
|
+
}
|
335
|
+
}
|
336
|
+
}
|
337
|
+
|
338
|
+
get isLocalHost() {
|
339
|
+
// Returns TRUE if first repository on the list is 'local host'
|
340
|
+
return this.repositories.length > 0 &&
|
341
|
+
this.repositories[0].name === 'local host';
|
342
|
+
}
|
343
|
+
|
344
|
+
getRepositories() {
|
345
|
+
// Gets the list of repository names from the server
|
346
|
+
this.repositories.length = 0;
|
347
|
+
fetch('repo/', postData({action: 'list'}))
|
348
|
+
.then((response) => {
|
349
|
+
if(!response.ok) {
|
350
|
+
UI.alert(`ERROR ${response.status}: ${response.statusText}`);
|
351
|
+
}
|
352
|
+
return response.text();
|
353
|
+
})
|
354
|
+
.then((data) => {
|
355
|
+
if(UI.postResponseOK(data)) {
|
356
|
+
// NOTE: trim to prevent empty name strings
|
357
|
+
const rl = data.trim().split('\n');
|
358
|
+
for(let i = 0; i < rl.length; i++) {
|
359
|
+
this.addRepository(rl[i].trim());
|
360
|
+
}
|
361
|
+
}
|
362
|
+
// NOTE: set index to first repository on list (typically local host)
|
363
|
+
// unless the list is empty
|
364
|
+
this.repository_index = Math.min(0, this.repositories.length - 1);
|
365
|
+
this.updateDialog();
|
366
|
+
})
|
367
|
+
.catch((err) => UI.warn(UI.WARNING.NO_CONNECTION, err));
|
368
|
+
}
|
369
|
+
|
370
|
+
addRepository(name) {
|
371
|
+
// Adds repository if name is unique and valid
|
372
|
+
let r = null,
|
373
|
+
can_store = false;
|
374
|
+
if(name.endsWith('+')) {
|
375
|
+
can_store = true;
|
376
|
+
name = name.slice(0, -1);
|
377
|
+
}
|
378
|
+
if(this.repositoryByName(name)) {
|
379
|
+
UI.warn(`Multiple listings for repository "${name}"`);
|
380
|
+
} else if(!UI.validName(name)) {
|
381
|
+
UI.warn(`Invalid name for repository "${name}"`);
|
382
|
+
} else {
|
383
|
+
r = new Repository(name, can_store);
|
384
|
+
this.repositories.push(r);
|
385
|
+
this.repository_index = this.repositories.length - 1;
|
386
|
+
r.getModuleList();
|
387
|
+
}
|
388
|
+
return r;
|
389
|
+
}
|
390
|
+
|
391
|
+
removeRepository() {
|
392
|
+
// Removes selected repository from list
|
393
|
+
// NOTE: do not remove the first item (local host)
|
394
|
+
if(this.repository_index < 1) return;
|
395
|
+
fetch('repo/', postData({
|
396
|
+
action: 'remove',
|
397
|
+
repo: this.repositories[this.repository_index].name
|
398
|
+
}))
|
399
|
+
.then((response) => {
|
400
|
+
if(!response.ok) {
|
401
|
+
UI.alert(`ERROR ${response.status}: ${response.statusText}`);
|
402
|
+
}
|
403
|
+
return response.text();
|
404
|
+
})
|
405
|
+
.then((data) => {
|
406
|
+
if(data !== this.repositories[this.repository_index].name) {
|
407
|
+
UI.alert('ERROR: ' + data);
|
408
|
+
} else {
|
409
|
+
this.repositories.splice(this.repository_index, 1);
|
410
|
+
this.repository_index = -1;
|
411
|
+
this.updateDialog();
|
412
|
+
}
|
413
|
+
})
|
414
|
+
.catch((err) => UI.warn(UI.WARNING.NO_CONNECTION, err));
|
415
|
+
}
|
416
|
+
|
417
|
+
promptForRepository() {
|
418
|
+
// Opens "Add repository" dialog
|
419
|
+
this.add_modal.element('name').value = '';
|
420
|
+
this.add_modal.element('url').value = '';
|
421
|
+
this.add_modal.element('token').value = '';
|
422
|
+
this.add_modal.show('name');
|
423
|
+
}
|
424
|
+
|
425
|
+
registerRepository() {
|
426
|
+
// Checks whether URL defines a Linny-R repository, and if so, adds it
|
427
|
+
fetch('repo/', postData({
|
428
|
+
action: 'add',
|
429
|
+
repo: this.add_modal.element('name').value,
|
430
|
+
url: this.add_modal.element('url').value,
|
431
|
+
token: this.add_modal.element('token').value
|
432
|
+
}))
|
433
|
+
.then((response) => {
|
434
|
+
if(!response.ok) {
|
435
|
+
UI.alert(`ERROR ${response.status}: ${response.statusText}`);
|
436
|
+
}
|
437
|
+
return response.text();
|
438
|
+
})
|
439
|
+
.then((data) => {
|
440
|
+
if(UI.postResponseOK(data) &&
|
441
|
+
data === this.add_modal.element('name').value) {
|
442
|
+
console.log('Verified URL for', data);
|
443
|
+
this.add_modal.hide();
|
444
|
+
// NOTE: assume that the token is valid when it is 32 hex digits
|
445
|
+
// (so no real validity check on the remote server; this will reveal
|
446
|
+
// itself when actually trying to store a model on that server)
|
447
|
+
let can_store = '',
|
448
|
+
re = /[0-9A-Fa-f]{32}/g;
|
449
|
+
if(re.test(this.add_modal.element('token').value)) can_store = '+';
|
450
|
+
this.addRepository(data + can_store);
|
451
|
+
this.updateDialog();
|
452
|
+
}
|
453
|
+
})
|
454
|
+
.catch((err) => UI.warn(UI.WARNING.NO_CONNECTION, err));
|
455
|
+
}
|
456
|
+
|
457
|
+
promptForAccess() {
|
458
|
+
// Opens "Access repository" dialog for selected repository
|
459
|
+
if(this.repository_index >= 0 &&
|
460
|
+
document.getElementById('repo-access-btn').classList.contains('enab')) {
|
461
|
+
const r = this.repositories[this.repository_index];
|
462
|
+
this.access_modal.element('name').innerText = r.name;
|
463
|
+
this.access_modal.element('token').value = '';
|
464
|
+
this.access_modal.show('token');
|
465
|
+
}
|
466
|
+
}
|
467
|
+
|
468
|
+
accessRepository() {
|
469
|
+
// Sets token for selected repository
|
470
|
+
if(this.repository_index < 0) return;
|
471
|
+
let r = this.repositories[this.repository_index],
|
472
|
+
e = this.access_modal.element('token'),
|
473
|
+
t = e.value.trim(),
|
474
|
+
re = /[0-9A-Fa-f]{32}/g;
|
475
|
+
if(!re.test(t)) {
|
476
|
+
UI.warn('Token must be a 32-digit hexadecimal number');
|
477
|
+
e.focus();
|
478
|
+
return;
|
479
|
+
}
|
480
|
+
fetch('repo/', postData({action: 'access', repo: r.name, token: t}))
|
481
|
+
.then((response) => {
|
482
|
+
if(!response.ok) {
|
483
|
+
UI.alert(`ERROR ${response.status}: ${response.statusText}`);
|
484
|
+
}
|
485
|
+
return response.text();
|
486
|
+
})
|
487
|
+
.then((data) => {
|
488
|
+
if(UI.postResponseOK(data, true)) {
|
489
|
+
r.authorized = true;
|
490
|
+
this.access_modal.hide();
|
491
|
+
this.updateDialog();
|
492
|
+
}
|
493
|
+
})
|
494
|
+
.catch((err) => UI.warn(UI.WARNING.NO_CONNECTION, err));
|
495
|
+
}
|
496
|
+
|
497
|
+
selectRepository() {
|
498
|
+
this.repository_index = parseInt(this.repository_selector.value);
|
499
|
+
this.module_index = -1;
|
500
|
+
if(this.repository_index >= 0) {
|
501
|
+
const r = this.repositories[this.repository_index];
|
502
|
+
r.getModuleList();
|
503
|
+
} else {
|
504
|
+
this.updateDialog();
|
505
|
+
}
|
506
|
+
}
|
507
|
+
|
508
|
+
selectModule(n) {
|
509
|
+
// Select module in list; if double-clicked, load it and hide dialog
|
510
|
+
if(this.repository_index >= 0) {
|
511
|
+
const
|
512
|
+
now = Date.now(),
|
513
|
+
dt = now - this.last_time_selected;
|
514
|
+
this.last_time_selected = now;
|
515
|
+
if(n === this.module_index) {
|
516
|
+
// Consider click to be "double" if it occurred less than 300 ms ago
|
517
|
+
if(dt < 300) {
|
518
|
+
this.last_time_selected = 0;
|
519
|
+
this.includeModule();
|
520
|
+
return;
|
521
|
+
}
|
522
|
+
}
|
523
|
+
this.module_index = n;
|
524
|
+
} else {
|
525
|
+
this.module_index = -1;
|
526
|
+
}
|
527
|
+
this.updateModulesTable();
|
528
|
+
}
|
529
|
+
|
530
|
+
showInfo(n, shift) {
|
531
|
+
if(this.repository_index >= 0) {
|
532
|
+
const r = this.repositories[this.repository_index];
|
533
|
+
if(n < r.module_names.length) {
|
534
|
+
const m = new Module(r.module_names[n]);
|
535
|
+
if(shift) {
|
536
|
+
// Only get data from server when Shift key is pressed
|
537
|
+
r.getModuleInfo(n, m);
|
538
|
+
} else {
|
539
|
+
// Only update the status line
|
540
|
+
DOCUMENTATION_MANAGER.update(m, shift);
|
541
|
+
}
|
542
|
+
}
|
543
|
+
}
|
544
|
+
}
|
545
|
+
|
546
|
+
updateModulesTable() {
|
547
|
+
// Refresh the module table
|
548
|
+
let mcount = 0;
|
549
|
+
const trl = [];
|
550
|
+
if(this.repository_index >= 0) {
|
551
|
+
const r = this.repositories[this.repository_index];
|
552
|
+
mcount = r.module_names.length;
|
553
|
+
for(let i = 0; i < mcount; i++) {
|
554
|
+
const n = r.module_names[i],
|
555
|
+
sel = (i === this.module_index ? ' sel-set' : '');
|
556
|
+
trl.push('<tr class="module', sel, '" title="',
|
557
|
+
n, '" onclick="REPOSITORY_BROWSER.selectModule(', i,
|
558
|
+
');" onmouseover="REPOSITORY_BROWSER.showInfo(\'', i,
|
559
|
+
'\', event.shiftKey);">',
|
560
|
+
'<td class="v-name">', n, '</td></tr>');
|
561
|
+
}
|
562
|
+
}
|
563
|
+
this.modules_table.innerHTML = trl.join('');
|
564
|
+
this.modules_count.innerHTML = pluralS(mcount, 'module');
|
565
|
+
if(this.module_index >= 0) {
|
566
|
+
UI.enableButtons('repo-load repo-include');
|
567
|
+
// NOTE: only allow deletion from local host repository
|
568
|
+
if(this.repository_index === 0 && this.isLocalHost) {
|
569
|
+
UI.enableButtons(' repo-delete');
|
570
|
+
} else {
|
571
|
+
UI.disableButtons(' repo-delete');
|
572
|
+
}
|
573
|
+
} else {
|
574
|
+
UI.disableButtons('repo-load repo-include repo-delete');
|
575
|
+
}
|
576
|
+
}
|
577
|
+
|
578
|
+
updateDialog() {
|
579
|
+
// Refreshes all dialog elements
|
580
|
+
const ol = [];
|
581
|
+
for(let i = 0; i < this.repositories.length; i++) {
|
582
|
+
ol.push('<option value="', i,
|
583
|
+
(i === this.repository_index ? '"selected="selected' : ''),
|
584
|
+
'">', this.repositories[i].name , '</option>');
|
585
|
+
}
|
586
|
+
this.repository_selector.innerHTML = ol.join('');
|
587
|
+
UI.disableButtons('repo-access repo-remove repo-store');
|
588
|
+
// NOTE: on remote installation, do not allow add/remove/store
|
589
|
+
if(!this.isLocalHost) {
|
590
|
+
UI.disableButtons('repo-add');
|
591
|
+
} else if(this.repository_index >= 0) {
|
592
|
+
const r = this.repositories[this.repository_index];
|
593
|
+
if(r.authorized) {
|
594
|
+
UI.enableButtons('repo-store');
|
595
|
+
} else {
|
596
|
+
UI.enableButtons('repo-access');
|
597
|
+
}
|
598
|
+
if(r.name !== 'local host') {
|
599
|
+
// NOTE: cannot remove 'local host'
|
600
|
+
UI.enableButtons('repo-remove');
|
601
|
+
}
|
602
|
+
}
|
603
|
+
this.updateModulesTable();
|
604
|
+
}
|
605
|
+
|
606
|
+
promptForInclusion(repo, file, node) {
|
607
|
+
// Add entities defined in the parsed XML tree with root `node`
|
608
|
+
IO_CONTEXT = new IOContext(repo, file, node);
|
609
|
+
const md = this.include_modal;
|
610
|
+
md.element('name').innerHTML = IO_CONTEXT.file_name;
|
611
|
+
md.element('prefix').value = '';
|
612
|
+
md.element('actor').value = '';
|
613
|
+
md.element('scroll-area').innerHTML = IO_CONTEXT.parameterTable;
|
614
|
+
md.show('prefix');
|
615
|
+
}
|
616
|
+
|
617
|
+
updateActors() {
|
618
|
+
// Adds actor (if specified) to model, and then updates the selector options
|
619
|
+
// for each actor binding selector
|
620
|
+
if(!IO_CONTEXT) return;
|
621
|
+
const
|
622
|
+
aname = this.include_modal.element('actor').value.trim(),
|
623
|
+
aid = UI.nameToID(aname);
|
624
|
+
if(aname && !MODEL.actors.hasOwnProperty(aid)) {
|
625
|
+
MODEL.addActor(aname);
|
626
|
+
for(let id in IO_CONTEXT.bindings)
|
627
|
+
if(IO_CONTEXT.bindings.hasOwnProperty(id)) {
|
628
|
+
const b = IO_CONTEXT.bindings[id];
|
629
|
+
if(b.entity_type === 'Actor' && b.io_type === 1) {
|
630
|
+
const o = new Option(aname, aid);
|
631
|
+
o.innerHTML = aname;
|
632
|
+
document.getElementById(b.id).appendChild(o);
|
633
|
+
}
|
634
|
+
}
|
635
|
+
}
|
636
|
+
}
|
637
|
+
|
638
|
+
parameterBinding(name) {
|
639
|
+
// Returns the selected option (as DOM element) of the the parameter
|
640
|
+
// selector identified by its element name (!) in the Include modal
|
641
|
+
const lst = document.getElementsByName(name);
|
642
|
+
let e = null;
|
643
|
+
for(let i = 0; i < lst.length; i++) {
|
644
|
+
if(lst[i].type.indexOf('select') === 0) {
|
645
|
+
e = lst[i];
|
646
|
+
break;
|
647
|
+
}
|
648
|
+
}
|
649
|
+
if(!e) UI.alert(`Parameter selector "${b.id}" not found`);
|
650
|
+
return e;
|
651
|
+
}
|
652
|
+
|
653
|
+
performInclusion() {
|
654
|
+
// Includes the selected model as "module" cluster in the model;
|
655
|
+
// this is effectuated by "re-initializing" the current model using
|
656
|
+
// the XML of the model-to-be-included with the contextualization as
|
657
|
+
// indicated by the modeler
|
658
|
+
if(!IO_CONTEXT) {
|
659
|
+
UI.alert('Cannot include module without context');
|
660
|
+
return;
|
661
|
+
}
|
662
|
+
const pref = this.include_modal.element('prefix');
|
663
|
+
IO_CONTEXT.prefix = pref.value.trim();
|
664
|
+
if(!UI.validName(IO_CONTEXT.prefix)) {
|
665
|
+
UI.warn(`Invalid cluster name "${IO_CONTEXT.prefix}"`);
|
666
|
+
pref.focus();
|
667
|
+
return;
|
668
|
+
}
|
669
|
+
// NOTE: prefix must not already be in use as entity name
|
670
|
+
let obj = MODEL.objectByName(IO_CONTEXT.prefix);
|
671
|
+
if(obj) {
|
672
|
+
UI.warningEntityExists(obj, IO_CONTEXT.prefix);
|
673
|
+
pref.value = '';
|
674
|
+
pref.focus();
|
675
|
+
return;
|
676
|
+
}
|
677
|
+
IO_CONTEXT.actor_name = this.include_modal.element('actor').value.trim();
|
678
|
+
MODEL.clearSelection();
|
679
|
+
IO_CONTEXT.bindParameters();
|
680
|
+
// NOTE: including may affect focal cluster, so store it...
|
681
|
+
const fc = MODEL.focal_cluster;
|
682
|
+
MODEL.initFromXML(IO_CONTEXT.xml);
|
683
|
+
// ... and restore it afterwards
|
684
|
+
MODEL.focal_cluster = fc;
|
685
|
+
let counts = `: ${pluralS(IO_CONTEXT.added_nodes.length, 'node')}, ` +
|
686
|
+
pluralS(IO_CONTEXT.added_links.length, 'link');
|
687
|
+
if(IO_CONTEXT.superseded.length > 0) {
|
688
|
+
counts += ` (superseded ${IO_CONTEXT.superseded.length})`;
|
689
|
+
console.log('SUPERSEDED:', IO_CONTEXT.superseded);
|
690
|
+
}
|
691
|
+
UI.notify(`Model <tt>${IO_CONTEXT.file_name}</tt> included from ` +
|
692
|
+
`<strong>${IO_CONTEXT.repo_name}</strong>${counts}`);
|
693
|
+
// Get the containing cluster
|
694
|
+
obj = MODEL.objectByName(IO_CONTEXT.clusterName);
|
695
|
+
// Position it in the focal cluster
|
696
|
+
if(obj instanceof Cluster) {
|
697
|
+
obj.x = IO_CONTEXT.centroid_x;
|
698
|
+
obj.y = IO_CONTEXT.centroid_y;
|
699
|
+
obj.clearAllProcesses();
|
700
|
+
} else {
|
701
|
+
UI.alert('Include failed to create a cluster');
|
702
|
+
}
|
703
|
+
// Reset the IO context
|
704
|
+
IO_CONTEXT = null;
|
705
|
+
this.include_modal.hide();
|
706
|
+
MODEL.cleanUpActors();
|
707
|
+
MODEL.focal_cluster.clearAllProcesses();
|
708
|
+
UI.drawDiagram(MODEL);
|
709
|
+
// Select the newly added cluster
|
710
|
+
if(obj) MODEL.select(obj);
|
711
|
+
// Update dataset manager if shown (as new datasets may have been added)
|
712
|
+
if(DATASET_MANAGER.visible) DATASET_MANAGER.updateDialog();
|
713
|
+
}
|
714
|
+
|
715
|
+
cancelInclusion() {
|
716
|
+
// Clears the IO context and closes the inclusion dialog
|
717
|
+
IO_CONTEXT = null;
|
718
|
+
this.include_modal.hide();
|
719
|
+
}
|
720
|
+
|
721
|
+
promptForStoring() {
|
722
|
+
if(this.repository_index >= 0) {
|
723
|
+
this.store_modal.element('name').innerText =
|
724
|
+
this.repositories[this.repository_index].name;
|
725
|
+
this.store_modal.element('model-name').value =
|
726
|
+
this.asFileName(MODEL.name);
|
727
|
+
this.store_modal.show('model-name');
|
728
|
+
}
|
729
|
+
}
|
730
|
+
|
731
|
+
storeModel() {
|
732
|
+
if(this.repository_index >= 0) {
|
733
|
+
const
|
734
|
+
mn = this.store_modal.element('model-name').value.trim(),
|
735
|
+
r = this.repositories[this.repository_index];
|
736
|
+
if(mn.length > 1) {
|
737
|
+
r.storeModelAsModule(mn);
|
738
|
+
this.store_modal.hide();
|
739
|
+
}
|
740
|
+
}
|
741
|
+
}
|
742
|
+
|
743
|
+
promptForBlackBoxing() {
|
744
|
+
if(this.repository_index >= 0) {
|
745
|
+
this.store_bb_modal.element('name').innerText =
|
746
|
+
this.repositories[this.repository_index].name;
|
747
|
+
this.store_bb_modal.element('model-name').value =
|
748
|
+
this.asFileName(MODEL.name);
|
749
|
+
this.store_bb_modal.show('model-name');
|
750
|
+
}
|
751
|
+
}
|
752
|
+
|
753
|
+
storeBlackBoxModel() {
|
754
|
+
if(this.repository_index >= 0) {
|
755
|
+
const
|
756
|
+
mn = this.store_bb_modal.element('model-name').value.trim(),
|
757
|
+
r = this.repositories[this.repository_index];
|
758
|
+
if(mn.length > 1) {
|
759
|
+
// NOTE: second parameter indicates: store with "black box XML"
|
760
|
+
r.storeModelAsModule(mn, true);
|
761
|
+
this.store_bb_modal.hide();
|
762
|
+
}
|
763
|
+
}
|
764
|
+
}
|
765
|
+
|
766
|
+
loadModuleAsModel() {
|
767
|
+
// Loads selected module as model
|
768
|
+
this.confirm_load_modal.hide();
|
769
|
+
if(this.repository_index >= 0 && this.module_index >= 0) {
|
770
|
+
// NOTE: when loading new model, the stay-on-top dialogs must be reset
|
771
|
+
UI.hideStayOnTopDialogs();
|
772
|
+
const r = this.repositories[this.repository_index];
|
773
|
+
// NOTE: pass FALSE to indicate "no inclusion; load XML as model"
|
774
|
+
r.loadModule(this.module_index, false);
|
775
|
+
}
|
776
|
+
}
|
777
|
+
|
778
|
+
includeModule() {
|
779
|
+
// Includes selected module into the current model
|
780
|
+
if(this.repository_index >= 0 && this.module_index >= 0) {
|
781
|
+
const r = this.repositories[this.repository_index];
|
782
|
+
r.loadModule(this.module_index, true);
|
783
|
+
}
|
784
|
+
}
|
785
|
+
|
786
|
+
confirmLoadModuleAsModel() {
|
787
|
+
// Prompts modeler to confirm loading the selected module as model
|
788
|
+
if(this.repository_index >= 0 && this.module_index >= 0 &&
|
789
|
+
document.getElementById('repo-load-btn').classList.contains('enab')) {
|
790
|
+
const r = this.repositories[this.repository_index];
|
791
|
+
this.confirm_load_modal.element('mod-name').innerText =
|
792
|
+
r.module_names[this.module_index];
|
793
|
+
this.confirm_load_modal.show();
|
794
|
+
}
|
795
|
+
}
|
796
|
+
|
797
|
+
confirmDeleteFromRepository() {
|
798
|
+
// Prompts modeler to confirm deletion of the selected module
|
799
|
+
if(this.repository_index >= 0 && this.module_index >= 0 &&
|
800
|
+
document.getElementById('repo-delete-btn').classList.contains('enab')) {
|
801
|
+
const r = this.repositories[this.repository_index];
|
802
|
+
this.confirm_delete_modal.element('name').innerText = r.name;
|
803
|
+
this.confirm_delete_modal.element('mod-name').innerText =
|
804
|
+
r.module_names[this.module_index];
|
805
|
+
this.confirm_delete_modal.show();
|
806
|
+
}
|
807
|
+
}
|
808
|
+
|
809
|
+
deleteFromRepository() {
|
810
|
+
// Deletes the selected modulle from the current repository
|
811
|
+
if(this.repository_index >= 0 && this.module_index >= 0) {
|
812
|
+
const r = this.repositories[this.repository_index];
|
813
|
+
if(r) r.deleteModule(this.module_index);
|
814
|
+
this.confirm_delete_modal.hide();
|
815
|
+
}
|
816
|
+
}
|
817
|
+
|
818
|
+
} // END of class GUIRepositoryBrowser
|
819
|
+
|