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.
Files changed (50) hide show
  1. package/README.md +162 -74
  2. package/package.json +1 -1
  3. package/server.js +145 -49
  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/octaeder.svg +993 -0
  21. package/static/images/sort-asc-lead.png +0 -0
  22. package/static/images/sort-asc.png +0 -0
  23. package/static/images/sort-desc-lead.png +0 -0
  24. package/static/images/sort-desc.png +0 -0
  25. package/static/images/sort-not.png +0 -0
  26. package/static/index.html +72 -647
  27. package/static/linny-r.css +199 -417
  28. package/static/scripts/linny-r-gui-actor-manager.js +340 -0
  29. package/static/scripts/linny-r-gui-chart-manager.js +944 -0
  30. package/static/scripts/linny-r-gui-constraint-editor.js +681 -0
  31. package/static/scripts/linny-r-gui-controller.js +4005 -0
  32. package/static/scripts/linny-r-gui-dataset-manager.js +1176 -0
  33. package/static/scripts/linny-r-gui-documentation-manager.js +739 -0
  34. package/static/scripts/linny-r-gui-equation-manager.js +307 -0
  35. package/static/scripts/linny-r-gui-experiment-manager.js +1944 -0
  36. package/static/scripts/linny-r-gui-expression-editor.js +449 -0
  37. package/static/scripts/linny-r-gui-file-manager.js +392 -0
  38. package/static/scripts/linny-r-gui-finder.js +727 -0
  39. package/static/scripts/linny-r-gui-model-autosaver.js +230 -0
  40. package/static/scripts/linny-r-gui-monitor.js +448 -0
  41. package/static/scripts/linny-r-gui-paper.js +2789 -0
  42. package/static/scripts/linny-r-gui-receiver.js +323 -0
  43. package/static/scripts/linny-r-gui-repository-browser.js +819 -0
  44. package/static/scripts/linny-r-gui-scale-unit-manager.js +244 -0
  45. package/static/scripts/linny-r-gui-sensitivity-analysis.js +778 -0
  46. package/static/scripts/linny-r-gui-undo-redo.js +560 -0
  47. package/static/scripts/linny-r-model.js +27 -11
  48. package/static/scripts/linny-r-utils.js +17 -2
  49. package/static/scripts/linny-r-vm.js +31 -12
  50. package/static/scripts/linny-r-gui.js +0 -16761
@@ -0,0 +1,739 @@
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-docu.js) provides the GUI functionality
9
+ for the Linny-R model documentation manager: the draggable dialog that allows
10
+ viewing and editing documentation text for model entities.
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 DocumentationManager
37
+ class DocumentationManager {
38
+ constructor() {
39
+ this.dialog = UI.draggableDialog('documentation');
40
+ UI.resizableDialog('documentation', 'DOCUMENTATION_MANAGER');
41
+ this.close_btn = document.getElementById('documentation-close-btn');
42
+ this.title = document.getElementById('docu-item-title');
43
+ this.editor = document.getElementById('docu-editor');
44
+ this.viewer = document.getElementById('docu-viewer');
45
+ this.edit_btn = document.getElementById('docu-edit-btn');
46
+ this.copy_btn = document.getElementById('docu-copy-btn');
47
+ this.model_info_btn = document.getElementById('docu-model-info-btn');
48
+ this.compare_btn = document.getElementById('compare-btn');
49
+ this.save_btn = document.getElementById('docu-save-btn');
50
+ this.cancel_btn = document.getElementById('docu-cancel-btn');
51
+ this.info_btn = document.getElementById('docu-info-btn');
52
+ this.resume_btn = document.getElementById('docu-resume-btn');
53
+ this.symbols = document.getElementById('docu-symbols');
54
+ this.message_hint = document.getElementById('docu-message-hint');
55
+ // Make toolbar buttons responsive
56
+ this.close_btn.addEventListener('click',
57
+ (event) => UI.toggleDialog(event));
58
+ this.edit_btn.addEventListener('click',
59
+ () => DOCUMENTATION_MANAGER.editMarkup());
60
+ this.model_info_btn.addEventListener('click',
61
+ () => DOCUMENTATION_MANAGER.showAllDocumentation());
62
+ this.copy_btn.addEventListener('click',
63
+ () => DOCUMENTATION_MANAGER.copyDocToClipboard());
64
+ this.save_btn.addEventListener('click',
65
+ () => DOCUMENTATION_MANAGER.saveMarkup());
66
+ this.cancel_btn.addEventListener('click',
67
+ () => DOCUMENTATION_MANAGER.stopEditing());
68
+ this.info_btn.addEventListener('click',
69
+ () => DOCUMENTATION_MANAGER.showGuidelines());
70
+ this.resume_btn.addEventListener('click',
71
+ () => DOCUMENTATION_MANAGER.hideGuidelines());
72
+ const
73
+ sym_btns = document.getElementsByClassName('docu-sym'),
74
+ insert_sym = (event) =>
75
+ DOCUMENTATION_MANAGER.insertSymbol(event.target.innerHTML);
76
+ for(let i = 0; i < sym_btns.length; i++) {
77
+ sym_btns[i].addEventListener('click', insert_sym);
78
+ }
79
+ // NOTE: Compare button opens a modal dialog to prompt for file
80
+ this.compare_btn.addEventListener('click',
81
+ () => DOCUMENTATION_MANAGER.comparison_modal.show());
82
+ this.comparison_modal = new ModalDialog('comparison');
83
+ this.comparison_modal.ok.addEventListener('click',
84
+ () => FILE_MANAGER.loadModelToCompare());
85
+ this.comparison_modal.ok.addEventListener('click',
86
+ () => DOCUMENTATION_MANAGER.comparison_modal.hide());
87
+
88
+ // Intitialize markup rewriting rules
89
+ this.rules = [
90
+ { // No HTML entities
91
+ pattern: /&/g,
92
+ rewrite: '&amp;'
93
+ },
94
+ { // No HTML tags
95
+ pattern: /</g,
96
+ rewrite: '&lt;'
97
+ },
98
+ { // URLs become anchors
99
+ pattern: /((http|https):\/\/[^ "]+)/gmi,
100
+ rewrite: '<a href="$1" target="_blank">$1</a>'
101
+ },
102
+ { // 3 or more trailing spaces before a newline become a line break
103
+ pattern: / {3,}$/gm,
104
+ rewrite: '<br>'
105
+ },
106
+ { // Text following ^ (until next ^ or whitespace) becomes superscript
107
+ pattern: /\^([^\s\^]*)[\^]?/g,
108
+ rewrite: '<sup>$1</sup>'
109
+ },
110
+ { // Text following _ (until next _ or whitespace) becomes subscript
111
+ pattern: /_([^\s_]*)_?/g,
112
+ rewrite: '<sub>$1</sub>'
113
+ },
114
+
115
+ // NOTE: all other patterns are "enclosure" patterns
116
+
117
+ { // Unlike MediaWiki, more = signs make BIGGER headers
118
+ pattern: /===([^\s].*[^\s]?)===/g,
119
+ rewrite: '<h1>$1</h1>'
120
+ },
121
+ {
122
+ pattern: /==([^\s].*[^\s]?)==/g,
123
+ rewrite: '<h2>$1</h2>'
124
+ },
125
+ {
126
+ pattern: /=([^\s].*[^\s]?)=/g,
127
+ rewrite: '<h3>$1</h3>'
128
+ },
129
+ { // Double asterisks make **bold face** print
130
+ pattern: /\*\*([^\s][^\*]*[^\s]?)\*\*/g,
131
+ rewrite: '<strong>$1</strong>'
132
+ },
133
+ { // Single asterisk makes *italic* print
134
+ pattern: /\*([^\s][^\*]*[^\s]?)\*/g,
135
+ rewrite: '<em>$1</em>'
136
+ },
137
+ { // Double minus makes deleted text (red + strike-through)
138
+ pattern: /--([^\s].*[^\s]?)--/g,
139
+ rewrite: '<del>$1</del>'
140
+ },
141
+ { // Double plus makes inserted text (blue + underline)
142
+ pattern: /\+\+([^\s].*[^\s]?)\+\+/g,
143
+ rewrite: '<ins>$1</ins>'
144
+ },
145
+ { // Double grave makes highlighted text (yellow text background)
146
+ pattern: /``([^`]+)``/g,
147
+ rewrite: '<cite>$1</cite>'
148
+ },
149
+ { // Single grave makes monospaced text
150
+ pattern: /`([^`]+)`/g,
151
+ rewrite: '<tt>$1</tt>'
152
+ },
153
+ ];
154
+
155
+ // Default content to display when no entity is being viewed
156
+ this.about_linny_r = `
157
+ <div style="font-family: sans-serif; font-size: 10px; ">
158
+ <img src="images/logo.png" style="height:25px; margin-right: 8px">
159
+ <div style="display: inline-block; min-height: 20px;
160
+ vertical-align: top; padding-top: 8px">
161
+ [LINNY_R_VERSION]
162
+ </div>
163
+ </div>
164
+ <div style="font-family: serif; font-size: 12px">
165
+ <p><a href="https://linny-r.info" target="blank">Documentation
166
+ on Linny-R</a> is still scant, but you can learn a lot by
167
+ moving the cursor over buttons, and read the tool-tips that then typically
168
+ will appear.
169
+ </p>
170
+ <p>The primary function of this dialog is to allow you to document a model.
171
+ As you <em><strong>hold down the</em><span style="font: 11px sans-serif">
172
+ Shift</span><em> key</strong></em>, and then move the cursor over a model
173
+ entity (nodes or links in the diagram, but also actors, datasets, charts,
174
+ experiments or modules listed in a dialog), annotations (if any) will
175
+ appear here.
176
+ </p>
177
+ <p>To add or edit an annotation, release the
178
+ <span style="font: 11px sans-serif">Shift</span> key, and then
179
+ click on the <span style="font: 11px sans-serif">Edit</span> button in the
180
+ left corner below.
181
+ </p>
182
+ </div>`;
183
+
184
+ // Markup guidelines to display when modeler clicks on the info-button
185
+ this.markup_guide = `
186
+ <h3>Linny-R Markup Conventions</h3>
187
+ <p>You can format your documentation text using these markup conventions:</p>
188
+ <table style="width: 100%; table-layout: fixed">
189
+ <tr>
190
+ <td class="markup">*italic*, **bold**, or ***both***</td>
191
+ <td class="markdown">
192
+ <em>italic</em>, <strong>bold</strong>, or <em><strong>both</strong></em>
193
+ </td>
194
+ </tr>
195
+ <tr>
196
+ <td class="markup">` +
197
+ '``highlighted text``' + `, ++new text++, or --deleted text--
198
+ </td>
199
+ <td class="markdown">
200
+ <cite>highlighted text</cite>, <ins>new text</ins>,
201
+ or <del>deleted text</del>
202
+ </td>
203
+ </tr>
204
+ <tr>
205
+ <td class="markup">
206
+ ^super^script and _sub_script, but also m^3 and CO_2 shorthand
207
+ </td>
208
+ <td class="markdown">
209
+ <sup>super</sup>script and <sub>sub</sub>script,
210
+ but also m<sup>3</sup> and CO<sub>2</sub> shorthand
211
+ </td>
212
+ </tr>
213
+ <tr>
214
+ <td class="markup">URLs become links: https://linny-r.org</td>
215
+ <td class="markdown">URLs become links:
216
+ <a href="https://linny-r.org" target="_blank">https://linny-r.org</a>
217
+ </td>
218
+ </tr>
219
+ <tr>
220
+ <td class="markup">
221
+ Blank lines<br><br>separate paragraphs;<br>single line breaks do not.
222
+ </td>
223
+ <td class="markdown">
224
+ <p>Blank lines</p>
225
+ <p>separate paragraphs; single line breaks do not.</p>
226
+ </td>
227
+ </tr>
228
+ <tr>
229
+ <td class="markup">List items start with a dash<br>- like this,<br>
230
+ - until the next item,<br>&nbsp;&nbsp;or a blank line.<br><br>
231
+ Numbered list items start with digit-period-space<br>
232
+ 3. like this,<br>
233
+ 3. but the numbering<br>&nbsp;&nbsp;&nbsp;always starts at 1.
234
+ </td>
235
+ <td class="markdown">
236
+ <p>List items start with a dash</p>
237
+ <ul>
238
+ <li>like this,</li>
239
+ <li>until the next item, or a blank line.</li>
240
+ </ul>
241
+ <p>Numbered list items start with digit-period-space</p>
242
+ <ol>
243
+ <li>like this,</li>
244
+ <li>but the numbering always starts at 1.</li>
245
+ </ol>
246
+ </td>
247
+ </tr>
248
+ <tr>
249
+ <td class="markup">
250
+ =Small header=<br><br>==Medium header==<br><br>===Large header===
251
+ </td>
252
+ <td class="markdown">
253
+ <h3>Small header</h3><h2>Medium header</h2><h1>Large header</h1>
254
+ </td>
255
+ </tr>
256
+ <tr>
257
+ <td class="markup">
258
+ A single line with only dashes and spaces, e.g.,<br><br>- - -<br><br>
259
+ becomes a horizontal rule.
260
+ </td>
261
+ <td class="markdown">
262
+ <p>A single line with only dashes and spaces, e.g.,</p><hr>
263
+ <p>becomes a horizontal rule.</p>
264
+ </td>
265
+ </tr>
266
+ </table>`;
267
+
268
+ // Initialize properties
269
+ this.reset();
270
+ }
271
+
272
+ reset() {
273
+ this.entity = null;
274
+ this.visible = false;
275
+ this.editing = false;
276
+ this.markup = '';
277
+ this.info_messages = [];
278
+ this.symbols.style.display = 'none';
279
+ }
280
+
281
+ clearEntity(list) {
282
+ // To be called when entities are deleted
283
+ if(list.indexOf(this.entity) >= 0) {
284
+ this.stopEditing();
285
+ this.entity = null;
286
+ this.title.innerHTML = 'Documentation';
287
+ this.viewer.innerHTML = this.about_linny_r;
288
+ }
289
+ }
290
+
291
+ checkEntity() {
292
+ // Check if entity still exists in model
293
+ const e = this.entity;
294
+ if(!e || e === MODEL) return;
295
+ if(e.hasOwnProperty('name') && !MODEL.objectByName(e.name)) {
296
+ // Clear entity if not null, but not in model
297
+ this.clearEntity([e]);
298
+ }
299
+ }
300
+
301
+ updateDialog() {
302
+ // Resizing dialog needs no special action, but entity may have been
303
+ // deleted or renamed
304
+ this.checkEntity();
305
+ if(this.entity) {
306
+ this.title.innerHTML =
307
+ `<em>${this.entity.type}:</em>&nbsp;${this.entity.displayName}`;
308
+ }
309
+ }
310
+
311
+ update(e, shift) {
312
+ // Display name of entity under cursor on the infoline, and details in
313
+ // the documentation dialog
314
+ if(!e) return;
315
+ const
316
+ et = e.type,
317
+ edn = e.displayName;
318
+ // TO DO: when debugging, display additional data for nodes on the infoline
319
+ UI.setMessage(
320
+ e instanceof NodeBox ? e.infoLineName : `<em>${et}:</em> ${edn}`);
321
+ // NOTE: update the dialog ONLY when shift is pressed (this permits modelers
322
+ // to rapidly browse comments without having to click on entities, and then
323
+ // release the shift key to move to the documentation dialog to edit)
324
+ // Moreover, the documentation dialog must be visible, and the entity must
325
+ // have the `comments` property
326
+ if(!this.editing && shift && this.visible && e.hasOwnProperty('comments')) {
327
+ this.title.innerHTML = `<em>${et}:</em>&nbsp;${edn}`;
328
+ this.entity = e;
329
+ this.markup = (e.comments ? e.comments : '');
330
+ this.editor.value = this.markup;
331
+ this.viewer.innerHTML = this.markdown;
332
+ this.edit_btn.classList.remove('disab');
333
+ this.edit_btn.classList.add('enab');
334
+ // NOTE: permit documentation of the model by raising the dialog
335
+ if(this.entity === MODEL) this.dialog.style.zIndex = 101;
336
+ }
337
+ }
338
+
339
+ rewrite(str) {
340
+ // Apply all the rewriting rules to `str`
341
+ str = '\n' + str + '\n';
342
+ this.rules.forEach(
343
+ (rule) => { str = str.replace(rule.pattern, rule.rewrite); });
344
+ return str.trim();
345
+ }
346
+
347
+ makeList(par, isp, type) {
348
+ // Split on the *global multi-line* item separator pattern
349
+ const splitter = new RegExp(isp, 'gm'),
350
+ list = par.split(splitter);
351
+ if(list.length < 2) return false;
352
+ // Now we know that the paragraph contains at least one list item line
353
+ let start = 0;
354
+ // Paragraph may start with plain text, so check using the original pattern
355
+ if(!par.match(isp)) {
356
+ // If so, retain this first part as a separate paragraph...
357
+ start = 1;
358
+ // NOTE: add it only if it contains text
359
+ par = (list[0].trim() ? `<p>${this.rewrite(list[0])}</p>` : '');
360
+ // ... and clear it as list item
361
+ list[0] = '';
362
+ } else {
363
+ par = '';
364
+ }
365
+ // Rewrite each list item fragment that contains text
366
+ for(let j = start; j < list.length; j++) {
367
+ list[j] = (list[j].trim() ? `<li>${this.rewrite(list[j])}</li>` : '');
368
+ }
369
+ // Return assembled parts
370
+ return [par, '<', type, 'l>', list.join(''), '</', type, 'l>'].join('');
371
+ }
372
+
373
+ get markdown() {
374
+ if(!this.markup) this.markup = '';
375
+ const html = this.markup.split(/\n{2,}/);
376
+ let list;
377
+ for(let i = 0; i < html.length; i++) {
378
+ // Paragraph with only dashes and spaces becomes a horizontal rule
379
+ if(html[i].match(/^( *-)+$/)) {
380
+ html[i] = '<hr>';
381
+ // Paragraph may contain a bulleted list
382
+ } else if ((list = this.makeList(html[i], /^ *- +/, 'u')) !== false) {
383
+ html[i] = list;
384
+ // Paragraph may contain a numbered list
385
+ } else if ((list = this.makeList(html[i], /^ *\d+. +/, 'o')) !== false) {
386
+ html[i] = list;
387
+ // Otherwise: default HTML paragraph
388
+ } else {
389
+ html[i] = `<p>${this.rewrite(html[i])}</p>`;
390
+ }
391
+ }
392
+ return html.join('');
393
+ }
394
+
395
+ editMarkup() {
396
+ if(this.edit_btn.classList.contains('disab')) return;
397
+ this.dialog.style.opacity = 1;
398
+ this.viewer.style.display = 'none';
399
+ this.editor.style.display = 'block';
400
+ this.edit_btn.style.display = 'none';
401
+ this.model_info_btn.style.display = 'none';
402
+ this.copy_btn.style.display = 'none';
403
+ this.compare_btn.style.display = 'none';
404
+ this.message_hint.style.display = 'none';
405
+ this.save_btn.style.display = 'block';
406
+ this.cancel_btn.style.display = 'block';
407
+ this.info_btn.style.display = 'block';
408
+ this.symbols.style.display = 'block';
409
+ this.editor.focus();
410
+ this.editing = true;
411
+ }
412
+
413
+ insertSymbol(sym) {
414
+ // Insert symbol (clicked item in list below text area) into text area
415
+ this.editor.focus();
416
+ let p = this.editor.selectionStart;
417
+ const
418
+ v = this.editor.value,
419
+ tb = v.substring(0, p),
420
+ ta = v.substring(p, v.length);
421
+ this.editor.value = `${tb}${sym}${ta}`;
422
+ p += sym.length;
423
+ this.editor.setSelectionRange(p, p);
424
+ }
425
+
426
+ saveMarkup() {
427
+ this.markup = this.editor.value.trim();
428
+ this.checkEntity();
429
+ if(this.entity) {
430
+ this.entity.comments = this.markup;
431
+ this.viewer.innerHTML = this.markdown;
432
+ if(this.entity instanceof Link) {
433
+ UI.drawLinkArrows(MODEL.focal_cluster, this.entity);
434
+ } else if(this.entity instanceof Constraint) {
435
+ UI.paper.drawConstraint(this.entity);
436
+ } else if (typeof this.entity.draw === 'function') {
437
+ // Only draw if the entity responds to that method
438
+ this.entity.draw();
439
+ }
440
+ }
441
+ this.stopEditing();
442
+ }
443
+
444
+ stopEditing() {
445
+ this.editing = false;
446
+ this.editor.style.display = 'none';
447
+ this.viewer.style.display = 'block';
448
+ this.save_btn.style.display = 'none';
449
+ this.cancel_btn.style.display = 'none';
450
+ this.info_btn.style.display = 'none';
451
+ this.symbols.style.display = 'none';
452
+ this.edit_btn.style.display = 'block';
453
+ this.model_info_btn.style.display = 'block';
454
+ this.copy_btn.style.display = 'block';
455
+ this.compare_btn.style.display = 'block';
456
+ this.message_hint.style.display = 'block';
457
+ this.dialog.style.opacity = 0.85;
458
+ }
459
+
460
+ showGuidelines() {
461
+ this.editor.style.display = 'none';
462
+ this.save_btn.style.display = 'none';
463
+ this.cancel_btn.style.display = 'none';
464
+ this.info_btn.style.display = 'none';
465
+ this.symbols.style.display = 'none';
466
+ this.viewer.innerHTML = this.markup_guide;
467
+ this.viewer.style.display = 'block';
468
+ this.resume_btn.style.display = 'block';
469
+ }
470
+
471
+ hideGuidelines() {
472
+ this.viewer.style.display = 'none';
473
+ this.resume_btn.style.display = 'none';
474
+ this.editor.style.display = 'block';
475
+ this.save_btn.style.display = 'block';
476
+ this.cancel_btn.style.display = 'block';
477
+ this.info_btn.style.display = 'block';
478
+ this.symbols.style.display = 'block';
479
+ this.viewer.innerHTML = this.editor.value.trim();
480
+ this.editor.focus();
481
+ }
482
+
483
+ addMessage(msg) {
484
+ // Append message to the info messages list
485
+ if(msg) this.info_messages.push(msg);
486
+ // Update dialog only when it is showing
487
+ if(!UI.hidden(this.dialog.id)) this.showInfoMessages(true);
488
+ }
489
+
490
+ showInfoMessages(shift) {
491
+ // Show all messages that have appeared on the status line
492
+ const
493
+ n = this.info_messages.length,
494
+ title = pluralS(n, 'message') + ' since the current model was loaded';
495
+ document.getElementById('info-line').setAttribute(
496
+ 'title', 'Status: ' + title);
497
+ if(shift && !this.editing) {
498
+ const divs = [];
499
+ for(let i = n - 1; i >= 0; i--) {
500
+ const
501
+ m = this.info_messages[i],
502
+ first = (i === n - 1 ? '-msg first' : '');
503
+ divs.push('<div><div class="', m.status, '-time">', m.time, '</div>',
504
+ '<div class="', m.status, first, '-msg">', m.text, '</div></div>');
505
+ }
506
+ this.viewer.innerHTML = divs.join('');
507
+ // Set the dialog title
508
+ this.title.innerHTML = title;
509
+ }
510
+ }
511
+
512
+ showArrowLinks(arrow) {
513
+ // Show list of links represented by a composite arrow
514
+ const
515
+ n = arrow.links.length,
516
+ msg = 'Arrow represents ' + pluralS(n, 'link');
517
+ UI.setMessage(msg);
518
+ if(this.visible && !this.editing) {
519
+ // Set the dialog title
520
+ this.title.innerHTML = msg;
521
+ // Show list
522
+ const lis = [];
523
+ let l, dn, c, af;
524
+ for(let i = 0; i < n; i++) {
525
+ l = arrow.links[i];
526
+ dn = l.displayName;
527
+ if(l.from_node instanceof Process) {
528
+ c = UI.color.produced;
529
+ dn = dn.replace(l.from_node.displayName,
530
+ `<em>${l.from_node.displayName}</em>`);
531
+ } else if(l.to_node instanceof Process) {
532
+ c = UI.color.consumed;
533
+ dn = dn.replace(l.to_node.displayName,
534
+ `<em>${l.to_node.displayName}</em>`);
535
+ } else {
536
+ c = 'gray';
537
+ }
538
+ if(MODEL.solved && l instanceof Link) {
539
+ af = l.actualFlow(MODEL.t);
540
+ if(Math.abs(af) > VM.SIG_DIF_FROM_ZERO) {
541
+ dn = dn.replace(UI.LINK_ARROW,
542
+ `<span style="color: ${c}">\u291A[${VM.sig4Dig(af)}]\u21FE</span>`);
543
+ }
544
+ }
545
+ lis.push(`<li>${dn}</li>`);
546
+ }
547
+ lis.sort(ciCompare);
548
+ this.viewer.innerHTML = `<ul>${lis.join('')}</ul>`;
549
+ }
550
+ }
551
+
552
+ showHiddenIO(node, arrow) {
553
+ // Show list of products or processes linked to node by an invisible arrow
554
+ let msg, iol;
555
+ if(arrow === UI.BLOCK_IN) {
556
+ iol = node.hidden_inputs;
557
+ msg = pluralS(iol.length, 'more input');
558
+ } else if(arrow === UI.BLOCK_OUT) {
559
+ iol = node.hidden_outputs;
560
+ msg = pluralS(iol.length, 'more output');
561
+ } else {
562
+ iol = node.hidden_io;
563
+ msg = pluralS(iol.length, 'more double linkage');
564
+ }
565
+ msg = node.displayName + ' has ' + msg;
566
+ UI.on_block_arrow = true;
567
+ UI.setMessage(msg);
568
+ if(this.visible && !this.editing) {
569
+ // Set the dialog title
570
+ this.title.innerHTML = msg;
571
+ // Show list
572
+ const lis = [];
573
+ for(let i = 0; i < iol.length; i++) {
574
+ lis.push(`<li>${iol[i].displayName}</li>`);
575
+ }
576
+ lis.sort(ciCompare);
577
+ this.viewer.innerHTML = `<ul>${lis.join('')}</ul>`;
578
+ }
579
+ }
580
+
581
+ showAllDocumentation() {
582
+ const
583
+ html = [],
584
+ sl = MODEL.listOfAllComments;
585
+ for(let i = 0; i < sl.length; i++) {
586
+ if(sl[i].startsWith('_____')) {
587
+ // 5-underscore leader indicates: start of new category
588
+ html.push('<h2>', sl[i].substring(5), '</h2>');
589
+ } else {
590
+ // Expect model element name...
591
+ html.push('<p><tt>', sl[i], '</tt><br><small>');
592
+ // ... immediately followed by its associated marked-up comments
593
+ i++;
594
+ this.markup = sl[i];
595
+ html.push(this.markdown, '</small></p>');
596
+ }
597
+ }
598
+ this.title.innerHTML = 'Complete model documentation';
599
+ this.viewer.innerHTML = html.join('');
600
+ // Deselect entity and disable editing
601
+ this.entity = null;
602
+ this.edit_btn.classList.remove('enab');
603
+ this.edit_btn.classList.add('disab');
604
+ }
605
+
606
+ copyDocToClipboard() {
607
+ UI.copyHtmlToClipboard(this.viewer.innerHTML);
608
+ UI.notify('Documentation copied to clipboard (as HTML)');
609
+ }
610
+
611
+ compareModels(data) {
612
+ this.comparison_modal.hide();
613
+ this.model = new LinnyRModel('', '');
614
+ // NOTE: while loading, make the second model "main" so it will initialize
615
+ const loaded = MODEL;
616
+ MODEL = this.model;
617
+ try {
618
+ // NOTE: Convert %23 back to # (escaped by function saveModel)
619
+ const xml = parseXML(data.replace(/%23/g, '#'));
620
+ // NOTE: loading, not including => make sure that IO context is NULL
621
+ IO_CONTEXT = null;
622
+ this.model.initFromXML(xml);
623
+ } catch(err) {
624
+ UI.normalCursor();
625
+ UI.alert('Error while parsing model: ' + err);
626
+ // Restore original "main" model
627
+ MODEL = loaded;
628
+ this.model = null;
629
+ return false;
630
+ }
631
+ // Restore original "main" model
632
+ MODEL = loaded;
633
+ try {
634
+ // Store differences as HTML in local storage
635
+ console.log('Storing differences between model A (' + MODEL.displayName +
636
+ ') and model B (' + this.model.displayName + ') as HTML');
637
+ const html = this.differencesAsHTML(MODEL.differences(this.model));
638
+ window.localStorage.setItem('linny-r-differences-A-B', html);
639
+ UI.notify('Comparison report can be viewed ' +
640
+ '<a href="./show-diff.html" target="_blank"><strong>here</strong></a>');
641
+ } catch(err) {
642
+ UI.alert(`Failed to store model differences: ${err}`);
643
+ }
644
+ // Dispose the model-for-comparison
645
+ this.model = null;
646
+ // Cursor is set to WAITING when loading starts
647
+ UI.normalCursor();
648
+ }
649
+
650
+ propertyName(p) {
651
+ // Returns the name of a Linny-R entity property as HTML-italicized string
652
+ // if `p` is recognized as such, or otherwise `p` itself
653
+ if(p in UI.MC.SETTINGS_PROPS) return `<em>${UI.MC.SETTINGS_PROPS[p]}:</em>`;
654
+ if(UI.MC.ALL_PROPS.indexOf(p) >= 0) return '<em>' + p.charAt(0).toUpperCase() +
655
+ p.slice(1).replace('_', '&nbsp;') + ':</em>';
656
+ return p;
657
+ }
658
+
659
+ propertyAsString(p) {
660
+ // Returns the value of `p` as an HTML string for Model Comparison report
661
+ if(p === true) return '<code>true</code>';
662
+ if(p === false) return '<code>false</code>';
663
+ const top = typeof p;
664
+ if(top === 'number') return VM.sig4Dig(p);
665
+ if(top === 'string') return (p.length === 0 ? '<em>(empty)</em>' : p);
666
+ return p.toString();
667
+ }
668
+
669
+ differencesAsHTML(d) {
670
+ const html = [];
671
+ let n = (Object.keys(d).length > 0 ? 'D' : 'No d');
672
+ html.push('<h1>' + n + 'ifferences between model A and model B</h1>');
673
+ html.push('<p><em>Model</em> <strong>A</strong> <em>is <u>current</u>, ',
674
+ 'model</em> <strong>B</strong> <em>was loaded for comparison only.</em>');
675
+ html.push('<table><tr><th>Model</th><th>Name</th><th>Author</th></tr>');
676
+ html.push('<tr><td>A</td><td>' + this.propertyAsString(MODEL.name) +
677
+ '</td><td>'+ this.propertyAsString(MODEL.author) + '</td></tr>');
678
+ html.push('<tr><td>B</td><td>' + this.propertyAsString(this.model.name) +
679
+ '</td><td>' + this.propertyAsString(this.model.author) +
680
+ '</td></tr></table>');
681
+ if('settings' in d) html.push('<h2>Model settings</h2>',
682
+ this.differenceAsTable(d.settings));
683
+ if('units' in d) html.push('<h2>Units</h2>',
684
+ this.differenceAsTable(d.units));
685
+ for(let i = 0; i < UI.MC.ENTITY_PROPS.length; i++) {
686
+ const e = UI.MC.ENTITY_PROPS[i];
687
+ if(e in d) html.push('<h2>' + this.propertyName(e) + '</h2>',
688
+ this.differenceAsTable(d[e]));
689
+ }
690
+ if('charts' in d) html.push('<h2><em>Charts</em></h2>',
691
+ this.differenceAsTable(d.charts));
692
+ return html.join('\n');
693
+ }
694
+
695
+ differenceAsTableRow(dd, k) {
696
+ const d = dd[k];
697
+ // NOTE: recursive method, as cells can contain tables
698
+ let tr = '';
699
+ if(Array.isArray(d) && d.length >= 2) {
700
+ tr = '<tr><td class="mc-name">' + this.propertyName(d[1]) + '</td>';
701
+ if(d[0] === UI.MC.MODIFIED) {
702
+ if(d[2].hasOwnProperty('A') && d[2].hasOwnProperty('B')) {
703
+ // Leaf node showing the differring property values in A and B
704
+ const mfd = markFirstDifference(d[2].A, d[2].B);
705
+ tr += `<td class="mc-modified">${mfd}</td><td>${d[2].B}</td>`;
706
+ } else {
707
+ // Compound "dictionary" of differences
708
+ tr += '<td colspan="2">' + this.differenceAsTable(d[2]) + '</td>';
709
+ }
710
+ } else {
711
+ // Addition and deletions are shown for model A
712
+ tr += `<td class="mc-${UI.MC.STATE[d[0]]}">${UI.MC.STATE[d[0]]}</td><td></td>`;
713
+ }
714
+ tr += '</tr>';
715
+ } else if(d.hasOwnProperty('A') && d.hasOwnProperty('B')) {
716
+ tr = '<tr><td>' + this.propertyName(k) + '</td><td class="mc-modified">'+
717
+ markFirstDifference(d.A, d.B) + '</td><td class="mc-former">' +
718
+ d.B + '</td></tr>';
719
+ } else {
720
+ tr = '<tr><td>' + this.differenceAsTable(d) + '</td></tr>';
721
+ }
722
+ return tr;
723
+ }
724
+
725
+ differenceAsTable(d) {
726
+ if(typeof d === 'object') {
727
+ const
728
+ html = ['<table>'],
729
+ keys = Object.keys(d).sort();
730
+ for(let i = 0; i < keys.length; i++) {
731
+ html.push(this.differenceAsTableRow(d, keys[i]));
732
+ }
733
+ html.push('</table>');
734
+ return html.join('\n');
735
+ }
736
+ return '';
737
+ }
738
+
739
+ } // END of class DocumentationManager