linny-r 1.4.3 → 1.4.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +102 -48
- package/package.json +1 -1
- package/server.js +31 -6
- package/static/images/check-off-not-same-changed.png +0 -0
- package/static/images/check-off-not-same-not-changed.png +0 -0
- package/static/images/check-off-same-changed.png +0 -0
- package/static/images/check-off-same-not-changed.png +0 -0
- package/static/images/check-on-not-same-changed.png +0 -0
- package/static/images/check-on-not-same-not-changed.png +0 -0
- package/static/images/check-on-same-changed.png +0 -0
- package/static/images/check-on-same-not-changed.png +0 -0
- package/static/images/eq-not-same-changed.png +0 -0
- package/static/images/eq-not-same-not-changed.png +0 -0
- package/static/images/eq-same-changed.png +0 -0
- package/static/images/eq-same-not-changed.png +0 -0
- package/static/images/ne-not-same-changed.png +0 -0
- package/static/images/ne-not-same-not-changed.png +0 -0
- package/static/images/ne-same-changed.png +0 -0
- package/static/images/ne-same-not-changed.png +0 -0
- package/static/images/sort-asc-lead.png +0 -0
- package/static/images/sort-asc.png +0 -0
- package/static/images/sort-desc-lead.png +0 -0
- package/static/images/sort-desc.png +0 -0
- package/static/images/sort-not.png +0 -0
- package/static/index.html +51 -35
- package/static/linny-r.css +167 -53
- package/static/scripts/linny-r-gui-actor-manager.js +340 -0
- package/static/scripts/linny-r-gui-chart-manager.js +944 -0
- package/static/scripts/linny-r-gui-constraint-editor.js +681 -0
- package/static/scripts/linny-r-gui-controller.js +4005 -0
- package/static/scripts/linny-r-gui-dataset-manager.js +1176 -0
- package/static/scripts/linny-r-gui-documentation-manager.js +739 -0
- package/static/scripts/linny-r-gui-equation-manager.js +307 -0
- package/static/scripts/linny-r-gui-experiment-manager.js +1944 -0
- package/static/scripts/linny-r-gui-expression-editor.js +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 +24 -11
- package/static/scripts/linny-r-utils.js +10 -0
- package/static/scripts/linny-r-vm.js +21 -12
- package/static/scripts/linny-r-gui.js +0 -16908
@@ -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>◂</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
|