linny-r 2.0.8 → 2.0.10
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 +3 -40
- package/package.json +6 -2
- package/server.js +19 -157
- package/static/images/solve-not-same-changed.png +0 -0
- package/static/images/solve-not-same-not-changed.png +0 -0
- package/static/images/solve-same-changed.png +0 -0
- package/static/images/solve-same-not-changed.png +0 -0
- package/static/index.html +137 -20
- package/static/linny-r.css +260 -23
- package/static/scripts/iro.min.js +7 -7
- package/static/scripts/linny-r-ctrl.js +126 -85
- package/static/scripts/linny-r-gui-actor-manager.js +23 -33
- package/static/scripts/linny-r-gui-chart-manager.js +56 -53
- package/static/scripts/linny-r-gui-constraint-editor.js +10 -14
- package/static/scripts/linny-r-gui-controller.js +644 -260
- package/static/scripts/linny-r-gui-dataset-manager.js +64 -66
- package/static/scripts/linny-r-gui-documentation-manager.js +11 -17
- package/static/scripts/linny-r-gui-equation-manager.js +22 -22
- package/static/scripts/linny-r-gui-experiment-manager.js +124 -141
- package/static/scripts/linny-r-gui-expression-editor.js +26 -12
- package/static/scripts/linny-r-gui-file-manager.js +42 -48
- package/static/scripts/linny-r-gui-finder.js +294 -55
- package/static/scripts/linny-r-gui-model-autosaver.js +2 -4
- package/static/scripts/linny-r-gui-monitor.js +35 -41
- package/static/scripts/linny-r-gui-paper.js +42 -70
- package/static/scripts/linny-r-gui-power-grid-manager.js +31 -34
- package/static/scripts/linny-r-gui-receiver.js +1 -2
- package/static/scripts/linny-r-gui-repository-browser.js +44 -46
- package/static/scripts/linny-r-gui-scale-unit-manager.js +32 -32
- package/static/scripts/linny-r-gui-sensitivity-analysis.js +61 -67
- package/static/scripts/linny-r-gui-undo-redo.js +94 -95
- package/static/scripts/linny-r-milp.js +20 -24
- package/static/scripts/linny-r-model.js +1932 -2274
- package/static/scripts/linny-r-utils.js +27 -27
- package/static/scripts/linny-r-vm.js +900 -998
- package/static/show-png.html +0 -113
@@ -197,9 +197,9 @@ NOTE: Grouping groups results in a single group, e.g., (1;2);(3;4;5) evaluates a
|
|
197
197
|
let n = '',
|
198
198
|
a = '';
|
199
199
|
if(ids[0] === 'link') {
|
200
|
-
n = document.getElementById('link-from-name').
|
200
|
+
n = document.getElementById('link-from-name').innerText +
|
201
201
|
UI.LINK_ARROW +
|
202
|
-
document.getElementById('link-to-name').
|
202
|
+
document.getElementById('link-to-name').innerText;
|
203
203
|
} else {
|
204
204
|
n = document.getElementById(ids[0] + '-name').value;
|
205
205
|
if(ids[0] === 'process') {
|
@@ -218,6 +218,7 @@ NOTE: Grouping groups results in a single group, e.g., (1;2);(3;4;5) evaluates a
|
|
218
218
|
this.obj.value = 0;
|
219
219
|
this.updateVariableBar();
|
220
220
|
this.clearStatusBar();
|
221
|
+
this.showPrefix(UI.entityPrefix(prop));
|
221
222
|
md.show('text');
|
222
223
|
}
|
223
224
|
|
@@ -257,15 +258,20 @@ NOTE: Grouping groups results in a single group, e.g., (1;2);(3;4;5) evaluates a
|
|
257
258
|
// the dataset and the selector as extra parameters for the parser.
|
258
259
|
let own = null,
|
259
260
|
sel = '';
|
260
|
-
if(!this.edited_input_id
|
261
|
-
|
262
|
-
|
263
|
-
|
264
|
-
|
265
|
-
|
266
|
-
|
267
|
-
|
268
|
-
|
261
|
+
if(!this.edited_input_id) {
|
262
|
+
if(DATASET_MANAGER.edited_expression) {
|
263
|
+
own = DATASET_MANAGER.selected_dataset;
|
264
|
+
sel = DATASET_MANAGER.selected_modifier.selector;
|
265
|
+
} else if(EQUATION_MANAGER.edited_expression) {
|
266
|
+
own = MODEL.equations_dataset;
|
267
|
+
sel = EQUATION_MANAGER.selected_modifier.selector;
|
268
|
+
} else if(CONSTRAINT_EDITOR.edited_expression) {
|
269
|
+
own = CONSTRAINT_EDITOR.selected;
|
270
|
+
sel = CONSTRAINT_EDITOR.selected_selector;
|
271
|
+
} else if(UI.modals.datasetgroup.showing) {
|
272
|
+
own = UI.modals.datasetgroup.selected_ds;
|
273
|
+
sel = UI.modals.datasetgroup.selected_selector;
|
274
|
+
}
|
269
275
|
} else {
|
270
276
|
own = UI.edited_object;
|
271
277
|
sel = this.edited_input_id.split('-').pop();
|
@@ -298,6 +304,8 @@ NOTE: Grouping groups results in a single group, e.g., (1;2);(3;4;5) evaluates a
|
|
298
304
|
} else if(CONSTRAINT_EDITOR.edited_expression) {
|
299
305
|
// NOTE: Boundline selector expressions may result in a grouping.
|
300
306
|
CONSTRAINT_EDITOR.modifyExpression(xp.expr, xp.concatenating);
|
307
|
+
} else if(UI.modals.datasetgroup.showing) {
|
308
|
+
UI.modals.datasetgroup.modifyExpression(xp.expr);
|
301
309
|
}
|
302
310
|
UI.modals.expression.hide();
|
303
311
|
return true;
|
@@ -308,7 +316,13 @@ NOTE: Grouping groups results in a single group, e.g., (1;2);(3;4;5) evaluates a
|
|
308
316
|
this.status.style.backgroundColor = UI.color.dialog_background;
|
309
317
|
this.status.innerHTML = ' ';
|
310
318
|
}
|
311
|
-
|
319
|
+
|
320
|
+
showPrefix(prefix) {
|
321
|
+
// When editing an expression for a prefixed entity, show the prefix
|
322
|
+
// on the status line.
|
323
|
+
if(prefix) this.status.innerHTML = '<em>Prefix:</em> ' + prefix;
|
324
|
+
}
|
325
|
+
|
312
326
|
namesByType(type) {
|
313
327
|
// Returns a list of entity names of the specified types
|
314
328
|
// (used only to generate the options of SELECT elements)
|
@@ -47,13 +47,11 @@ class GUIFileManager {
|
|
47
47
|
// buttons on the top menu.
|
48
48
|
|
49
49
|
getRemoteData(dataset, url) {
|
50
|
-
// Gets data from a URL, or from a file on the local host
|
50
|
+
// Gets data from a URL, or from a file on the local host.
|
51
51
|
if(url === '') return;
|
52
52
|
if(url.indexOf('%') >= 0) {
|
53
53
|
// Expand %i, %j and %k if used in the URL.
|
54
|
-
const
|
55
|
-
for(let i = 0; i < letters.length; i++) {
|
56
|
-
const l = letters[i];
|
54
|
+
for(const l of ['i', 'j', 'k']) {
|
57
55
|
url = url.replaceAll('%' + l, valueOfIndexVariable(l));
|
58
56
|
}
|
59
57
|
}
|
@@ -361,9 +359,8 @@ class GUIFileManager {
|
|
361
359
|
}
|
362
360
|
fetch('autosave/', postData({
|
363
361
|
action: 'store',
|
364
|
-
file:
|
365
|
-
(MODEL.
|
366
|
-
(MODEL.author || 'no-author')),
|
362
|
+
file: asFileName((MODEL.name || 'no-name') +
|
363
|
+
'_by_' + (MODEL.author || 'no-author')),
|
367
364
|
xml: MODEL.asXML,
|
368
365
|
wsd: workspace
|
369
366
|
}))
|
@@ -404,10 +401,9 @@ class GUIFileManager {
|
|
404
401
|
}
|
405
402
|
}
|
406
403
|
|
407
|
-
|
408
|
-
//
|
409
|
-
|
410
|
-
if(tight) {
|
404
|
+
saveDiagramAsSVG(event) {
|
405
|
+
// Output SVG as string with nodes and arrows 100% opaque.
|
406
|
+
if(event.altKey) {
|
411
407
|
// First align to grid and then fit to size.
|
412
408
|
MODEL.alignToGrid();
|
413
409
|
UI.paper.fitToSize(1);
|
@@ -415,48 +411,15 @@ class GUIFileManager {
|
|
415
411
|
UI.paper.fitToSize();
|
416
412
|
MODEL.alignToGrid();
|
417
413
|
}
|
418
|
-
|
419
|
-
|
420
|
-
|
421
|
-
renderSVGAsPNG(svg) {
|
422
|
-
// Sends SVG to the server, which will convert it to PNG using Inkscape;
|
423
|
-
// if successful, the server will return the URL to the PNG file location;
|
424
|
-
// this URL is passed via the browser's local storage to the newly opened
|
425
|
-
// browser tab that awaits this URL and then loads it
|
426
|
-
const form = {
|
427
|
-
action: 'png',
|
428
|
-
user: VM.solver_user,
|
429
|
-
token: VM.solver_token,
|
430
|
-
data: btoa(encodeURI(svg))
|
431
|
-
};
|
432
|
-
fetch('solver/', postData(form))
|
433
|
-
.then((response) => {
|
434
|
-
if(!response.ok) {
|
435
|
-
UI.alert(`ERROR ${response.status}: ${response.statusText}`);
|
436
|
-
}
|
437
|
-
return response.text();
|
438
|
-
})
|
439
|
-
.then((data) => {
|
440
|
-
// Pass URL of image to the newly opened browser window
|
441
|
-
window.localStorage.setItem('png-url', data);
|
442
|
-
})
|
443
|
-
.catch((err) => UI.warn(UI.WARNING.NO_CONNECTION, err));
|
444
|
-
}
|
445
|
-
|
446
|
-
saveDiagramAsSVG(tight) {
|
447
|
-
// Output SVG as string with nodes and arrows 100% opaque.
|
448
|
-
if(tight) {
|
449
|
-
// First align to grid and then fit to size.
|
450
|
-
MODEL.alignToGrid();
|
451
|
-
UI.paper.fitToSize(1);
|
414
|
+
if(event.shiftKey) {
|
415
|
+
this.pushOutSVG(UI.paper.opaqueSVG);
|
452
416
|
} else {
|
453
|
-
UI.paper.
|
454
|
-
MODEL.alignToGrid();
|
417
|
+
this.pushOutPNG(UI.paper.opaqueSVG);
|
455
418
|
}
|
456
|
-
this.pushOutSVG(UI.paper.opaqueSVG);
|
457
419
|
}
|
458
420
|
|
459
421
|
pushOutSVG(svg) {
|
422
|
+
// Output SVG to browser as SVG image file download.
|
460
423
|
const blob = new Blob([svg], {'type': 'image/svg+xml'});
|
461
424
|
const e = document.getElementById('svg-saver');
|
462
425
|
e.download = 'model.svg';
|
@@ -464,5 +427,36 @@ class GUIFileManager {
|
|
464
427
|
e.href = (window.URL || webkitURL).createObjectURL(blob);
|
465
428
|
e.click();
|
466
429
|
}
|
430
|
+
|
431
|
+
pushOutPNG(svg) {
|
432
|
+
// Output SVG to browser as PNG image file download.
|
433
|
+
const
|
434
|
+
bytes = new TextEncoder().encode(svg),
|
435
|
+
binstr = Array.from(bytes, (b) => String.fromCodePoint(b)).join(''),
|
436
|
+
uri = 'data:image/svg+xml;base64,' + window.btoa(binstr),
|
437
|
+
img = new Image();
|
438
|
+
img.onload = () => {
|
439
|
+
const
|
440
|
+
cvs = document.createElement('canvas'),
|
441
|
+
ctx = cvs.getContext('2d');
|
442
|
+
cvs.width = img.width * 4;
|
443
|
+
cvs.height = img.height * 4;
|
444
|
+
ctx.scale(4, 4);
|
445
|
+
ctx.drawImage(img, 0, 0);
|
446
|
+
cvs.toBlob(blob => {
|
447
|
+
const
|
448
|
+
e = document.getElementById('svg-saver'),
|
449
|
+
url = (window.URL || webkitURL).createObjectURL(blob),
|
450
|
+
name = asFileName(MODEL.focal_cluster.parent ?
|
451
|
+
MODEL.focal_cluster.displayName : MODEL.name) ||
|
452
|
+
'Linny-R-model';
|
453
|
+
e.download = name + '.png';
|
454
|
+
e.type = 'image/png';
|
455
|
+
e.href = url;
|
456
|
+
e.click();
|
457
|
+
});
|
458
|
+
};
|
459
|
+
img.src = uri;
|
460
|
+
}
|
467
461
|
|
468
462
|
} // END of class GUIFileManager
|
@@ -46,7 +46,10 @@ class Finder {
|
|
46
46
|
this.filter_input.addEventListener('input', () => FINDER.changeFilter());
|
47
47
|
this.edit_btn = document.getElementById('finder-edit-btn');
|
48
48
|
this.edit_btn.addEventListener(
|
49
|
-
'click', (
|
49
|
+
'click', () => FINDER.editAttributes());
|
50
|
+
this.chart_btn = document.getElementById('finder-chart-btn');
|
51
|
+
this.chart_btn.addEventListener(
|
52
|
+
'click', () => FINDER.confirmAddChartVariables());
|
50
53
|
this.copy_btn = document.getElementById('finder-copy-btn');
|
51
54
|
this.copy_btn.addEventListener(
|
52
55
|
'click', (event) => FINDER.copyAttributesToClipboard(event.shiftKey));
|
@@ -54,6 +57,13 @@ class Finder {
|
|
54
57
|
this.item_table = document.getElementById('finder-item-table');
|
55
58
|
this.expression_table = document.getElementById('finder-expression-table');
|
56
59
|
|
60
|
+
// The Confirm add chart variables modal.
|
61
|
+
this.add_chart_variables_modal = new ModalDialog('confirm-add-chart-variables');
|
62
|
+
this.add_chart_variables_modal.ok.addEventListener(
|
63
|
+
'click', () => FINDER.addVariablesToChart());
|
64
|
+
this.add_chart_variables_modal.cancel.addEventListener(
|
65
|
+
'click', () => FINDER.add_chart_variables_modal.hide());
|
66
|
+
|
57
67
|
// Attribute headers are used by Finder to output entity attribute values.
|
58
68
|
this.attribute_headers = {
|
59
69
|
A: 'ACTORS:\tWeight\tCash IN\tCash OUT\tCash FLOW',
|
@@ -246,8 +256,8 @@ class Finder {
|
|
246
256
|
}
|
247
257
|
}
|
248
258
|
// Also allow search for scale unit names.
|
249
|
-
if(et
|
250
|
-
imgs
|
259
|
+
if(et === 'U') {
|
260
|
+
imgs = '<img src="images/scale.png">';
|
251
261
|
for(let k in MODEL.products) if(MODEL.products.hasOwnProperty(k)) {
|
252
262
|
if(fp && !k.startsWith(UI.BLACK_BOX) && patternMatch(
|
253
263
|
MODEL.products[k].scale_unit, this.filter_pattern)) {
|
@@ -256,6 +266,37 @@ class Finder {
|
|
256
266
|
addDistinct('Q', this.filtered_types);
|
257
267
|
}
|
258
268
|
}
|
269
|
+
for(let k in MODEL.datasets) if(MODEL.datasets.hasOwnProperty(k)) {
|
270
|
+
if(fp && !k.startsWith(UI.BLACK_BOX)) {
|
271
|
+
const ds = MODEL.datasets[k];
|
272
|
+
if(ds !== MODEL.equations_dataset && patternMatch(
|
273
|
+
ds.scale_unit, this.filter_pattern)) {
|
274
|
+
enl.push(k);
|
275
|
+
this.entities.push(MODEL.datasets[k]);
|
276
|
+
addDistinct('D', this.filtered_types);
|
277
|
+
}
|
278
|
+
}
|
279
|
+
}
|
280
|
+
}
|
281
|
+
// Also allow search for dataset modifier selectors.
|
282
|
+
if(et.indexOf('S') >= 0) {
|
283
|
+
imgs = '<img src="images/dataset.png">';
|
284
|
+
for(let k in MODEL.datasets) if(MODEL.datasets.hasOwnProperty(k)) {
|
285
|
+
if(fp && !k.startsWith(UI.BLACK_BOX)) {
|
286
|
+
const ds = MODEL.datasets[k];
|
287
|
+
if(ds !== MODEL.equations_dataset) {
|
288
|
+
for(let mk in ds.modifiers) if(ds.modifiers.hasOwnProperty(mk)) {
|
289
|
+
if(patternMatch(
|
290
|
+
ds.modifiers[mk].selector, this.filter_pattern)) {
|
291
|
+
enl.push(k);
|
292
|
+
this.entities.push(MODEL.datasets[k]);
|
293
|
+
addDistinct('D', this.filtered_types);
|
294
|
+
break;
|
295
|
+
}
|
296
|
+
}
|
297
|
+
}
|
298
|
+
}
|
299
|
+
}
|
259
300
|
}
|
260
301
|
// Also allow search for link multiplier symbols.
|
261
302
|
if(et.indexOf('M') >= 0) {
|
@@ -301,41 +342,100 @@ class Finder {
|
|
301
342
|
this.edit_btn.style.display = 'none';
|
302
343
|
this.copy_btn.style.display = 'none';
|
303
344
|
if(n > 0) {
|
304
|
-
this.copy_btn.style.display = 'block';
|
345
|
+
this.copy_btn.style.display = 'inline-block';
|
346
|
+
if(CHART_MANAGER.visible && CHART_MANAGER.chart_index >= 0) {
|
347
|
+
const ca = this.commonAttributes;
|
348
|
+
if(ca.length) {
|
349
|
+
this.chart_btn.title = 'Add ' + pluralS(n, 'variable') +
|
350
|
+
' to selected chart';
|
351
|
+
this.chart_btn.style.display = 'inline-block';
|
352
|
+
}
|
353
|
+
}
|
305
354
|
n = this.entityGroup.length;
|
306
355
|
if(n > 0) {
|
307
356
|
this.edit_btn.title = 'Edit attributes of ' +
|
308
357
|
pluralS(n, this.entities[0].type.toLowerCase());
|
309
|
-
this.edit_btn.style.display = 'block';
|
358
|
+
this.edit_btn.style.display = 'inline-block';
|
310
359
|
}
|
311
360
|
}
|
312
361
|
this.updateRightPane();
|
313
362
|
}
|
314
363
|
|
364
|
+
get commonAttributes() {
|
365
|
+
// Returns list of attributes that all filtered entities have in common.
|
366
|
+
let ca = Object.keys(VM.attribute_names);
|
367
|
+
for(const et of this.filtered_types) {
|
368
|
+
ca = intersection(ca, VM.attribute_codes[et]);
|
369
|
+
}
|
370
|
+
return ca;
|
371
|
+
}
|
372
|
+
|
315
373
|
get entityGroup() {
|
316
374
|
// Returns the list of filtered entities if all are of the same type,
|
317
|
-
// while excluding (no actor), (top cluster),
|
375
|
+
// while excluding (no actor), (top cluster), and equations.
|
318
376
|
const
|
319
377
|
eg = [],
|
320
|
-
|
321
|
-
if(
|
322
|
-
const
|
323
|
-
|
324
|
-
|
325
|
-
|
326
|
-
|
327
|
-
|
328
|
-
|
329
|
-
// many of their properties should not be changed.
|
330
|
-
!e.name.startsWith('$')) {
|
331
|
-
eg.push(e);
|
332
|
-
}
|
378
|
+
ft = this.filtered_types[0];
|
379
|
+
if(this.filtered_types.length === 1 && ft !== 'E') {
|
380
|
+
for(const e of this.entities) {
|
381
|
+
// Exclude "no actor" and top cluster.
|
382
|
+
if(e.name && e.name !== '(no_actor)' && e.name !== '(top_cluster)' &&
|
383
|
+
// Also exclude actor cash flow data products because
|
384
|
+
// many of their properties should not be changed.
|
385
|
+
!e.name.startsWith('$')) {
|
386
|
+
eg.push(e);
|
333
387
|
}
|
334
388
|
}
|
335
389
|
}
|
336
390
|
return eg;
|
337
391
|
}
|
338
392
|
|
393
|
+
confirmAddChartVariables() {
|
394
|
+
// Show confirmation dialog to add variables to chart.
|
395
|
+
const
|
396
|
+
md = this.add_chart_variables_modal,
|
397
|
+
n = this.entities.length,
|
398
|
+
ca = this.commonAttributes;
|
399
|
+
let html,
|
400
|
+
et = '1 entity';
|
401
|
+
if(this.filtered_types.length === 1) {
|
402
|
+
et = pluralS(n, this.entities[0].type.toLowerCase());
|
403
|
+
} else if(n !== 1) {
|
404
|
+
et = `${n} entities`;
|
405
|
+
}
|
406
|
+
for(const a of ca) {
|
407
|
+
html += `<option value="${a}">${VM.attribute_names[a]}</option>`;
|
408
|
+
}
|
409
|
+
md.element('attribute').innerHTML = html;
|
410
|
+
md.element('count').innerText = et;
|
411
|
+
md.show();
|
412
|
+
}
|
413
|
+
|
414
|
+
addVariablesToChart() {
|
415
|
+
// Add selected attribute for each filtered entity as chart variable
|
416
|
+
// to the selected chart.
|
417
|
+
const
|
418
|
+
md = this.add_chart_variables_modal,
|
419
|
+
ci = CHART_MANAGER.chart_index;
|
420
|
+
// Double-check whether chart exists.
|
421
|
+
if(ci < 0 || ci >= MODEL.charts.length) {
|
422
|
+
console.log('ANOMALY: No chart for index', ci);
|
423
|
+
}
|
424
|
+
const
|
425
|
+
c = MODEL.charts[ci],
|
426
|
+
a = md.element('attribute').value,
|
427
|
+
s = UI.boxChecked('confirm-add-chart-variables-stacked'),
|
428
|
+
enl = [];
|
429
|
+
for(const e of this.entities) enl.push(e.name);
|
430
|
+
enl.sort((a, b) => UI.compareFullNames(a, b, true));
|
431
|
+
for(const en of enl) {
|
432
|
+
const vi = c.addVariable(en, a);
|
433
|
+
if(vi !== null) c.variables[vi].stacked = s;
|
434
|
+
}
|
435
|
+
CHART_MANAGER.updateDialog();
|
436
|
+
md.hide();
|
437
|
+
}
|
438
|
+
|
339
439
|
updateRightPane() {
|
340
440
|
const
|
341
441
|
se = this.selected_entity,
|
@@ -352,10 +452,7 @@ class Finder {
|
|
352
452
|
if(se.cluster) occ.push(se.cluster.identifier);
|
353
453
|
} else if(se instanceof Product) {
|
354
454
|
// Products "occur" in clusters where they have a position.
|
355
|
-
const
|
356
|
-
for(let i = 0; i < cl.length; i++) {
|
357
|
-
occ.push(cl[i].identifier);
|
358
|
-
}
|
455
|
+
for(const c of se.productPositionClusters) occ.push(c.identifier);
|
359
456
|
} else if(se instanceof Actor) {
|
360
457
|
// Actors "occur" in clusters where they "own" processes or clusters.
|
361
458
|
for(let k in MODEL.processes) if(MODEL.processes.hasOwnProperty(k)) {
|
@@ -374,13 +471,12 @@ class Finder {
|
|
374
471
|
// NOTE: No "occurrence" of datasets or equations.
|
375
472
|
// @@TO DO: identify MODULES (?)
|
376
473
|
// All entities can also occur as chart variables.
|
377
|
-
for(let
|
378
|
-
const c = MODEL.charts[
|
379
|
-
for(
|
380
|
-
const v = c.variables[k];
|
474
|
+
for(let ci = 0; ci < MODEL.charts.length; ci++) {
|
475
|
+
const c = MODEL.charts[ci];
|
476
|
+
for(const v of c.variables) {
|
381
477
|
if(v.object === se || (se instanceof DatasetModifier &&
|
382
478
|
se.identifier === UI.nameToID(v.attribute))) {
|
383
|
-
occ.push(MODEL.chart_id_prefix +
|
479
|
+
occ.push(MODEL.chart_id_prefix + ci);
|
384
480
|
break;
|
385
481
|
}
|
386
482
|
}
|
@@ -437,9 +533,8 @@ class Finder {
|
|
437
533
|
// Check all notes in clusters for their color expressions and field.
|
438
534
|
for(let k in MODEL.clusters) if(MODEL.clusters.hasOwnProperty(k)) {
|
439
535
|
const c = MODEL.clusters[k];
|
440
|
-
for(
|
441
|
-
|
442
|
-
// Look for entity in both note contents and note color expression
|
536
|
+
for(const n of c.notes) {
|
537
|
+
// Look for entity in both note contents and note color expression.
|
443
538
|
if(re.test(n.color.text) || re.test(n.contents)) {
|
444
539
|
xal.push('NOTE');
|
445
540
|
xol.push(n.identifier);
|
@@ -463,11 +558,9 @@ class Finder {
|
|
463
558
|
const c = MODEL.constraints[k];
|
464
559
|
for(let i = 0; i < c.bound_lines.length; i++) {
|
465
560
|
const bl = c.bound_lines[i];
|
466
|
-
for(
|
467
|
-
|
468
|
-
|
469
|
-
xol.push(c.identifier);
|
470
|
-
}
|
561
|
+
for(const sel of bl.selectors) if(re.test(sel.expression.text)) {
|
562
|
+
xal.push('I' + (i + 1));
|
563
|
+
xol.push(c.identifier);
|
471
564
|
}
|
472
565
|
}
|
473
566
|
}
|
@@ -539,7 +632,7 @@ class Finder {
|
|
539
632
|
// look only for the entity types denoted by these letters.
|
540
633
|
let ft = this.filter_input.value,
|
541
634
|
et = VM.entity_letters;
|
542
|
-
if(/^(\*|U|M|[ABCDELPQ]+)\?/i.test(ft)) {
|
635
|
+
if(/^(\*|U|M|S|[ABCDELPQ]+)\?/i.test(ft)) {
|
543
636
|
ft = ft.split('?');
|
544
637
|
// NOTE: *? denotes "all entity types except constraints".
|
545
638
|
et = (ft[0] === '*' ? 'ACDELPQ' : ft[0].toUpperCase());
|
@@ -582,6 +675,7 @@ class Finder {
|
|
582
675
|
if(UI.hidden('dataset-dlg')) {
|
583
676
|
UI.buttons.dataset.dispatchEvent(new Event('click'));
|
584
677
|
}
|
678
|
+
DATASET_MANAGER.expandToShow(obj.name);
|
585
679
|
DATASET_MANAGER.selected_dataset = obj;
|
586
680
|
DATASET_MANAGER.updateDialog();
|
587
681
|
} else if(obj instanceof DatasetModifier) {
|
@@ -715,33 +809,183 @@ class Finder {
|
|
715
809
|
UI.showLinkPropertiesDialog(e, 'R', false, group);
|
716
810
|
} else if(e instanceof Cluster) {
|
717
811
|
UI.showClusterPropertiesDialog(e, group);
|
812
|
+
} else if(e instanceof Dataset) {
|
813
|
+
this.showDatasetGroupDialog(e, group);
|
718
814
|
}
|
719
815
|
}
|
720
816
|
|
817
|
+
showDatasetGroupDialog(ds, dsl) {
|
818
|
+
// Initialize fields with properties of first element of `dsl`.
|
819
|
+
if(!dsl.length) return;
|
820
|
+
const md = UI.modals.datasetgroup;
|
821
|
+
md.group = dsl;
|
822
|
+
md.selected_ds = ds;
|
823
|
+
md.element('no-time-msg').style.display = (ds.array ? 'block' : 'none');
|
824
|
+
md.show('prefix', ds);
|
825
|
+
}
|
826
|
+
|
827
|
+
updateDatasetGroupProperties() {
|
828
|
+
// Update properties of selected group of datasets.
|
829
|
+
const md = UI.modals.datasetgroup;
|
830
|
+
if(!md.group.length) return;
|
831
|
+
// Reduce multiple spaces to a single space.
|
832
|
+
let prefix = md.element('prefix').value.replaceAll(/\s+/gi, ' ').trim();
|
833
|
+
// Trim trailing colons (also when they have spaces between them).
|
834
|
+
while(prefix.endsWith(':')) prefix = prefix.slice(0, -1).trim();
|
835
|
+
// Count the updated chart variables and expressions.
|
836
|
+
let cv_cnt = 0,
|
837
|
+
xr_cnt = 0;
|
838
|
+
// Only rename datasets if prefix has been changed.
|
839
|
+
if(prefix !== md.shared_prefix) {
|
840
|
+
// Check whether prefix is valid.
|
841
|
+
if(prefix && !UI.validName(prefix)) {
|
842
|
+
UI.warn(`Invalid prefix "${prefix}"`);
|
843
|
+
return;
|
844
|
+
}
|
845
|
+
// Add the prefixer ": " to make it a true prefix.
|
846
|
+
if(prefix) prefix += UI.PREFIXER;
|
847
|
+
let old_prefix = md.shared_prefix;
|
848
|
+
if(old_prefix) old_prefix += UI.PREFIXER;
|
849
|
+
// Check whether prefix will create name conflicts.
|
850
|
+
let nc = 0;
|
851
|
+
for(const ds of md.group) {
|
852
|
+
let nn = ds.name;
|
853
|
+
if(nn.startsWith(old_prefix)) {
|
854
|
+
nn = nn.replace(old_prefix, prefix);
|
855
|
+
const obj = MODEL.objectByName(nn);
|
856
|
+
if(obj && obj !== ds) {
|
857
|
+
console.log('Anticipated name conflict with', obj.type,
|
858
|
+
obj.displayName);
|
859
|
+
nc++;
|
860
|
+
}
|
861
|
+
}
|
862
|
+
}
|
863
|
+
if(nc > 0) {
|
864
|
+
UI.warn(`Prefix "${prefix}" will result in` +
|
865
|
+
pluralS(nc, 'name conflict'));
|
866
|
+
return;
|
867
|
+
}
|
868
|
+
// Rename the datasets -- this may affect the group.
|
869
|
+
MODEL.renamePrefixedDatasets(old_prefix, prefix, md.group);
|
870
|
+
cv_cnt += MODEL.variable_count;
|
871
|
+
xr_cnt += MODEL.expression_count;
|
872
|
+
}
|
873
|
+
// Validate input field values.
|
874
|
+
const dv = UI.validNumericInput('datasetgroup-default', 'default value');
|
875
|
+
if(dv === false) return;
|
876
|
+
const ts = UI.validNumericInput('datasetgroup-time-scale', 'time step');
|
877
|
+
if(ts === false) return;
|
878
|
+
// No issues => update *only the modified* properties of all datasets in
|
879
|
+
// the group.
|
880
|
+
const data = {
|
881
|
+
'default': dv,
|
882
|
+
'unit': md.element('unit').value.trim(),
|
883
|
+
'periodic': UI.boxChecked('datasetgroup-periodic'),
|
884
|
+
'array': UI.boxChecked('datasetgroup-array'),
|
885
|
+
'time-scale': ts,
|
886
|
+
'time-unit': md.element('time-unit').value,
|
887
|
+
'method': md.element('method').value
|
888
|
+
};
|
889
|
+
for(let name in md.fields) if(md.changed[name]) {
|
890
|
+
const
|
891
|
+
prop = md.fields[name],
|
892
|
+
value = data[name];
|
893
|
+
for(const ds of md.group) ds[prop] = value;
|
894
|
+
}
|
895
|
+
// Also update the dataset modifiers.
|
896
|
+
const dsv_list = MODEL.datasetVariables;
|
897
|
+
for(const ds of md.group) {
|
898
|
+
for(const k of Object.keys(md.selectors)) {
|
899
|
+
const sel = md.selectors[k];
|
900
|
+
if(ds.modifiers.hasOwnProperty(k)) {
|
901
|
+
// If dataset `ds` has selector with key `k`,
|
902
|
+
// first check if it has been deleted.
|
903
|
+
if(sel.deleted) {
|
904
|
+
// If so, delete this modifier it from `ds`.
|
905
|
+
if(k === ds.default_selector) ds.default_selector = '';
|
906
|
+
delete ds.modifiers[k];
|
907
|
+
} else {
|
908
|
+
// If not deleted, check whether the selector was renamed.
|
909
|
+
const dsm = ds.modifiers[k];
|
910
|
+
let s = k;
|
911
|
+
if(sel.new_s) {
|
912
|
+
// If so, let `s` be the key for new selector.
|
913
|
+
s = UI.nameToID(sel.new_s);
|
914
|
+
dsm.selector = sel.new_s;
|
915
|
+
if(s !== k) {
|
916
|
+
// Add modifier with its own selector key.
|
917
|
+
ds.modifiers[s] = ds.modifiers[k];
|
918
|
+
delete ds.modifiers[k];
|
919
|
+
}
|
920
|
+
// Always update all chart variables referencing dataset + old selector.
|
921
|
+
for(const v of dsv_list) {
|
922
|
+
if(v.object === ds && v.attribute === sel.sel) {
|
923
|
+
v.attribute = sel.new_s;
|
924
|
+
cv_cnt++;
|
925
|
+
}
|
926
|
+
}
|
927
|
+
// Also replace old selector in all expressions (count these as well).
|
928
|
+
xr_cnt += MODEL.replaceAttributeInExpressions(
|
929
|
+
ds.name + '|' + sel.sel, sel.new_s);
|
930
|
+
}
|
931
|
+
// NOTE: Keep original expression unless a new expression is specified.
|
932
|
+
if(sel.new_x) {
|
933
|
+
dsm.expression.text = sel.new_x;
|
934
|
+
// Clear code so the expresion will be recompiled.
|
935
|
+
dsm.expression.code = null;
|
936
|
+
}
|
937
|
+
}
|
938
|
+
} else {
|
939
|
+
// If dataset `ds` has NO selector with key `k`, add the (new) selector.
|
940
|
+
let s = sel.sel,
|
941
|
+
id = k;
|
942
|
+
if(sel.new_s) {
|
943
|
+
s = sel.new_s;
|
944
|
+
id = UI.nameToID(sel.new_s);
|
945
|
+
}
|
946
|
+
const dsm = new DatasetModifier(ds, s);
|
947
|
+
dsm.expression.text = (sel.new_x === false ? sel.expr : sel.new_x);
|
948
|
+
ds.modifiers[id] = dsm;
|
949
|
+
}
|
950
|
+
}
|
951
|
+
// Set the new default selector (if changed).
|
952
|
+
if(md.new_defsel !== false) ds.default_selector = md.new_defsel;
|
953
|
+
}
|
954
|
+
// Notify modeler of changes (if any).
|
955
|
+
const msg = [];
|
956
|
+
if(cv_cnt) msg.push(pluralS(cv_cnt, ' chart variable'));
|
957
|
+
if(xr_cnt) msg.push(pluralS(xr_cnt, ' expression variable'));
|
958
|
+
if(msg.length) {
|
959
|
+
UI.notify('Updated ' + msg.join(' and '));
|
960
|
+
}
|
961
|
+
MODEL.cleanUpScaleUnits();
|
962
|
+
MODEL.updateDimensions();
|
963
|
+
md.hide();
|
964
|
+
// Also update the draggable dialogs that may be affected.
|
965
|
+
UI.updateControllerDialogs('CDEFIJX');
|
966
|
+
}
|
967
|
+
|
721
968
|
copyAttributesToClipboard(shift) {
|
722
969
|
// Copy relevant entity attributes as tab-separated text to clipboard.
|
723
|
-
// NOTE: All entity types have "get" `attributes` that returns an
|
970
|
+
// NOTE: All entity types have "get" method `attributes` that returns an
|
724
971
|
// object that for each defined attribute (and if model has been
|
725
972
|
// solved also each inferred attribute) has a property with its value.
|
726
|
-
// For dynamic expressions, the expression text is used
|
973
|
+
// For dynamic expressions, the expression text is used.
|
727
974
|
const ea_dict = {A: [], B: [], C: [], D: [], E: [], L: [], P: [], Q: []};
|
728
|
-
|
975
|
+
const e = this.selected_entity;
|
729
976
|
if(shift && e) {
|
730
977
|
ea_dict[e.typeLetter].push(e.attributes);
|
731
978
|
} else {
|
732
|
-
for(
|
733
|
-
e = this.entities[i];
|
734
|
-
ea_dict[e.typeLetter].push(e.attributes);
|
735
|
-
}
|
979
|
+
for(const e of this.entities) ea_dict[e.typeLetter].push(e.attributes);
|
736
980
|
}
|
737
981
|
const
|
738
982
|
seq = ['A', 'B', 'C', 'D', 'E', 'P', 'Q', 'L'],
|
739
983
|
text = [],
|
740
984
|
attr = [];
|
741
|
-
for(
|
985
|
+
for(const etl of seq) {
|
742
986
|
const
|
743
|
-
|
744
|
-
|
987
|
+
ead = ea_dict[etl],
|
988
|
+
atcodes = VM.attribute_codes[etl];
|
745
989
|
if(ead && ead.length > 0) {
|
746
990
|
// No blank line before first entity type.
|
747
991
|
if(text.length > 0) text.push('');
|
@@ -755,14 +999,9 @@ class Finder {
|
|
755
999
|
}
|
756
1000
|
text.push(ah);
|
757
1001
|
attr.length = 0;
|
758
|
-
for(
|
759
|
-
const
|
760
|
-
|
761
|
-
ac = VM.attribute_codes[etl],
|
762
|
-
al = [ea.name];
|
763
|
-
for(let j = 0; j < ac.length; j++) {
|
764
|
-
if(ea.hasOwnProperty(ac[j])) al.push(ea[ac[j]]);
|
765
|
-
}
|
1002
|
+
for(const ea of ead) {
|
1003
|
+
const al = [ea.name];
|
1004
|
+
for(const ac of atcodes) if(ea.hasOwnProperty(ac)) al.push(ea[ac]);
|
766
1005
|
attr.push(al.join('\t'));
|
767
1006
|
}
|
768
1007
|
attr.sort();
|