linny-r 1.4.2 → 1.4.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +162 -74
- package/package.json +1 -1
- package/server.js +145 -49
- package/static/images/check-off-not-same-changed.png +0 -0
- package/static/images/check-off-not-same-not-changed.png +0 -0
- package/static/images/check-off-same-changed.png +0 -0
- package/static/images/check-off-same-not-changed.png +0 -0
- package/static/images/check-on-not-same-changed.png +0 -0
- package/static/images/check-on-not-same-not-changed.png +0 -0
- package/static/images/check-on-same-changed.png +0 -0
- package/static/images/check-on-same-not-changed.png +0 -0
- package/static/images/eq-not-same-changed.png +0 -0
- package/static/images/eq-not-same-not-changed.png +0 -0
- package/static/images/eq-same-changed.png +0 -0
- package/static/images/eq-same-not-changed.png +0 -0
- package/static/images/ne-not-same-changed.png +0 -0
- package/static/images/ne-not-same-not-changed.png +0 -0
- package/static/images/ne-same-changed.png +0 -0
- package/static/images/ne-same-not-changed.png +0 -0
- package/static/images/octaeder.svg +993 -0
- package/static/images/sort-asc-lead.png +0 -0
- package/static/images/sort-asc.png +0 -0
- package/static/images/sort-desc-lead.png +0 -0
- package/static/images/sort-desc.png +0 -0
- package/static/images/sort-not.png +0 -0
- package/static/index.html +72 -647
- package/static/linny-r.css +199 -417
- package/static/scripts/linny-r-gui-actor-manager.js +340 -0
- package/static/scripts/linny-r-gui-chart-manager.js +944 -0
- package/static/scripts/linny-r-gui-constraint-editor.js +681 -0
- package/static/scripts/linny-r-gui-controller.js +4005 -0
- package/static/scripts/linny-r-gui-dataset-manager.js +1176 -0
- package/static/scripts/linny-r-gui-documentation-manager.js +739 -0
- package/static/scripts/linny-r-gui-equation-manager.js +307 -0
- package/static/scripts/linny-r-gui-experiment-manager.js +1944 -0
- package/static/scripts/linny-r-gui-expression-editor.js +449 -0
- package/static/scripts/linny-r-gui-file-manager.js +392 -0
- package/static/scripts/linny-r-gui-finder.js +727 -0
- package/static/scripts/linny-r-gui-model-autosaver.js +230 -0
- package/static/scripts/linny-r-gui-monitor.js +448 -0
- package/static/scripts/linny-r-gui-paper.js +2789 -0
- package/static/scripts/linny-r-gui-receiver.js +323 -0
- package/static/scripts/linny-r-gui-repository-browser.js +819 -0
- package/static/scripts/linny-r-gui-scale-unit-manager.js +244 -0
- package/static/scripts/linny-r-gui-sensitivity-analysis.js +778 -0
- package/static/scripts/linny-r-gui-undo-redo.js +560 -0
- package/static/scripts/linny-r-model.js +27 -11
- package/static/scripts/linny-r-utils.js +17 -2
- package/static/scripts/linny-r-vm.js +31 -12
- package/static/scripts/linny-r-gui.js +0 -16761
@@ -0,0 +1,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: '&'
|
93
|
+
},
|
94
|
+
{ // No HTML tags
|
95
|
+
pattern: /</g,
|
96
|
+
rewrite: '<'
|
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> 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> 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> ${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> ${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('_', ' ') + ':</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
|