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,727 @@
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-finder.js) provides the GUI functionality
9
+ for the Linny-R "finder": the draggable/resizable dialog for listing
10
+ model entities based on their name, and locating where they occur in the
11
+ model.
12
+
13
+ */
14
+
15
+ /*
16
+ Copyright (c) 2017-2023 Delft University of Technology
17
+
18
+ Permission is hereby granted, free of charge, to any person obtaining a copy
19
+ of this software and associated documentation files (the "Software"), to deal
20
+ in the Software without restriction, including without limitation the rights to
21
+ use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
22
+ of the Software, and to permit persons to whom the Software is furnished to do
23
+ so, subject to the following conditions:
24
+
25
+ The above copyright notice and this permission notice shall be included in
26
+ all copies or substantial portions of the Software.
27
+
28
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
29
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
30
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
31
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
32
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
33
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
34
+ SOFTWARE.
35
+ */
36
+
37
+ // CLASS Finder provides the finder dialog functionality
38
+ class Finder {
39
+ constructor() {
40
+ this.dialog = UI.draggableDialog('finder');
41
+ UI.resizableDialog('finder', 'FINDER');
42
+ this.close_btn = document.getElementById('finder-close-btn');
43
+ // Make toolbar buttons responsive
44
+ this.close_btn.addEventListener('click', (e) => UI.toggleDialog(e));
45
+ this.filter_input = document.getElementById('finder-filter-text');
46
+ this.filter_input.addEventListener('input', () => FINDER.changeFilter());
47
+ this.edit_btn = document.getElementById('finder-edit-btn');
48
+ this.edit_btn.addEventListener(
49
+ 'click', (event) => FINDER.editAttributes());
50
+ this.copy_btn = document.getElementById('finder-copy-btn');
51
+ this.copy_btn.addEventListener(
52
+ 'click', (event) => FINDER.copyAttributesToClipboard(event.shiftKey));
53
+ this.entity_table = document.getElementById('finder-table');
54
+ this.item_table = document.getElementById('finder-item-table');
55
+ this.expression_table = document.getElementById('finder-expression-table');
56
+
57
+ // Attribute headers are used by Finder to output entity attribute values.
58
+ this.attribute_headers = {
59
+ A: 'ACTORS:\tWeight\tCash IN\tCash OUT\tCash FLOW',
60
+ B: 'CONSTRAINTS (no attributes)',
61
+ C: 'CLUSTERS:\tCash IN\tCash OUT\tCash FLOW',
62
+ D: 'DATASETS:\tModifier\tValue/expression',
63
+ E: 'EQUATIONS:\tValue/expression',
64
+ L: 'LINKS:\nFrom\tTo\tRate\tDelay\tShare of cost\tActual flow',
65
+ P: 'PROCESSES:\tLower bound\tUpper bound\tInitial level\tLevel' +
66
+ '\tCash IN\tCash OUT\tCash FLOW\tCost price',
67
+ Q: 'PRODUCTS:\tLower bound\tUpper bound\tInitial level\tPrice' +
68
+ '\tLevel\tCost price\tHighest cost price'
69
+ };
70
+ // Set own properties.
71
+ this.entities = [];
72
+ this.filtered_types = [];
73
+ this.reset();
74
+ }
75
+
76
+ reset() {
77
+ this.entities.length = 0;
78
+ this.filtered_types.length = 0;
79
+ this.selected_entity = null;
80
+ this.filter_input.value = '';
81
+ this.filter_pattern = null;
82
+ this.entity_types = VM.entity_letters;
83
+ this.find_links = true;
84
+ this.last_time_clicked = 0;
85
+ this.clicked_object = null;
86
+ // Product cluster index "remembers" for which cluster a product was
87
+ // last revealed, so it can reveal the next cluster when clicked again.
88
+ this.product_cluster_index = 0;
89
+ }
90
+
91
+ doubleClicked(obj) {
92
+ const
93
+ now = Date.now(),
94
+ dt = now - this.last_time_clicked;
95
+ this.last_time_clicked = now;
96
+ if(obj === this.clicked_object) {
97
+ // Consider click to be "double" if it occurred less than 300 ms ago.
98
+ if(dt < 300) {
99
+ this.last_time_clicked = 0;
100
+ return true;
101
+ }
102
+ }
103
+ this.clicked_object = obj;
104
+ return false;
105
+ }
106
+
107
+ enterKey() {
108
+ // Open "edit properties" dialog for the selected entity
109
+ const srl = this.entity_table.getElementsByClassName('sel-set');
110
+ if(srl.length > 0) {
111
+ const r = this.entity_table.rows[srl[0].rowIndex];
112
+ if(r) {
113
+ const e = new Event('click');
114
+ e.altKey = true;
115
+ r.dispatchEvent(e);
116
+ }
117
+ }
118
+ }
119
+
120
+ upDownKey(dir) {
121
+ // Select row above or below the selected one (if possible)
122
+ const srl = this.entity_table.getElementsByClassName('sel-set');
123
+ if(srl.length > 0) {
124
+ const r = this.entity_table.rows[srl[0].rowIndex + dir];
125
+ if(r) {
126
+ UI.scrollIntoView(r);
127
+ r.dispatchEvent(new Event('click'));
128
+ }
129
+ }
130
+ }
131
+
132
+ updateDialog() {
133
+ const
134
+ el = [],
135
+ enl = [],
136
+ se = this.selected_entity,
137
+ et = this.entity_types,
138
+ fp = this.filter_pattern && this.filter_pattern.length > 0;
139
+ let imgs = '';
140
+ this.entities.length = 0;
141
+ this.filtered_types.length = 0;
142
+ // No list unless a pattern OR a specified SUB-set of entity types
143
+ if(fp || et && et !== VM.entity_letters) {
144
+ if(et.indexOf('A') >= 0) {
145
+ imgs += '<img src="images/actor.png">';
146
+ for(let k in MODEL.actors) if(MODEL.actors.hasOwnProperty(k)) {
147
+ if(!fp || patternMatch(MODEL.actors[k].name, this.filter_pattern)) {
148
+ enl.push(k);
149
+ this.entities.push(MODEL.actors[k]);
150
+ addDistinct('A', this.filtered_types);
151
+ }
152
+ }
153
+ }
154
+ // NOTE: do not list black-boxed entities
155
+ if(et.indexOf('P') >= 0) {
156
+ imgs += '<img src="images/process.png">';
157
+ for(let k in MODEL.processes) if(MODEL.processes.hasOwnProperty(k)) {
158
+ if(!k.startsWith(UI.BLACK_BOX) && (!fp || patternMatch(
159
+ MODEL.processes[k].displayName, this.filter_pattern))) {
160
+ enl.push(k);
161
+ this.entities.push(MODEL.processes[k]);
162
+ addDistinct('P', this.filtered_types);
163
+ }
164
+ }
165
+ }
166
+ if(et.indexOf('Q') >= 0) {
167
+ imgs += '<img src="images/product.png">';
168
+ for(let k in MODEL.products) if(MODEL.products.hasOwnProperty(k)) {
169
+ if(!k.startsWith(UI.BLACK_BOX) && (!fp || patternMatch(
170
+ MODEL.products[k].displayName, this.filter_pattern))) {
171
+ enl.push(k);
172
+ this.entities.push(MODEL.products[k]);
173
+ addDistinct('Q', this.filtered_types);
174
+ }
175
+ }
176
+ }
177
+ if(et.indexOf('C') >= 0) {
178
+ imgs += '<img src="images/cluster.png">';
179
+ for(let k in MODEL.clusters) if(MODEL.clusters.hasOwnProperty(k)) {
180
+ if(!k.startsWith(UI.BLACK_BOX) && (!fp || patternMatch(
181
+ MODEL.clusters[k].displayName, this.filter_pattern))) {
182
+ enl.push(k);
183
+ this.entities.push(MODEL.clusters[k]);
184
+ addDistinct('C', this.filtered_types);
185
+ }
186
+ }
187
+ }
188
+ if(et.indexOf('D') >= 0) {
189
+ imgs += '<img src="images/dataset.png">';
190
+ for(let k in MODEL.datasets) if(MODEL.datasets.hasOwnProperty(k)) {
191
+ const ds = MODEL.datasets[k];
192
+ if(!k.startsWith(UI.BLACK_BOX) && (!fp || patternMatch(
193
+ ds.displayName, this.filter_pattern))) {
194
+ // NOTE: do not list the equations dataset
195
+ if(ds !== MODEL.equations_dataset) {
196
+ enl.push(k);
197
+ this.entities.push(MODEL.datasets[k]);
198
+ addDistinct('D', this.filtered_types);
199
+ }
200
+ }
201
+ }
202
+ }
203
+ if(et.indexOf('E') >= 0) {
204
+ imgs += '<img src="images/equation.png">';
205
+ for(let k in MODEL.equations_dataset.modifiers) {
206
+ if(MODEL.equations_dataset.modifiers.hasOwnProperty(k)) {
207
+ if(!fp ||
208
+ patternMatch(MODEL.equations_dataset.modifiers[k].displayName,
209
+ this.filter_pattern)) {
210
+ enl.push(k);
211
+ this.entities.push(MODEL.equations_dataset.modifiers[k]);
212
+ addDistinct('E', this.filtered_types);
213
+ }
214
+ }
215
+ }
216
+ }
217
+ if(et.indexOf('L') >= 0) {
218
+ imgs += '<img src="images/link.png">';
219
+ for(let k in MODEL.links) if(MODEL.links.hasOwnProperty(k)) {
220
+ // NOTE: "black-boxed" link identifiers are not prefixed => other test
221
+ const
222
+ l = MODEL.links[k],
223
+ ldn = l.displayName,
224
+ // A links is "black-boxed" when BOTH nodes are "black-boxed"
225
+ bb = ldn.split(UI.BLACK_BOX).length > 2;
226
+ if(!bb && (!fp || patternMatch(ldn, this.filter_pattern))) {
227
+ enl.push(k);
228
+ this.entities.push(l);
229
+ addDistinct('L', this.filtered_types);
230
+ }
231
+ }
232
+ }
233
+ if(et.indexOf('B') >= 0) {
234
+ imgs += '<img src="images/constraint.png">';
235
+ for(let k in MODEL.constraints) {
236
+ // NOTE: likewise, constraint identifiers can be prefixed by %
237
+ if(MODEL.constraints.hasOwnProperty(k)) {
238
+ if(!k.startsWith(UI.BLACK_BOX) && (!fp || patternMatch(
239
+ MODEL.constraints[k].displayName, this.filter_pattern))) {
240
+ enl.push(k);
241
+ this.entities.push(MODEL.constraints[k]);
242
+ addDistinct('B', this.filtered_types);
243
+ }
244
+ }
245
+ }
246
+ }
247
+ enl.sort((a, b) => UI.compareFullNames(a, b, true));
248
+ }
249
+ document.getElementById('finder-entity-imgs').innerHTML = imgs;
250
+ let seid = 'etr';
251
+ for(let i = 0; i < enl.length; i++) {
252
+ const e = MODEL.objectByID(enl[i]);
253
+ if(e === se) seid += i;
254
+ el.push(['<tr id="etr', i, '" class="dataset',
255
+ (e === se ? ' sel-set' : ''), '" onclick="FINDER.selectEntity(\'',
256
+ enl[i], '\', event.altKey);" onmouseover="FINDER.showInfo(\'', enl[i],
257
+ '\', event.shiftKey);"><td draggable="true" ',
258
+ 'ondragstart="FINDER.drag(event);"><img class="finder" src="images/',
259
+ e.type.toLowerCase(), '.png">', e.displayName,
260
+ '</td></tr>'].join(''));
261
+ }
262
+ // NOTE: reset `selected_entity` if not in the new list
263
+ if(seid === 'etr') this.selected_entity = null;
264
+ this.entity_table.innerHTML = el.join('');
265
+ UI.scrollIntoView(document.getElementById(seid));
266
+ document.getElementById('finder-count').innerHTML = pluralS(
267
+ el.length, 'entity', 'entities');
268
+ // Only show the edit button if all filtered entities are of the
269
+ // same type.
270
+ let n = el.length;
271
+ this.edit_btn.style.display = 'none';
272
+ this.copy_btn.style.display = 'none';
273
+ if(n > 0) {
274
+ this.copy_btn.style.display = 'block';
275
+ n = this.entityGroup.length;
276
+ if(n > 0) {
277
+ this.edit_btn.title = 'Edit attributes of ' +
278
+ pluralS(n, this.entities[0].type.toLowerCase());
279
+ this.edit_btn.style.display = 'block';
280
+ }
281
+ }
282
+ this.updateRightPane();
283
+ }
284
+
285
+ get entityGroup() {
286
+ // Returns the list of filtered entities if all are of the same type,
287
+ // while excluding (no actor), (top cluster), datasets and equations.
288
+ const
289
+ eg = [],
290
+ n = this.entities.length;
291
+ if(n > 0) {
292
+ const ft = this.filtered_types[0];
293
+ if(this.filtered_types.length === 1 && 'DE'.indexOf(ft) < 0) {
294
+ for(let i = 0; i < n; i++) {
295
+ const e = this.entities[i];
296
+ // Exclude "no actor" and top cluster.
297
+ if(e.name !== '(no_actor)' && e.name !== '(top_cluster)') {
298
+ eg.push(e);
299
+ }
300
+ }
301
+ }
302
+ }
303
+ return eg;
304
+ }
305
+
306
+ updateRightPane() {
307
+ const
308
+ se = this.selected_entity,
309
+ occ = [], // list with occurrences (clusters, processes or charts)
310
+ xol = [], // list with identifier of "expression owning" entities
311
+ xal = [], // list with attributes having matching expressions
312
+ el = []; // list of HTML elements (table rows) to be added
313
+ let hdr = '(no entity selected)';
314
+ if(se) {
315
+ hdr = `<em>${se.type}:</em> <strong>${se.displayName}</strong>`;
316
+ // Make occurrence list
317
+ if(se instanceof Process || se instanceof Cluster) {
318
+ // Processes and clusters "occur" in their parent cluster
319
+ if(se.cluster) occ.push(se.cluster.identifier);
320
+ } else if(se instanceof Product) {
321
+ // Products "occur" in clusters where they have a position
322
+ const cl = se.productPositionClusters;
323
+ for(let i = 0; i < cl.length; i++) {
324
+ occ.push(cl[i].identifier);
325
+ }
326
+ } else if(se instanceof Actor) {
327
+ // Actors "occur" in clusters where they "own" processes or clusters
328
+ for(let k in MODEL.processes) if(MODEL.processes.hasOwnProperty(k)) {
329
+ const p = MODEL.processes[k];
330
+ if(p.actor === se) occ.push(p.identifier);
331
+ }
332
+ for(let k in MODEL.clusters) if(MODEL.clusters.hasOwnProperty(k)) {
333
+ const c = MODEL.clusters[k];
334
+ if(c.actor === se) occ.push(c.identifier);
335
+ }
336
+ } else if(se instanceof Link || se instanceof Constraint) {
337
+ // Links and constraints "occur" in their "best" parent cluster
338
+ const c = MODEL.inferParentCluster(se);
339
+ if(c) occ.push(c.identifier);
340
+ }
341
+ // NOTE: no "occurrence" of datasets or equations
342
+ // @@TO DO: identify MODULES (?)
343
+ // All entities can also occur as chart variables
344
+ for(let j = 0; j < MODEL.charts.length; j++) {
345
+ const c = MODEL.charts[j];
346
+ for(let k = 0; k < c.variables.length; k++) {
347
+ const v = c.variables[k];
348
+ if(v.object === se || (se instanceof DatasetModifier &&
349
+ se.identifier === UI.nameToID(v.attribute))) {
350
+ occ.push(MODEL.chart_id_prefix + j);
351
+ break;
352
+ }
353
+ }
354
+ }
355
+ // Now also look for occurrences of entity references in expressions
356
+ const
357
+ raw = escapeRegex(se.displayName),
358
+ re = new RegExp(
359
+ '\\[\\s*!?' + raw.replace(/\s+/g, '\\s+') + '\\s*[\\|\\@\\]]');
360
+ // Check actor weight expressions
361
+ for(let k in MODEL.actors) if(MODEL.actors.hasOwnProperty(k)) {
362
+ const a = MODEL.actors[k];
363
+ if(re.test(a.weight.text)) {
364
+ xal.push('W');
365
+ xol.push(a.identifier);
366
+ }
367
+ }
368
+ // Check all process attribute expressions
369
+ for(let k in MODEL.processes) if(MODEL.processes.hasOwnProperty(k)) {
370
+ const p = MODEL.processes[k];
371
+ if(re.test(p.lower_bound.text)) {
372
+ xal.push('LB');
373
+ xol.push(p.identifier);
374
+ }
375
+ if(re.test(p.upper_bound.text)) {
376
+ xal.push('UB');
377
+ xol.push(p.identifier);
378
+ }
379
+ if(re.test(p.initial_level.text)) {
380
+ xal.push('IL');
381
+ xol.push(p.identifier);
382
+ }
383
+ }
384
+ // Check all product attribute expressions
385
+ for(let k in MODEL.products) if(MODEL.products.hasOwnProperty(k)) {
386
+ const p = MODEL.products[k];
387
+ if(re.test(p.lower_bound.text)) {
388
+ xal.push('LB');
389
+ xol.push(p.identifier);
390
+ }
391
+ if(re.test(p.upper_bound.text)) {
392
+ xal.push('UB');
393
+ xol.push(p.identifier);
394
+ }
395
+ if(re.test(p.initial_level.text)) {
396
+ xal.push('IL');
397
+ xol.push(p.identifier);
398
+ }
399
+ if(re.test(p.price.text)) {
400
+ xal.push('P');
401
+ xol.push(p.identifier);
402
+ }
403
+ }
404
+ // Check all notes in clusters for their color expressions and field
405
+ for(let k in MODEL.clusters) if(MODEL.clusters.hasOwnProperty(k)) {
406
+ const c = MODEL.clusters[k];
407
+ for(let i = 0; i < c.notes.length; i++) {
408
+ const n = c.notes[i];
409
+ // Look for entity in both note contents and note color expression
410
+ if(re.test(n.color.text) || re.test(n.contents)) {
411
+ xal.push('NOTE');
412
+ xol.push(n.identifier);
413
+ }
414
+ }
415
+ }
416
+ // Check all link rate expressions
417
+ for(let k in MODEL.links) if(MODEL.links.hasOwnProperty(k)) {
418
+ const l = MODEL.links[k];
419
+ if(re.test(l.relative_rate.text)) {
420
+ xal.push('R');
421
+ xol.push(l.identifier);
422
+ }
423
+ if(re.test(l.flow_delay.text)) {
424
+ xal.push('D');
425
+ xol.push(l.identifier);
426
+ }
427
+ }
428
+ // Check all dataset modifier expressions
429
+ for(let k in MODEL.datasets) if(MODEL.datasets.hasOwnProperty(k)) {
430
+ const ds = MODEL.datasets[k];
431
+ for(let m in ds.modifiers) if(ds.modifiers.hasOwnProperty(m)) {
432
+ const dsm = ds.modifiers[m];
433
+ if(re.test(dsm.expression.text)) {
434
+ xal.push(dsm.selector);
435
+ xol.push(ds.identifier);
436
+ }
437
+ }
438
+ }
439
+ }
440
+ document.getElementById('finder-item-header').innerHTML = hdr;
441
+ for(let i = 0; i < occ.length; i++) {
442
+ const e = MODEL.objectByID(occ[i]);
443
+ el.push(['<tr id="eotr', i, '" class="dataset" onclick="FINDER.reveal(\'',
444
+ occ[i], '\');" onmouseover="FINDER.showInfo(\'',
445
+ occ[i], '\', event.shiftKey);"><td><img class="finder" src="images/',
446
+ e.type.toLowerCase(), '.png">', e.displayName,
447
+ '</td></tr>'].join(''));
448
+ }
449
+ this.item_table.innerHTML = el.join('');
450
+ // Clear the table row list
451
+ el.length = 0;
452
+ // Now fill it with entity+attribute having a matching expression
453
+ for(let i = 0; i < xal.length; i++) {
454
+ const
455
+ id = xol[i],
456
+ e = MODEL.objectByID(id),
457
+ attr = (e instanceof Note ? '' : xal[i]);
458
+ let img = e.type.toLowerCase(),
459
+ // NOTE: a small left-pointing triangle denotes that the right-hand
460
+ // part has the left hand part as its attribute
461
+ cs = '',
462
+ td = attr + '</td><td>&#x25C2;</td><td style="width:95%">' +
463
+ e.displayName;
464
+ // NOTE: equations may have LONG names while the equations dataset name
465
+ // is irrelevant, hence use 3 columns (no triangle)
466
+ if(e === MODEL.equations_dataset) {
467
+ img = 'equation';
468
+ cs = ' colspan="3"';
469
+ td = attr;
470
+ }
471
+ el.push(['<tr id="eoxtr', i,
472
+ '" class="dataset" onclick="FINDER.revealExpression(\'', id,
473
+ '\', \'', attr, '\', event.shiftKey, event.altKey);"><td', cs, '>',
474
+ '<img class="finder" src="images/', img, '.png">', td, '</td></tr>'
475
+ ].join(''));
476
+ }
477
+ this.expression_table.innerHTML = el.join('');
478
+ document.getElementById('finder-expression-hdr').innerHTML =
479
+ pluralS(el.length, 'expression');
480
+ }
481
+
482
+ drag(ev) {
483
+ // Start dragging the selected entity
484
+ let t = ev.target;
485
+ while(t && t.nodeName !== 'TD') t = t.parentNode;
486
+ ev.dataTransfer.setData('text', MODEL.objectByName(t.innerText).identifier);
487
+ ev.dataTransfer.setDragImage(t, 25, 20);
488
+ }
489
+
490
+ changeFilter() {
491
+ // Filter expression can start with 1+ entity letters plus `?` to
492
+ // look only for the entity types denoted by these letters
493
+ let ft = this.filter_input.value,
494
+ et = VM.entity_letters;
495
+ if(/^(\*|[ABCDELPQ]+)\?/i.test(ft)) {
496
+ ft = ft.split('?');
497
+ // NOTE: *? denotes "all entity types except constraints"
498
+ et = (ft[0] === '*' ? 'ACDELPQ' : ft[0].toUpperCase());
499
+ ft = ft.slice(1).join('=');
500
+ }
501
+ this.filter_pattern = patternList(ft);
502
+ this.entity_types = et;
503
+ this.updateDialog();
504
+ }
505
+
506
+ showInfo(id, shift) {
507
+ // Displays documentation for the entity identified by `id`
508
+ const e = MODEL.objectByID(id);
509
+ if(e) DOCUMENTATION_MANAGER.update(e, shift);
510
+ }
511
+
512
+ selectEntity(id, alt=false) {
513
+ // Looks up entity, selects it in the left pane, and updates the
514
+ // right pane; opens the "edit properties" modal dialog on double-click
515
+ // and Alt-click if the entity is editable
516
+ const obj = MODEL.objectByID(id);
517
+ this.selected_entity = obj;
518
+ this.updateDialog();
519
+ if(!obj) return;
520
+ if(alt || this.doubleClicked(obj)) {
521
+ if(obj instanceof Process) {
522
+ UI.showProcessPropertiesDialog(obj);
523
+ } else if(obj instanceof Product) {
524
+ UI.showProductPropertiesDialog(obj);
525
+ } else if(obj instanceof Link) {
526
+ UI.showLinkPropertiesDialog(obj);
527
+ } else if(obj instanceof Cluster && obj !== MODEL.top_cluster) {
528
+ UI.showClusterPropertiesDialog(obj);
529
+ } else if(obj instanceof Actor) {
530
+ ACTOR_MANAGER.showEditActorDialog(obj.name, obj.weight.text);
531
+ } else if(obj instanceof Note) {
532
+ obj.showNotePropertiesDialog();
533
+ } else if(obj instanceof Dataset) {
534
+ if(UI.hidden('dataset-dlg')) {
535
+ UI.buttons.dataset.dispatchEvent(new Event('click'));
536
+ }
537
+ DATASET_MANAGER.selected_dataset = obj;
538
+ DATASET_MANAGER.updateDialog();
539
+ } else if(obj instanceof DatasetModifier) {
540
+ if(UI.hidden('equation-dlg')) {
541
+ UI.buttons.equation.dispatchEvent(new Event('click'));
542
+ }
543
+ EQUATION_MANAGER.selected_modifier = obj;
544
+ EQUATION_MANAGER.updateDialog();
545
+ }
546
+ }
547
+ }
548
+
549
+ reveal(id) {
550
+ // Shows selected occurrence
551
+ const
552
+ se = this.selected_entity,
553
+ obj = (se ? MODEL.objectByID(id) : null);
554
+ if(!obj) console.log('Cannot reveal ID', id);
555
+ // If cluster, make it focal...
556
+ if(obj instanceof Cluster) {
557
+ UI.makeFocalCluster(obj);
558
+ // ... and select the entity unless it is an actor or dataset
559
+ if(!(se instanceof Actor || se instanceof Dataset)) {
560
+ MODEL.select(se);
561
+ if(se instanceof Link || se instanceof Constraint) {
562
+ const a = obj.arrows[obj.indexOfArrow(se.from_node, se.to_node)];
563
+ if(a) UI.scrollIntoView(a.shape.element.childNodes[0]);
564
+ } else {
565
+ UI.scrollIntoView(se.shape.element.childNodes[0]);
566
+ }
567
+ }
568
+ } else if(obj instanceof Process || obj instanceof Note) {
569
+ // If occurrence is a process or a note, then make its cluster focal...
570
+ UI.makeFocalCluster(obj.cluster);
571
+ // ... and select it
572
+ MODEL.select(obj);
573
+ UI.scrollIntoView(obj.shape.element.childNodes[0]);
574
+ } else if(obj instanceof Product) {
575
+ // @@TO DO: iterate through list of clusters containing this product
576
+ } else if(obj instanceof Link || obj instanceof Constraint) {
577
+ const c = MODEL.inferParentCluster(obj);
578
+ if(c) {
579
+ UI.makeFocalCluster(c);
580
+ MODEL.select(obj);
581
+ const a = c.arrows[c.indexOfArrow(obj.from_node, obj.to_node)];
582
+ if(a) UI.scrollIntoView(a.shape.element.childNodes[0]);
583
+ }
584
+ } else if(obj instanceof Chart) {
585
+ // If occurrence is a chart, select and show it in the chart manager
586
+ CHART_MANAGER.chart_index = MODEL.charts.indexOf(obj);
587
+ if(CHART_MANAGER.chart_index >= 0) {
588
+ if(UI.hidden('chart-dlg')) {
589
+ UI.buttons.chart.dispatchEvent(new Event('click'));
590
+ }
591
+ }
592
+ CHART_MANAGER.updateDialog();
593
+ }
594
+ // NOTE: return the object to save a second lookup by revealExpression
595
+ return obj;
596
+ }
597
+
598
+ revealExpression(id, attr, shift=false, alt=false) {
599
+ const obj = this.reveal(id);
600
+ if(!obj) return;
601
+ shift = shift || this.doubleClicked(obj);
602
+ if(attr && (shift || alt)) {
603
+ if(obj instanceof Process) {
604
+ // NOTE: the second argument makes the dialog focus on the specified
605
+ // attribute input field; the third makes it open the expression editor
606
+ // as if modeler clicked on edit expression button
607
+ UI.showProcessPropertiesDialog(obj, attr, alt);
608
+ } else if(obj instanceof Product) {
609
+ UI.showProductPropertiesDialog(obj, attr, alt);
610
+ } else if(obj instanceof Link) {
611
+ UI.showLinkPropertiesDialog(obj, attr, alt);
612
+ } else if(obj instanceof Note) {
613
+ // NOTE: for notes, do not open expression editor, as entity may be
614
+ // referenced not only in the color expression, but also in the text
615
+ obj.showNotePropertiesDialog();
616
+ } else if(obj === MODEL.equations_dataset) {
617
+ // NOTE: equations are special type of dataset, hence this order
618
+ if(UI.hidden('equation-dlg')) {
619
+ UI.buttons.equation.dispatchEvent(new Event('click'));
620
+ }
621
+ // Double-check whether equation `attr` exists
622
+ if(obj.modifiers.hasOwnProperty(attr)) {
623
+ EQUATION_MANAGER.selected_modifier = obj.modifiers[attr];
624
+ } else {
625
+ EQUATION_MANAGER.selected_modifier = null;
626
+ }
627
+ EQUATION_MANAGER.updateDialog();
628
+ if(alt) EQUATION_MANAGER.editEquation();
629
+ } else if(obj instanceof Dataset) {
630
+ if(UI.hidden('dataset-dlg')) {
631
+ UI.buttons.dataset.dispatchEvent(new Event('click'));
632
+ }
633
+ DATASET_MANAGER.selected_dataset = obj;
634
+ // Double-check whether dataset has `attr` as selector
635
+ if(obj.modifiers.hasOwnProperty(attr)) {
636
+ DATASET_MANAGER.selected_modifier = obj.modifiers[attr];
637
+ if(alt) DATASET_MANAGER.editExpression();
638
+ } else {
639
+ DATASET_MANAGER.selected_modifier = null;
640
+ }
641
+ DATASET_MANAGER.updateDialog();
642
+ }
643
+ }
644
+ }
645
+
646
+ editAttributes() {
647
+ // Show the Edit properties dialog for the filtered-out entities.
648
+ // These must all be of the same type, or the edit button will not
649
+ // show. Just in case, check anyway.
650
+ const
651
+ group = this.entityGroup,
652
+ n = group.length;
653
+ if(n === 0) return;
654
+ let e = group[0];
655
+ if(n === 1) {
656
+ // Single entity, then edit its properties as usual.
657
+ this.selectEntity(e.identifier, true);
658
+ return;
659
+ }
660
+ // If an entity is selected in the list, use it as base.
661
+ if(this.selected_entity) e = this.selected_entity;
662
+ if(e instanceof Process) {
663
+ UI.showProcessPropertiesDialog(e, 'LB', false, group);
664
+ } else if(e instanceof Product) {
665
+ UI.showProductPropertiesDialog(e, 'LB', false, group);
666
+ } else if(e instanceof Link) {
667
+ UI.showLinkPropertiesDialog(e, 'R', false, group);
668
+ } else if(e instanceof Cluster) {
669
+ UI.showClusterPropertiesDialog(e, group);
670
+ }
671
+ }
672
+
673
+ copyAttributesToClipboard(shift) {
674
+ // Copy relevant entity attributes as tab-separated text to clipboard.
675
+ // NOTE: All entity types have "get" `attributes` that returns an
676
+ // object that for each defined attribute (and if model has been
677
+ // solved also each inferred attribute) has a property with its value.
678
+ // For dynamic expressions, the expression text is used
679
+ const ea_dict = {A: [], B: [], C: [], D: [], E: [], L: [], P: [], Q: []};
680
+ let e = this.selected_entity;
681
+ if(shift && e) {
682
+ ea_dict[e.typeLetter].push(e.attributes);
683
+ } else {
684
+ for(let i = 0; i < this.entities.length; i++) {
685
+ e = this.entities[i];
686
+ ea_dict[e.typeLetter].push(e.attributes);
687
+ }
688
+ }
689
+ const
690
+ seq = ['A', 'B', 'C', 'D', 'E', 'P', 'Q', 'L'],
691
+ text = [],
692
+ attr = [];
693
+ for(let i = 0; i < seq.length; i++) {
694
+ const
695
+ etl = seq[i],
696
+ ead = ea_dict[etl];
697
+ if(ead && ead.length > 0) {
698
+ // No blank line before first entity type.
699
+ if(text.length > 0) text.push('');
700
+ const en = capitalized(VM.entity_names[etl]);
701
+ let ah = en + '\t' + VM.entity_attribute_names[etl].join('\t');
702
+ if(etl === 'L' || etl === 'B') ah = ah.replace(en, `${en} FROM\tTO`);
703
+ if(!MODEL.infer_cost_prices) {
704
+ // If no cost price calculation, trim associated attributes
705
+ // from the header.
706
+ ah = ah.replace('\tCost price', '').replace('\tShare of cost', '');
707
+ }
708
+ text.push(ah);
709
+ attr.length = 0;
710
+ for(let i = 0; i < ead.length; i++) {
711
+ const
712
+ ea = ead[i],
713
+ ac = VM.attribute_codes[etl],
714
+ al = [ea.name];
715
+ for(let j = 0; j < ac.length; j++) {
716
+ if(ea.hasOwnProperty(ac[j])) al.push(ea[ac[j]]);
717
+ }
718
+ attr.push(al.join('\t'));
719
+ }
720
+ attr.sort();
721
+ text.push(attr.join('\n'));
722
+ }
723
+ }
724
+ UI.copyStringToClipboard(text.join('\n'));
725
+ }
726
+
727
+ } // END of class Finder