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,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
+