linny-r 2.0.10 → 2.0.12
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/package.json +1 -1
- package/static/index.html +2 -11
- package/static/linny-r.css +10 -0
- package/static/scripts/linny-r-ctrl.js +18 -13
- package/static/scripts/linny-r-gui-controller.js +38 -23
- package/static/scripts/linny-r-gui-dataset-manager.js +79 -65
- package/static/scripts/linny-r-gui-experiment-manager.js +3 -3
- package/static/scripts/linny-r-gui-finder.js +10 -1
- package/static/scripts/linny-r-gui-repository-browser.js +18 -0
- package/static/scripts/linny-r-model.js +80 -46
- package/static/scripts/linny-r-utils.js +13 -2
- package/static/scripts/linny-r-vm.js +61 -4
package/package.json
CHANGED
package/static/index.html
CHANGED
@@ -1942,17 +1942,7 @@ NOTE: * and ? will be interpreted as wildcards"
|
|
1942
1942
|
title="Delete selected dataset"
|
1943
1943
|
style="position: absolute; right: 2px">
|
1944
1944
|
</div>
|
1945
|
-
<div id="dataset-header">
|
1946
|
-
Datasets
|
1947
|
-
<img id="ds-filter-btn" class="btn enab" src="images/filter.png"
|
1948
|
-
title="Filter on name">
|
1949
|
-
</div>
|
1950
|
-
<div id="ds-filter-bar">
|
1951
|
-
<input id="ds-filter-text" type="text"
|
1952
|
-
placeholder="(name filtering pattern)"
|
1953
|
-
title="Pattern may contain logical & (AND), | (OR) and ^ (NOT)
|
1954
|
-
Start with = to find exact match, with ~ to match first characters">
|
1955
|
-
</div>
|
1945
|
+
<div id="dataset-header">Datasets</div>
|
1956
1946
|
<div id="dataset-scroll-area">
|
1957
1947
|
<table id="dataset-table">
|
1958
1948
|
</table>
|
@@ -1984,6 +1974,7 @@ Start with = to find exact match, with ~ to match first characters">
|
|
1984
1974
|
<div id="dataset-export">↑</div>
|
1985
1975
|
</div>
|
1986
1976
|
</div>
|
1977
|
+
<div id="dataset-prefixed-count" class="blink"></div>
|
1987
1978
|
<div id="dataset-separator"></div>
|
1988
1979
|
<div id="dataset-modif-header">(no dataset selected)</div>
|
1989
1980
|
<div id="dataset-modif-ds-name"></div>
|
package/static/linny-r.css
CHANGED
@@ -284,6 +284,7 @@ img.off {
|
|
284
284
|
}
|
285
285
|
}
|
286
286
|
|
287
|
+
div.blink,
|
287
288
|
img.blink {
|
288
289
|
animation: blink 1s step-start 0s infinite;
|
289
290
|
}
|
@@ -2665,6 +2666,15 @@ div.io-box {
|
|
2665
2666
|
height: 59px;
|
2666
2667
|
}
|
2667
2668
|
|
2669
|
+
#dataset-prefixed-count {
|
2670
|
+
position: absolute;
|
2671
|
+
bottom: 2px;
|
2672
|
+
left: 2px;
|
2673
|
+
max-width: 40%;
|
2674
|
+
color: #700090;
|
2675
|
+
font-weight: bold;
|
2676
|
+
}
|
2677
|
+
|
2668
2678
|
#dataset-outcome {
|
2669
2679
|
position: absolute;
|
2670
2680
|
bottom: 0px;
|
@@ -1266,30 +1266,30 @@ class ExperimentManager {
|
|
1266
1266
|
}
|
1267
1267
|
|
1268
1268
|
startExperiment(n=-1) {
|
1269
|
-
// Recompile expressions, as these may have been changed by the modeler
|
1269
|
+
// Recompile expressions, as these may have been changed by the modeler.
|
1270
1270
|
MODEL.compileExpressions();
|
1271
|
-
// Start sequence of solving model parametrizations
|
1271
|
+
// Start sequence of solving model parametrizations.
|
1272
1272
|
const x = this.selected_experiment;
|
1273
1273
|
if(x) {
|
1274
|
-
// Store original model settings
|
1274
|
+
// Store original model settings.
|
1275
1275
|
x.original_model_settings = MODEL.settingsString;
|
1276
1276
|
x.original_round_sequence = MODEL.round_sequence;
|
1277
|
-
// NOTE:
|
1277
|
+
// NOTE: Switch off run chart display.
|
1278
1278
|
CHART_MANAGER.setRunsChart(false);
|
1279
1279
|
// When Chart manager is showing, close it and notify modeler that charts
|
1280
|
-
// should not be viewed during experiments
|
1280
|
+
// should not be viewed during experiments.
|
1281
1281
|
if(CHART_MANAGER.visible) {
|
1282
1282
|
UI.buttons.chart.dispatchEvent(new Event('click'));
|
1283
1283
|
UI.notify(UI.NOTICE.NO_CHARTS);
|
1284
1284
|
}
|
1285
|
-
// Change the buttons -- will return TRUE if experiment was paused
|
1285
|
+
// Change the buttons -- will return TRUE if experiment was paused.
|
1286
1286
|
const paused = this.resumeButtons();
|
1287
1287
|
if(x.completed && n >= 0) {
|
1288
1288
|
x.single_run = n;
|
1289
1289
|
x.active_combination_index = n;
|
1290
1290
|
MODEL.running_experiment = x;
|
1291
1291
|
} else if(!paused) {
|
1292
|
-
// Clear previous run results (if any) unless resuming
|
1292
|
+
// Clear previous run results (if any) unless resuming.
|
1293
1293
|
x.clearRuns();
|
1294
1294
|
x.inferVariables();
|
1295
1295
|
x.time_started = new Date().getTime();
|
@@ -1369,11 +1369,14 @@ class ExperimentManager {
|
|
1369
1369
|
// Perform post-processing after run results have been added.
|
1370
1370
|
const x = MODEL.running_experiment;
|
1371
1371
|
if(!x) return;
|
1372
|
-
const
|
1372
|
+
const
|
1373
|
+
aci = x.active_combination_index,
|
1374
|
+
single = (aci == x.single_run);
|
1373
1375
|
// Always add solver messages.
|
1374
1376
|
x.runs[aci].addMessages();
|
1375
1377
|
const n = x.combinations.length;
|
1376
|
-
if(!VM.halted && aci < n - 1 &&
|
1378
|
+
if(!VM.halted && aci < n - 1 && !single) {
|
1379
|
+
// Continue with the next run.
|
1377
1380
|
if(this.must_pause) {
|
1378
1381
|
this.pausedButtons(aci);
|
1379
1382
|
this.must_pause = false;
|
@@ -1384,12 +1387,13 @@ class ExperimentManager {
|
|
1384
1387
|
// NOTE: When executing a remote command, wait for 1 second to
|
1385
1388
|
// allow enough time for report writing.
|
1386
1389
|
if(RECEIVER.active && RECEIVER.experiment) {
|
1387
|
-
UI.setMessage('Reporting run #' +
|
1390
|
+
UI.setMessage('Reporting run #' + aci);
|
1388
1391
|
delay = 1000;
|
1389
1392
|
}
|
1390
1393
|
setTimeout(() => EXPERIMENT_MANAGER.runModel(), delay);
|
1391
1394
|
}
|
1392
1395
|
} else {
|
1396
|
+
// Stop the run sequence.
|
1393
1397
|
x.time_stopped = new Date().getTime();
|
1394
1398
|
if(x.single_run >= 0) {
|
1395
1399
|
x.single_run = -1;
|
@@ -1409,18 +1413,19 @@ class ExperimentManager {
|
|
1409
1413
|
RECEIVER.experiment = '';
|
1410
1414
|
RECEIVER.callBack();
|
1411
1415
|
}
|
1412
|
-
// Restore original model settings
|
1416
|
+
// Restore original model settings.
|
1413
1417
|
MODEL.running_experiment = null;
|
1414
1418
|
MODEL.parseSettings(x.original_model_settings);
|
1415
1419
|
MODEL.round_sequence = x.original_round_sequence;
|
1416
1420
|
// Reset the Virtual Machine so t=0 at the status line,
|
1417
1421
|
// and ALL expressions are reset as well.
|
1418
|
-
VM.reset();
|
1422
|
+
if(!single) VM.reset();
|
1419
1423
|
this.readyButtons();
|
1420
1424
|
}
|
1421
1425
|
this.drawTable();
|
1422
1426
|
// Reset the model, as results of last run will be showing still.
|
1423
|
-
|
1427
|
+
// NOTE: Do NOT do this after a single run.
|
1428
|
+
if(!single) UI.resetModel();
|
1424
1429
|
CHART_MANAGER.resetChartVectors();
|
1425
1430
|
// NOTE: Clear chart only when done; charts do not update when an
|
1426
1431
|
// experiment is running.
|
@@ -845,7 +845,7 @@ class GUIController extends Controller {
|
|
845
845
|
'time-unit': 'time_unit',
|
846
846
|
'method': 'method'
|
847
847
|
});
|
848
|
-
|
848
|
+
|
849
849
|
// Initially, no dialog being dragged or resized.
|
850
850
|
this.dr_dialog = null;
|
851
851
|
|
@@ -1225,6 +1225,12 @@ class GUIController extends Controller {
|
|
1225
1225
|
// Ensure that all modal windows respond to ESCape
|
1226
1226
|
// (and more in general to other special keys).
|
1227
1227
|
document.addEventListener('keydown', (event) => UI.checkModals(event));
|
1228
|
+
// Ensure that all modal dialogs "swallow" mousedown events, as otherwise
|
1229
|
+
// these may alo be processed by the main window drawing canvas.
|
1230
|
+
for(const modal of document.getElementsByClassName('modal')) {
|
1231
|
+
modal.addEventListener('mousedown', (event) => event.stopPropagation());
|
1232
|
+
}
|
1233
|
+
|
1228
1234
|
}
|
1229
1235
|
|
1230
1236
|
setConstraintUnderCursor(c) {
|
@@ -1704,14 +1710,14 @@ class GUIController extends Controller {
|
|
1704
1710
|
//
|
1705
1711
|
|
1706
1712
|
draggableDialog(d) {
|
1707
|
-
// Make dialog draggable
|
1713
|
+
// Make dialog draggable.
|
1708
1714
|
const
|
1709
1715
|
dlg = document.getElementById(d + '-dlg'),
|
1710
1716
|
hdr = document.getElementById(d + '-hdr');
|
1711
1717
|
let cx = 0,
|
1712
1718
|
cy = 0;
|
1713
1719
|
if(dlg && hdr) {
|
1714
|
-
// NOTE:
|
1720
|
+
// NOTE: Dialogs are draggable only by their header.
|
1715
1721
|
hdr.onmousedown = dialogHeaderMouseDown;
|
1716
1722
|
dlg.onmousedown = dialogMouseDown;
|
1717
1723
|
return dlg;
|
@@ -1722,13 +1728,13 @@ class GUIController extends Controller {
|
|
1722
1728
|
|
1723
1729
|
function dialogMouseDown(e) {
|
1724
1730
|
e = e || window.event;
|
1725
|
-
// NOTE:
|
1726
|
-
// Find the dialog element
|
1731
|
+
// NOTE: No `preventDefault`, as this disables selector elements.
|
1732
|
+
// Find the dialog element.
|
1727
1733
|
let de = e.target;
|
1728
1734
|
while(de && !de.id.endsWith('-dlg')) { de = de.parentElement; }
|
1729
|
-
//
|
1735
|
+
// Move the dialog (`this`) to the top of the order.
|
1730
1736
|
const doi = UI.dr_dialog_order.indexOf(de);
|
1731
|
-
// NOTE:
|
1737
|
+
// NOTE: Do not reorder when already at end of list (= at top).
|
1732
1738
|
if(doi >= 0 && doi !== UI.dr_dialog_order.length - 1) {
|
1733
1739
|
UI.dr_dialog_order.splice(doi, 1);
|
1734
1740
|
UI.dr_dialog_order.push(de);
|
@@ -1739,12 +1745,12 @@ class GUIController extends Controller {
|
|
1739
1745
|
function dialogHeaderMouseDown(e) {
|
1740
1746
|
e = e || window.event;
|
1741
1747
|
e.preventDefault();
|
1742
|
-
// Find the dialog element
|
1748
|
+
// Find the dialog element.
|
1743
1749
|
let de = e.target;
|
1744
1750
|
while(de && !de.id.endsWith('-dlg')) { de = de.parentElement; }
|
1745
|
-
// Record the affected dialog
|
1751
|
+
// Record the affected dialog.
|
1746
1752
|
UI.dr_dialog = de;
|
1747
|
-
// Get the mouse cursor position at startup
|
1753
|
+
// Get the mouse cursor position at startup.
|
1748
1754
|
cx = e.clientX;
|
1749
1755
|
cy = e.clientY;
|
1750
1756
|
document.onmouseup = stopDragDialog;
|
@@ -1927,18 +1933,19 @@ class GUIController extends Controller {
|
|
1927
1933
|
// Button functionality
|
1928
1934
|
//
|
1929
1935
|
|
1930
|
-
enableButtons(btns) {
|
1936
|
+
enableButtons(btns, blink=false) {
|
1931
1937
|
for(const btn of btns.trim().split(/\s+/)) {
|
1932
1938
|
const b = document.getElementById(btn + '-btn');
|
1933
|
-
b.classList.remove('disab', 'activ');
|
1939
|
+
b.classList.remove('disab', 'activ', 'blink');
|
1934
1940
|
b.classList.add('enab');
|
1941
|
+
if(blink) b.classList.add('blink');
|
1935
1942
|
}
|
1936
1943
|
}
|
1937
1944
|
|
1938
1945
|
disableButtons(btns) {
|
1939
1946
|
for(const btn of btns.trim().split(/\s+/)) {
|
1940
1947
|
const b = document.getElementById(btn + '-btn');
|
1941
|
-
b.classList.remove('enab', 'activ', 'stay-activ');
|
1948
|
+
b.classList.remove('enab', 'activ', 'stay-activ', 'blink');
|
1942
1949
|
b.classList.add('disab');
|
1943
1950
|
}
|
1944
1951
|
}
|
@@ -2251,6 +2258,8 @@ class GUIController extends Controller {
|
|
2251
2258
|
this.net_move_y = 0;
|
2252
2259
|
// Get the paper coordinates indicated by the cursor.
|
2253
2260
|
const cp = this.paper.cursorPosition(e.pageX, e.pageY);
|
2261
|
+
this.mouse_x = cp[0];
|
2262
|
+
this.mouse_y = cp[1];
|
2254
2263
|
this.mouse_down_x = cp[0];
|
2255
2264
|
this.mouse_down_y = cp[1];
|
2256
2265
|
// De-activate "stay active" buttons if dysfunctional, or if SHIFT,
|
@@ -2632,17 +2641,11 @@ class GUIController extends Controller {
|
|
2632
2641
|
// Handler for keyboard events
|
2633
2642
|
//
|
2634
2643
|
|
2635
|
-
|
2636
|
-
//
|
2637
|
-
const
|
2638
|
-
ttype = e.target.type,
|
2639
|
-
ttag = e.target.tagName,
|
2640
|
-
modals = document.getElementsByClassName('modal');
|
2641
|
-
// Modal dialogs: hide on ESC and move to next input on ENTER.
|
2644
|
+
get topModal() {
|
2645
|
+
// Return the topmost visible modal dialog, or NULL if none are showing.
|
2646
|
+
const modals = document.getElementsByClassName('modal');
|
2642
2647
|
let maxz = 0,
|
2643
|
-
topmod = null
|
2644
|
-
code = e.code,
|
2645
|
-
alt = e.altKey;
|
2648
|
+
topmod = null;
|
2646
2649
|
for(const m of modals) {
|
2647
2650
|
const
|
2648
2651
|
cs = window.getComputedStyle(m),
|
@@ -2652,6 +2655,18 @@ class GUIController extends Controller {
|
|
2652
2655
|
maxz = z;
|
2653
2656
|
}
|
2654
2657
|
}
|
2658
|
+
return topmod;
|
2659
|
+
}
|
2660
|
+
|
2661
|
+
checkModals(e) {
|
2662
|
+
// Respond to Escape, Enter and shortcut keys.
|
2663
|
+
const
|
2664
|
+
ttype = e.target.type,
|
2665
|
+
ttag = e.target.tagName,
|
2666
|
+
code = e.code,
|
2667
|
+
alt = e.altKey,
|
2668
|
+
topmod = this.topModal;
|
2669
|
+
// Modal dialogs: hide on ESC and move to next input on ENTER.
|
2655
2670
|
// NOTE: Consider only the top modal (if any is showing).
|
2656
2671
|
if(code === 'Escape') {
|
2657
2672
|
e.stopImmediatePropagation();
|
@@ -11,7 +11,7 @@ for the Linny-R Dataset Manager dialog.
|
|
11
11
|
*/
|
12
12
|
|
13
13
|
/*
|
14
|
-
Copyright (c) 2017-
|
14
|
+
Copyright (c) 2017-2025 Delft University of Technology
|
15
15
|
|
16
16
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
17
17
|
of this software and associated documentation files (the "Software"), to deal
|
@@ -56,15 +56,11 @@ class GUIDatasetManager extends DatasetManager {
|
|
56
56
|
'click', () => DATASET_MANAGER.load_csv_modal.show());
|
57
57
|
document.getElementById('ds-delete-btn').addEventListener(
|
58
58
|
'click', () => DATASET_MANAGER.deleteDataset());
|
59
|
-
document.getElementById('ds-filter-btn').addEventListener(
|
60
|
-
'click', () => DATASET_MANAGER.toggleFilter());
|
61
|
-
// Update when filter input text changes.
|
62
|
-
this.filter_text = document.getElementById('ds-filter-text');
|
63
|
-
this.filter_text.addEventListener(
|
64
|
-
'input', () => DATASET_MANAGER.changeFilter());
|
65
59
|
this.dataset_table = document.getElementById('dataset-table');
|
66
|
-
// Data properties pane.
|
60
|
+
// Data properties pane below the dataset scroll area.
|
67
61
|
this.properties = document.getElementById('dataset-properties');
|
62
|
+
// Number of prefixed datasets is displayed at bottom of left pane.
|
63
|
+
this.prefixed_count = document.getElementById('dataset-prefixed-count');
|
68
64
|
// Toggle buttons at bottom of dialog.
|
69
65
|
this.blackbox = document.getElementById('dataset-blackbox');
|
70
66
|
this.blackbox.addEventListener(
|
@@ -144,12 +140,13 @@ class GUIDatasetManager extends DatasetManager {
|
|
144
140
|
reset() {
|
145
141
|
super.reset();
|
146
142
|
this.selected_prefix_row = null;
|
143
|
+
this.selected_dataset = null;
|
147
144
|
this.selected_modifier = null;
|
148
145
|
this.edited_expression = null;
|
149
|
-
this.filter_pattern = null;
|
150
146
|
this.clicked_object = null;
|
151
147
|
this.last_time_clicked = 0;
|
152
148
|
this.focal_table = null;
|
149
|
+
this.prefixed_datasets = [];
|
153
150
|
this.expanded_rows = [];
|
154
151
|
}
|
155
152
|
|
@@ -291,6 +288,17 @@ class GUIDatasetManager extends DatasetManager {
|
|
291
288
|
for(const r of this.dataset_table.rows) if(r.dataset.prefix === lcp) return r;
|
292
289
|
return null;
|
293
290
|
}
|
291
|
+
|
292
|
+
datasetsByPrefix(prefix) {
|
293
|
+
// Return the list of datasets having the specified prefix.
|
294
|
+
const
|
295
|
+
pid = UI.nameToID(prefix + UI.PREFIXER),
|
296
|
+
dsl = [];
|
297
|
+
for(const k of Object.keys(MODEL.datasets)) {
|
298
|
+
if(k.startsWith(pid)) dsl.push(k);
|
299
|
+
}
|
300
|
+
return dsl;
|
301
|
+
}
|
294
302
|
|
295
303
|
selectPrefixRow(e) {
|
296
304
|
// Select expand/collapse prefix row.
|
@@ -302,6 +310,7 @@ class GUIDatasetManager extends DatasetManager {
|
|
302
310
|
const toggle = r.classList.contains('tree-btn');
|
303
311
|
while(r.tagName !== 'TR') r = r.parentNode;
|
304
312
|
this.selected_prefix_row = r;
|
313
|
+
this.prefixed_datasets = this.datasetsByPrefix(r.dataset.prefix);
|
305
314
|
const sel = this.dataset_table.getElementsByClassName('sel-set');
|
306
315
|
this.selected_dataset = null;
|
307
316
|
if(sel.length > 0) {
|
@@ -311,7 +320,7 @@ class GUIDatasetManager extends DatasetManager {
|
|
311
320
|
r.classList.add('sel-set');
|
312
321
|
if(!e.target) r.scrollIntoView({block: 'center'});
|
313
322
|
if(toggle || e.altKey || this.doubleClicked(r)) this.togglePrefixRow(e);
|
314
|
-
|
323
|
+
this.updatePanes();
|
315
324
|
}
|
316
325
|
|
317
326
|
updateDialog() {
|
@@ -321,18 +330,13 @@ class GUIDatasetManager extends DatasetManager {
|
|
321
330
|
dnl = [],
|
322
331
|
sd = this.selected_dataset,
|
323
332
|
ioclass = ['', 'import', 'export'];
|
324
|
-
for(let d in MODEL.datasets) if(MODEL.datasets.hasOwnProperty(d)
|
325
|
-
|
326
|
-
|
327
|
-
|
328
|
-
MODEL.datasets[d] !== MODEL.equations_dataset) {
|
329
|
-
if(!this.filter_pattern || this.filter_pattern.length === 0 ||
|
330
|
-
patternMatch(MODEL.datasets[d].displayName, this.filter_pattern)) {
|
331
|
-
dnl.push(d);
|
332
|
-
}
|
333
|
+
for(let d in MODEL.datasets) if(MODEL.datasets.hasOwnProperty(d)) {
|
334
|
+
// NOTE: Do not list "black-boxed" entities or the equations dataset.
|
335
|
+
if(!d.startsWith(UI.BLACK_BOX) &&
|
336
|
+
MODEL.datasets[d] !== MODEL.equations_dataset) dnl.push(d);
|
333
337
|
}
|
334
338
|
dnl.sort((a, b) => UI.compareFullNames(a, b, true));
|
335
|
-
// First determine indentation levels, prefixes and names
|
339
|
+
// First determine indentation levels, prefixes and names.
|
336
340
|
const
|
337
341
|
indent = [],
|
338
342
|
pref_ids = [],
|
@@ -341,11 +345,11 @@ class GUIDatasetManager extends DatasetManager {
|
|
341
345
|
xids = [];
|
342
346
|
for(const dn of dnl) {
|
343
347
|
const pref = UI.prefixesAndName(MODEL.datasets[dn].name);
|
344
|
-
// NOTE:
|
348
|
+
// NOTE: Only the name part (so no prefixes at all) will be shown.
|
345
349
|
names.push(pref.pop());
|
346
350
|
indent.push(pref.length);
|
347
|
-
// NOTE:
|
348
|
-
// can contain any character; only the prefixer is "reserved"
|
351
|
+
// NOTE: Ignore case but join again with ": " because prefixes
|
352
|
+
// can contain any character; only the prefixer is "reserved".
|
349
353
|
const pref_id = pref.join(UI.PREFIXER).toLowerCase();
|
350
354
|
pref_ids.push(pref_id);
|
351
355
|
pref_names[pref_id] = pref;
|
@@ -363,11 +367,11 @@ class GUIDatasetManager extends DatasetManager {
|
|
363
367
|
} else {
|
364
368
|
ind_div = '';
|
365
369
|
}
|
366
|
-
// NOTE:
|
370
|
+
// NOTE: Empty string should not add a collapse/expand row.
|
367
371
|
if(pid && pid != prev_id && xids.indexOf(pid) < 0) {
|
368
372
|
// NOTE: XX: aa may be followed by XX: YY: ZZ: bb, which requires
|
369
373
|
// *two* collapsable lines: XX: YY and XX: YY: ZZ: before adding
|
370
|
-
// XX: YY: ZZ: bb
|
374
|
+
// XX: YY: ZZ: bb.
|
371
375
|
const
|
372
376
|
ps = pid.split(UI.PREFIXER),
|
373
377
|
pps = prev_id.split(UI.PREFIXER),
|
@@ -375,20 +379,20 @@ class GUIDatasetManager extends DatasetManager {
|
|
375
379
|
pns = pn.join(UI.PREFIXER),
|
376
380
|
lpl = [];
|
377
381
|
let lindent = 0;
|
378
|
-
// Ignore identical leading prefixes
|
382
|
+
// Ignore identical leading prefixes.
|
379
383
|
while(ps.length > 0 && pps.length > 0 && ps[0] === pps[0]) {
|
380
384
|
lpl.push(ps.shift());
|
381
385
|
pps.shift();
|
382
386
|
pn.shift();
|
383
387
|
lindent++;
|
384
388
|
}
|
385
|
-
// Add a "collapse" row for each new prefix
|
389
|
+
// Add a "collapse" row for each new prefix.
|
386
390
|
while(ps.length > 0) {
|
387
391
|
lpl.push(ps.shift());
|
388
392
|
lindent++;
|
389
393
|
const lpid = lpl.join(UI.PREFIXER);
|
390
394
|
dl.push(['<tr data-prefix="', lpid,
|
391
|
-
'" data-prefix-name="', pns, '" class="dataset"',
|
395
|
+
'" data-prefix-name="', pns.slice(0, lpid.length), '" class="dataset"',
|
392
396
|
'onclick="DATASET_MANAGER.selectPrefixRow(event);"><td>',
|
393
397
|
// NOTE: data-prefix="x" signals that this is an extra row
|
394
398
|
(lindent > 0 ?
|
@@ -398,7 +402,7 @@ class GUIDatasetManager extends DatasetManager {
|
|
398
402
|
'<div data-prefix="x" class="tree-btn">',
|
399
403
|
(this.expanded_rows.indexOf(lpid) >= 0 ? '\u25BC' : '\u25BA'),
|
400
404
|
'</div>', pn.shift(), '</td></tr>'].join(''));
|
401
|
-
// Add to the list to prevent multiple c/x-rows for the same prefix
|
405
|
+
// Add to the list to prevent multiple c/x-rows for the same prefix.
|
402
406
|
xids.push(lpid);
|
403
407
|
}
|
404
408
|
}
|
@@ -437,6 +441,7 @@ class GUIDatasetManager extends DatasetManager {
|
|
437
441
|
sd = this.selected_dataset,
|
438
442
|
btns = 'ds-data ds-clone ds-delete ds-rename';
|
439
443
|
if(sd) {
|
444
|
+
this.prefixed_count.style.display = 'none';
|
440
445
|
this.properties.style.display = 'block';
|
441
446
|
document.getElementById('dataset-default').innerHTML =
|
442
447
|
VM.sig4Dig(sd.default_value) +
|
@@ -469,8 +474,22 @@ class GUIDatasetManager extends DatasetManager {
|
|
469
474
|
UI.enableButtons(btns);
|
470
475
|
} else {
|
471
476
|
this.properties.style.display = 'none';
|
477
|
+
const
|
478
|
+
pdsl = this.prefixed_datasets.length,
|
479
|
+
npds = pluralS(pdsl, 'dataset');
|
480
|
+
this.prefixed_count.innerText = npds;
|
481
|
+
this.prefixed_count.style.display = (pdsl ? 'block' : 'none');
|
472
482
|
UI.disableButtons(btns);
|
473
|
-
if(this.selected_prefix_row)
|
483
|
+
if(this.selected_prefix_row) {
|
484
|
+
UI.enableButtons('ds-rename ds-delete', true);
|
485
|
+
document.getElementById('ds-rename-btn')
|
486
|
+
.title = `Rename ${npds} by changing prefix "${this.selectedPrefix}"`;
|
487
|
+
document.getElementById('ds-delete-btn')
|
488
|
+
.title = `Delete ${npds} having prefix "${this.selectedPrefix}"`;
|
489
|
+
} else {
|
490
|
+
document.getElementById('ds-rename-btn').title = 'Rename selected dataset';
|
491
|
+
document.getElementById('ds-delete-btn').title = 'Delete selected dataset';
|
492
|
+
}
|
474
493
|
}
|
475
494
|
this.updateModifiers();
|
476
495
|
}
|
@@ -555,37 +574,14 @@ class GUIDatasetManager extends DatasetManager {
|
|
555
574
|
if(d) DOCUMENTATION_MANAGER.update(d, shift);
|
556
575
|
}
|
557
576
|
|
558
|
-
toggleFilter() {
|
559
|
-
const
|
560
|
-
btn = document.getElementById('ds-filter-btn'),
|
561
|
-
bar = document.getElementById('ds-filter-bar'),
|
562
|
-
dsa = document.getElementById('dataset-scroll-area');
|
563
|
-
if(btn.classList.toggle('stay-activ')) {
|
564
|
-
bar.style.display = 'block';
|
565
|
-
dsa.style.top = '81px';
|
566
|
-
dsa.style.height = 'calc(100% - 141px)';
|
567
|
-
this.changeFilter();
|
568
|
-
} else {
|
569
|
-
bar.style.display = 'none';
|
570
|
-
dsa.style.top = '62px';
|
571
|
-
dsa.style.height = 'calc(100% - 122px)';
|
572
|
-
this.filter_pattern = null;
|
573
|
-
this.updateDialog();
|
574
|
-
}
|
575
|
-
}
|
576
|
-
|
577
|
-
changeFilter() {
|
578
|
-
this.filter_pattern = patternList(this.filter_text.value);
|
579
|
-
this.updateDialog();
|
580
|
-
}
|
581
|
-
|
582
577
|
selectDataset(event, id) {
|
583
|
-
// Select dataset, or edit it when Alt- or double-clicked
|
578
|
+
// Select dataset, or edit it when Alt- or double-clicked.
|
584
579
|
this.focal_table = this.dataset_table;
|
585
580
|
const
|
586
581
|
d = MODEL.datasets[id] || null,
|
587
582
|
edit = event.altKey || this.doubleClicked(d);
|
588
583
|
this.selected_dataset = d;
|
584
|
+
this.prefixed_datasets.length = 0;
|
589
585
|
if(d && edit) {
|
590
586
|
this.last_time_clicked = 0;
|
591
587
|
this.editData();
|
@@ -748,18 +744,34 @@ class GUIDatasetManager extends DatasetManager {
|
|
748
744
|
this.updateDialog();
|
749
745
|
}
|
750
746
|
}
|
747
|
+
|
748
|
+
get selectedAsList() {
|
749
|
+
// Return list of datasets selected directly or by prefix.
|
750
|
+
const dsl = [];
|
751
|
+
// Prevent including the equations dataset (just in case).
|
752
|
+
if(this.selected_dataset && this.selected_dataset !== MODEL.equations_dataset) {
|
753
|
+
dsl.push(this.selected_dataset);
|
754
|
+
} else {
|
755
|
+
// NOTE: List of prefixed datasets contains keys, not objects.
|
756
|
+
for(const k of this.prefixed_datasets) {
|
757
|
+
const ds = MODEL.datasets[k];
|
758
|
+
if(ds !== MODEL.equations_dataset) dsl.push();
|
759
|
+
}
|
760
|
+
}
|
761
|
+
return dsl;
|
762
|
+
}
|
751
763
|
|
752
764
|
deleteDataset() {
|
753
|
-
|
754
|
-
|
755
|
-
|
756
|
-
MODEL.
|
757
|
-
MODEL.
|
758
|
-
delete MODEL.datasets[d.identifier];
|
759
|
-
this.selected_dataset = null;
|
760
|
-
this.updateDialog();
|
761
|
-
MODEL.updateDimensions();
|
765
|
+
// Delete selected dataset(s).
|
766
|
+
for(const ds of this.selectedAsList) {
|
767
|
+
MODEL.removeImport(ds);
|
768
|
+
MODEL.removeExport(ds);
|
769
|
+
delete MODEL.datasets[ds.identifier];
|
762
770
|
}
|
771
|
+
this.selected_dataset = null;
|
772
|
+
this.prefixed_datasets.length = 0;
|
773
|
+
this.updateDialog();
|
774
|
+
MODEL.updateDimensions();
|
763
775
|
}
|
764
776
|
|
765
777
|
toggleBlackBox() {
|
@@ -1249,7 +1261,7 @@ class GUIDatasetManager extends DatasetManager {
|
|
1249
1261
|
ods = MODEL.namedObjectByID(id),
|
1250
1262
|
ds = ods || MODEL.addDataset(n);
|
1251
1263
|
// NOTE: `ds` will now be either a new dataset or an existing one.
|
1252
|
-
if(ds) {
|
1264
|
+
if(ds instanceof Dataset) {
|
1253
1265
|
// Keep track of added/updated datasets.
|
1254
1266
|
const
|
1255
1267
|
odv = ds.default_value,
|
@@ -1262,6 +1274,8 @@ class GUIDatasetManager extends DatasetManager {
|
|
1262
1274
|
added++;
|
1263
1275
|
}
|
1264
1276
|
ds.computeStatistics();
|
1277
|
+
} else {
|
1278
|
+
UI.warn(`Name conflict: ${ds.type} "${ds.displayName}" already exists`);
|
1265
1279
|
}
|
1266
1280
|
}
|
1267
1281
|
// Notify modeler of changes (if any).
|
@@ -844,8 +844,8 @@ class GUIExperimentManager extends ExperimentManager {
|
|
844
844
|
}
|
845
845
|
|
846
846
|
showInfo(n, shift) {
|
847
|
-
// Display documentation for the n-th experiment defined in the model
|
848
|
-
// NOTE:
|
847
|
+
// Display documentation for the n-th experiment defined in the model.
|
848
|
+
// NOTE: Skip when viewer is showing!
|
849
849
|
if(!UI.hidden('experiment-viewer')) return;
|
850
850
|
if(n < MODEL.experiments.length) {
|
851
851
|
// NOTE: mouse move over title in viewer passes n = -1
|
@@ -856,7 +856,7 @@ class GUIExperimentManager extends ExperimentManager {
|
|
856
856
|
|
857
857
|
showRunInfo(n, shift) {
|
858
858
|
// Display information on the n-th combination if docu-viewer is visible
|
859
|
-
// and cursor is moved over run cell while Shift button is held down
|
859
|
+
// and cursor is moved over run cell while Shift button is held down.
|
860
860
|
if(shift && DOCUMENTATION_MANAGER.visible) {
|
861
861
|
const info = this.runInfo(n);
|
862
862
|
if(info) {
|
@@ -949,7 +949,16 @@ class Finder {
|
|
949
949
|
}
|
950
950
|
}
|
951
951
|
// Set the new default selector (if changed).
|
952
|
-
if(md.new_defsel !== false)
|
952
|
+
if(md.new_defsel !== false) {
|
953
|
+
// NOTE: `new_defsel` is a key; the actual selector name may have upper case
|
954
|
+
// letters, so get the selector name.
|
955
|
+
const dsm = ds.modifiers[md.new_defsel];
|
956
|
+
if(dsm) {
|
957
|
+
ds.default_selector = dsm.selector;
|
958
|
+
} else {
|
959
|
+
throw(`Unknown selector: ${md.new_defsel}`);
|
960
|
+
}
|
961
|
+
}
|
953
962
|
}
|
954
963
|
// Notify modeler of changes (if any).
|
955
964
|
const msg = [];
|
@@ -289,6 +289,8 @@ class GUIRepositoryBrowser extends RepositoryBrowser {
|
|
289
289
|
'click', () => REPOSITORY_BROWSER.performInclusion());
|
290
290
|
this.include_modal.cancel.addEventListener(
|
291
291
|
'click', () => REPOSITORY_BROWSER.cancelInclusion());
|
292
|
+
this.include_modal.element('prefix').addEventListener(
|
293
|
+
'blur', () => REPOSITORY_BROWSER.suggestBindings());
|
292
294
|
this.include_modal.element('actor').addEventListener(
|
293
295
|
'blur', () => REPOSITORY_BROWSER.updateActors());
|
294
296
|
|
@@ -614,6 +616,22 @@ class GUIRepositoryBrowser extends RepositoryBrowser {
|
|
614
616
|
md.show('prefix');
|
615
617
|
}
|
616
618
|
|
619
|
+
suggestBindings() {
|
620
|
+
// Select for each "Cluster: XXX" drop-down the one that matches the
|
621
|
+
// value of the prefix input field.
|
622
|
+
const
|
623
|
+
md = this.include_modal,
|
624
|
+
prefix = md.element('prefix').value.trim(),
|
625
|
+
sa = md.element('scroll-area'),
|
626
|
+
sels = sa.querySelectorAll('select');
|
627
|
+
for(const sel of sels) {
|
628
|
+
const
|
629
|
+
oid = UI.nameToID(prefix) + ':_' + sel.id,
|
630
|
+
ids = [...sel.options].map(o => o.value);
|
631
|
+
if(ids.indexOf(oid) >= 0) sel.value = oid;
|
632
|
+
}
|
633
|
+
}
|
634
|
+
|
617
635
|
updateActors() {
|
618
636
|
// Add actor (if specified) to model, and then updates the selector options
|
619
637
|
// for each actor binding selector.
|
@@ -501,6 +501,30 @@ class LinnyRModel {
|
|
501
501
|
return this.namedObjectByID(UI.nameToID(name));
|
502
502
|
}
|
503
503
|
|
504
|
+
validVariable(name) {
|
505
|
+
// Return TRUE if `name` references an entity plus valid attribute.
|
506
|
+
const
|
507
|
+
ea = name.split('|'),
|
508
|
+
en = ea[0].trim(),
|
509
|
+
e = this.objectByName(en);
|
510
|
+
if(!e) return `Unknown model entity "${en}"`;
|
511
|
+
const
|
512
|
+
ao = ea[1].split('@'),
|
513
|
+
a = ao[0].trim();
|
514
|
+
// Valid if no attribute, as all entity types have a default attribute.
|
515
|
+
if(!a) return true;
|
516
|
+
// Attribute should be valid for the entity type.
|
517
|
+
const
|
518
|
+
et = e.type.toLowerCase(),
|
519
|
+
ac = VM.attribute_codes[VM.entity_letter_codes[et]];
|
520
|
+
if(ac.indexOf(a) >= 0 || (e instanceof Cluster && a.startsWith('=')) ||
|
521
|
+
(e instanceof Dataset && e.modifiers.hasOwnProperty(a.toLowerCase()))) {
|
522
|
+
return true;
|
523
|
+
}
|
524
|
+
if(e instanceof Dataset) return `Dataset ${e.displayName} has no modifier "${a}"`;
|
525
|
+
return `Invalid attribute "${a}"`;
|
526
|
+
}
|
527
|
+
|
504
528
|
setByType(type) {
|
505
529
|
// Return a "dictionary" object with entities of the specified types
|
506
530
|
if(type === 'Process') return this.processes;
|
@@ -4370,22 +4394,24 @@ class IOBinding {
|
|
4370
4394
|
datastyle = (this.is_data ?
|
4371
4395
|
'; text-decoration: 1.5px dashed underline' : '');
|
4372
4396
|
let html = ['<tr class="', ioc[this.io_type], '-param">',
|
4373
|
-
'<td style="padding-bottom:2px">',
|
4374
|
-
'<span style="font-style:normal; font-weight:normal', datastyle, '">',
|
4397
|
+
'<td style="padding-bottom: 2px">',
|
4398
|
+
'<span style="font-style: normal; font-weight: normal', datastyle, '">',
|
4375
4399
|
this.entity_type, ':</span> ', this.name_in_module].join('');
|
4376
4400
|
if(this.io_type === 1) {
|
4377
4401
|
// An IMPORT binding generates two rows: the formal name (in the module)
|
4378
|
-
// and the actual name (in the current model) as dropdown box
|
4379
|
-
// NOTE:
|
4380
|
-
// means that the parameter is not bound to an entity in the current model
|
4402
|
+
// and the actual name (in the current model) as dropdown box.
|
4403
|
+
// NOTE: The first (default) option is the *prefixed* formal name, which
|
4404
|
+
// means that the parameter is not bound to an entity in the current model.
|
4381
4405
|
html += ['<br>⤷<select id="', this.id, '" name="', this.id,
|
4382
|
-
'" class="i-param"><option value="_CLUSTER">Cluster: ',
|
4406
|
+
'" class="i-param"><option value="_CLUSTER" style="color: purple">Cluster: ',
|
4383
4407
|
this.name_in_module, '</option>'].join('');
|
4384
4408
|
const
|
4385
4409
|
s = MODEL.setByType(this.entity_type),
|
4386
|
-
|
4410
|
+
tail = ':_' + this.name_in_module.toLowerCase(),
|
4411
|
+
index = Object.keys(s).sort(
|
4412
|
+
(a, b) => compareTailFirst(a, b, tail));
|
4387
4413
|
if(s === MODEL.datasets) {
|
4388
|
-
// NOTE:
|
4414
|
+
// NOTE: Do not list the model equations as dataset.
|
4389
4415
|
const i = index.indexOf(UI.EQUATIONS_DATASET_ID);
|
4390
4416
|
if(i >= 0) index.splice(i, 1);
|
4391
4417
|
}
|
@@ -4467,12 +4493,14 @@ class IOContext {
|
|
4467
4493
|
}
|
4468
4494
|
|
4469
4495
|
actualName(n, an='') {
|
4470
|
-
//
|
4496
|
+
// Return the actual name for a parameter with formal name `n`
|
4471
4497
|
// (and for processes and clusters: with actor name `an` if specified and
|
4472
|
-
// not "(no actor)")
|
4473
|
-
// NOTE:
|
4498
|
+
// not "(no actor)").
|
4499
|
+
// NOTE: Do not modify (no actor), nor the "dataset dot".
|
4474
4500
|
if(n === UI.NO_ACTOR || n === '.') return n;
|
4475
|
-
// NOTE:
|
4501
|
+
// NOTE: Do not modify "prefix-relative" variables.
|
4502
|
+
if(n.startsWith(':')) return n;
|
4503
|
+
// NOTE: The top cluster of the included model has the prefix as its name.
|
4476
4504
|
if(n === UI.TOP_CLUSTER_NAME || n === UI.FORMER_TOP_CLUSTER_NAME) {
|
4477
4505
|
return this.prefix;
|
4478
4506
|
}
|
@@ -4489,7 +4517,7 @@ class IOContext {
|
|
4489
4517
|
return n;
|
4490
4518
|
}
|
4491
4519
|
// All other entities are prefixed
|
4492
|
-
return (this.prefix ? this.prefix +
|
4520
|
+
return (this.prefix ? this.prefix + UI.PREFIXER : '') + n;
|
4493
4521
|
}
|
4494
4522
|
|
4495
4523
|
get clusterName() {
|
@@ -4581,19 +4609,19 @@ class IOContext {
|
|
4581
4609
|
}
|
4582
4610
|
|
4583
4611
|
supersede(obj) {
|
4584
|
-
//
|
4612
|
+
// Log that entity `obj` is superseded, i.e., that this entity already
|
4585
4613
|
// exists in the current model, and is initialized anew from the XML of
|
4586
4614
|
// the model that is being included. The log is shown to modeler afterwards.
|
4587
4615
|
addDistinct(obj.type + UI.PREFIXER + obj.displayName, this.superseded);
|
4588
4616
|
}
|
4589
4617
|
|
4590
4618
|
rewrite(x, n1='', n2='') {
|
4591
|
-
//
|
4592
|
-
// actual name after inclusion
|
4593
|
-
// NOTE:
|
4619
|
+
// Replace entity names of variables used in expression `x` by their
|
4620
|
+
// actual name after inclusion.
|
4621
|
+
// NOTE: When strings `n1` and `n2` are passed, replace entity name `n1`
|
4594
4622
|
// by `n2` in all variables (this is not IO-related, but used when the
|
4595
|
-
// modeler renames an entity)
|
4596
|
-
// NOTE:
|
4623
|
+
// modeler renames an entity).
|
4624
|
+
// NOTE: Nothing to do if expression contains no variables.
|
4597
4625
|
if(x.text.indexOf('[') < 0) return;
|
4598
4626
|
const rcnt = this.replace_count;
|
4599
4627
|
let s = '',
|
@@ -4607,28 +4635,28 @@ class IOContext {
|
|
4607
4635
|
while(true) {
|
4608
4636
|
p = x.text.indexOf('[', q + 1);
|
4609
4637
|
if(p < 0) {
|
4610
|
-
// No more '[' => add remaining part of text, and quit
|
4638
|
+
// No more '[' => add remaining part of text, and quit.
|
4611
4639
|
s += x.text.slice(q + 1);
|
4612
4640
|
break;
|
4613
4641
|
}
|
4614
|
-
// Add part from last ']' up to new '['
|
4642
|
+
// Add part from last ']' up to new '['.
|
4615
4643
|
s += x.text.slice(q + 1, p);
|
4616
4644
|
// Find next ']'
|
4617
4645
|
q = indexOfMatchingBracket(x.text, p);
|
4618
|
-
// Get the bracketed text (without brackets)
|
4646
|
+
// Get the bracketed text (without brackets).
|
4619
4647
|
ss = x.text.slice(p + 1, q);
|
4620
|
-
// Separate into variable and attribute + offset string (if any)
|
4648
|
+
// Separate into variable and attribute + offset string (if any).
|
4621
4649
|
vb = ss.lastIndexOf('|');
|
4622
4650
|
if(vb >= 0) {
|
4623
4651
|
v = ss.slice(0, vb);
|
4624
|
-
// NOTE:
|
4652
|
+
// NOTE: Attribute string includes the vertical bar '|'.
|
4625
4653
|
a = ss.slice(vb);
|
4626
4654
|
} else {
|
4627
|
-
// Separate into variable and offset string (if any)
|
4655
|
+
// Separate into variable and offset string (if any).
|
4628
4656
|
vb = ss.lastIndexOf('@');
|
4629
4657
|
if(vb >= 0) {
|
4630
4658
|
v = ss.slice(0, vb);
|
4631
|
-
// NOTE:
|
4659
|
+
// NOTE: Attribute string includes the "at" sign '@'.
|
4632
4660
|
a = ss.slice(vb);
|
4633
4661
|
} else {
|
4634
4662
|
v = ss;
|
@@ -4650,12 +4678,13 @@ class IOContext {
|
|
4650
4678
|
brace = '';
|
4651
4679
|
}
|
4652
4680
|
}
|
4653
|
-
// NOTE:
|
4681
|
+
// NOTE: Patterns used to compute statistics must not be rewritten.
|
4654
4682
|
let doit = true;
|
4655
4683
|
stat = v.split('$');
|
4656
4684
|
if(stat.length > 1 && VM.statistic_operators.indexOf(stat[0]) >= 0) {
|
4657
4685
|
if(brace) {
|
4658
|
-
// NOTE:
|
4686
|
+
// NOTE: This does not hold for statistics for experiment outcomes,
|
4687
|
+
// because there no patterns but actual names are used.
|
4659
4688
|
brace += stat[0] + '$';
|
4660
4689
|
v = stat.slice(1).join('$');
|
4661
4690
|
} else {
|
@@ -4663,21 +4692,21 @@ class IOContext {
|
|
4663
4692
|
}
|
4664
4693
|
}
|
4665
4694
|
if(doit) {
|
4666
|
-
// NOTE:
|
4667
|
-
// and if matching, replace it by `n2
|
4695
|
+
// NOTE: When `n1` and `n2` have been specified, compare `v` with `n1`,
|
4696
|
+
// and if matching, replace it by `n2`.
|
4668
4697
|
if(n1 && n2) {
|
4669
4698
|
// NOTE: UI.replaceEntity handles link names by replacing either the
|
4670
|
-
// FROM or TO node name if it matches with `n1
|
4699
|
+
// FROM or TO node name if it matches with `n1`.
|
4671
4700
|
const r = UI.replaceEntity(v, n1, n2);
|
4672
|
-
// Only replace `v` by `r` in case of a match
|
4701
|
+
// Only replace `v` by `r` in case of a match.
|
4673
4702
|
if(r) {
|
4674
4703
|
this.replace_count++;
|
4675
4704
|
v = r;
|
4676
4705
|
}
|
4677
4706
|
} else {
|
4678
4707
|
// When `n1` and `n2` are NOT specified, rewrite the variable
|
4679
|
-
// using the parameter bindings
|
4680
|
-
// NOTE:
|
4708
|
+
// using the parameter bindings.
|
4709
|
+
// NOTE: Link variables contain TWO entity names.
|
4681
4710
|
if(v.indexOf(UI.LINK_ARROW) >= 0) {
|
4682
4711
|
const ln = v.split(UI.LINK_ARROW);
|
4683
4712
|
v = this.actualName(ln[0]) + UI.LINK_ARROW + this.actualName(ln[1]);
|
@@ -4686,24 +4715,24 @@ class IOContext {
|
|
4686
4715
|
}
|
4687
4716
|
}
|
4688
4717
|
}
|
4689
|
-
// Add [actual name|attribute string] while preserving "by reference"
|
4718
|
+
// Add [actual name|attribute string] while preserving "by reference".
|
4690
4719
|
s += `[${brace}${by_ref}${v}${a}]`;
|
4691
4720
|
}
|
4692
|
-
// Increase expression count when 1 or more variables were replaced
|
4721
|
+
// Increase expression count when 1 or more variables were replaced.
|
4693
4722
|
if(this.replace_count > rcnt) this.expression_count++;
|
4694
|
-
// Replace the original expression by the new one
|
4723
|
+
// Replace the original expression by the new one.
|
4695
4724
|
x.text = s;
|
4696
|
-
// Force expression to recompile
|
4725
|
+
// Force expression to recompile.
|
4697
4726
|
x.code = null;
|
4698
4727
|
}
|
4699
4728
|
|
4700
4729
|
addedNode(node) {
|
4701
|
-
// Record that node was added
|
4730
|
+
// Record that node was added.
|
4702
4731
|
this.added_nodes.push(node);
|
4703
4732
|
}
|
4704
4733
|
|
4705
4734
|
addedLink(link) {
|
4706
|
-
// Record that link was added
|
4735
|
+
// Record that link was added.
|
4707
4736
|
this.added_links.push(link);
|
4708
4737
|
}
|
4709
4738
|
|
@@ -9196,12 +9225,17 @@ class Dataset {
|
|
9196
9225
|
// Data is stored simply as semicolon-separated floating point numbers,
|
9197
9226
|
// with N-digit precision to keep model files compact (default: N = 8).
|
9198
9227
|
let d = [];
|
9199
|
-
|
9200
|
-
|
9201
|
-
|
9202
|
-
|
9203
|
-
|
9204
|
-
|
9228
|
+
// NOTE: Guard against empty strings and other invalid data.
|
9229
|
+
for(const v of this.data) if(v) {
|
9230
|
+
try {
|
9231
|
+
// Convert number to string with the desired precision.
|
9232
|
+
const f = v.toPrecision(CONFIGURATION.dataset_precision);
|
9233
|
+
// Then parse it again, so that the number will be represented
|
9234
|
+
// (by JavaScript) in the most compact representation.
|
9235
|
+
d.push(parseFloat(f));
|
9236
|
+
} catch(err) {
|
9237
|
+
console.log('-- Notice: dataset', this.displayName, 'has invalid data', v);
|
9238
|
+
}
|
9205
9239
|
}
|
9206
9240
|
return d.join(';');
|
9207
9241
|
}
|
@@ -303,11 +303,21 @@ function markFirstDifference(s1, s2) {
|
|
303
303
|
//
|
304
304
|
|
305
305
|
function ciCompare(a, b) {
|
306
|
-
//
|
307
|
-
// between accented characters (as this differentiates between identifiers)
|
306
|
+
// Perform case-insensitive comparison that does differentiate
|
307
|
+
// between accented characters (as this differentiates between identifiers).
|
308
308
|
return a.localeCompare(b, undefined, {sensitivity: 'accent'});
|
309
309
|
}
|
310
310
|
|
311
|
+
function compareTailFirst(a, b, tail) {
|
312
|
+
// Sort strings while prioritizing the group of elements that end on `tail`.
|
313
|
+
const
|
314
|
+
a_tail = a.endsWith(tail),
|
315
|
+
b_tail = b.endsWith(tail);
|
316
|
+
if(a_tail && !b_tail) return -1;
|
317
|
+
if(!a_tail && b_tail) return 1;
|
318
|
+
return ciCompare(a, b);
|
319
|
+
}
|
320
|
+
|
311
321
|
function endsWithDigits(str) {
|
312
322
|
// Returns trailing digts of `str` (empty string will evaluate as FALSE)
|
313
323
|
let i = str.length - 1,
|
@@ -1143,6 +1153,7 @@ if(NODE) module.exports = {
|
|
1143
1153
|
differences: differences,
|
1144
1154
|
markFirstDifference: markFirstDifference,
|
1145
1155
|
ciCompare: ciCompare,
|
1156
|
+
compareTailFirst: compareTailFirst,
|
1146
1157
|
endsWithDigits: endsWithDigits,
|
1147
1158
|
indexOfMatchingBracket: indexOfMatchingBracket,
|
1148
1159
|
monoSpaced: monoSpaced,
|
@@ -951,7 +951,13 @@ class ExpressionParser {
|
|
951
951
|
// Variable name may start with a colon to denote that the owner
|
952
952
|
// prefix should be added.
|
953
953
|
name = UI.colonPrefixedName(name, this.owner_prefix);
|
954
|
-
|
954
|
+
// First check whether name refers to a valid attribute of an
|
955
|
+
// existing model entity.
|
956
|
+
const check = MODEL.validVariable(name);
|
957
|
+
if(check !== true) {
|
958
|
+
// If not TRUE, check will be an error message.
|
959
|
+
msg = check;
|
960
|
+
} else if(x.x) {
|
955
961
|
// Look up name in experiment outcomes list.
|
956
962
|
x.v = x.x.resultIndex(name);
|
957
963
|
if(x.v < 0 && name.indexOf('#') >= 0 &&
|
@@ -989,7 +995,26 @@ class ExpressionParser {
|
|
989
995
|
if(x.r === false && x.t === false) {
|
990
996
|
msg = 'Experiment run not specified';
|
991
997
|
} else if(x.v === false) {
|
992
|
-
|
998
|
+
// NOTE: Variable may not be defined as outcome of any experiment.
|
999
|
+
// This will be handled at runtime by VMI_push_run_result, but
|
1000
|
+
// it will be helpful to notify modelers at compile time when an
|
1001
|
+
// experiment is running, and also when they are editing an
|
1002
|
+
// expression (so when a modal dialog is showing).
|
1003
|
+
const
|
1004
|
+
notice = `No experiments have variable "${name}" as result`,
|
1005
|
+
tm = UI.topModal;
|
1006
|
+
// NOTE: Only notify when expression-editing modals are showing.
|
1007
|
+
if(tm) {
|
1008
|
+
const mid = tm.id.replace('-modal', '');
|
1009
|
+
if(['actor', 'note', 'link', 'boundline-data', 'process',
|
1010
|
+
'product', 'equation', 'expression'].indexOf(mid) >= 0) {
|
1011
|
+
UI.notify(notice);
|
1012
|
+
}
|
1013
|
+
}
|
1014
|
+
if(MODEL.running_experiment) {
|
1015
|
+
// Log message only for block 1.
|
1016
|
+
VM.logMessage(1, VM.WARNING + notice);
|
1017
|
+
}
|
993
1018
|
}
|
994
1019
|
}
|
995
1020
|
if(msg) {
|
@@ -2311,6 +2336,17 @@ class VirtualMachine {
|
|
2311
2336
|
P: 'process',
|
2312
2337
|
Q: 'product'
|
2313
2338
|
};
|
2339
|
+
// Reverse lookup for entity letter codes.
|
2340
|
+
this.entity_letter_codes = {
|
2341
|
+
actor: 'A',
|
2342
|
+
constraint: 'B',
|
2343
|
+
cluster: 'C',
|
2344
|
+
dataset: 'D',
|
2345
|
+
equation: 'E',
|
2346
|
+
link: 'L',
|
2347
|
+
process: 'P',
|
2348
|
+
product: 'Q'
|
2349
|
+
};
|
2314
2350
|
this.entity_letters = 'ABCDELPQ';
|
2315
2351
|
// Standard attributes of Linny-R entities.
|
2316
2352
|
this.attribute_names = {
|
@@ -7168,7 +7204,13 @@ function VMI_push_run_result(x, args) {
|
|
7168
7204
|
let xp = rrspec.x,
|
7169
7205
|
rn = rrspec.r,
|
7170
7206
|
rri = rrspec.v;
|
7171
|
-
if(xp === false)
|
7207
|
+
if(xp === false) {
|
7208
|
+
// If no experiment is specified, use the running experiment.
|
7209
|
+
// NOTE: To facilitate testing a "single run" without using the
|
7210
|
+
// Experiment manager, default to the experiment that is selected
|
7211
|
+
// in the Experiment manager (but not "running").
|
7212
|
+
xp = MODEL.running_experiment || EXPERIMENT_MANAGER.selected_experiment;
|
7213
|
+
}
|
7172
7214
|
if(xp instanceof Experiment) {
|
7173
7215
|
if(Array.isArray(rn)) {
|
7174
7216
|
// Let the running experiment infer run number from selector list `rn`
|
@@ -7176,7 +7218,22 @@ function VMI_push_run_result(x, args) {
|
|
7176
7218
|
rn = xp.matchingCombinationIndex(rn);
|
7177
7219
|
} else if(rn < 0) {
|
7178
7220
|
// Relative run number: use current run # + r (first run has number 0).
|
7179
|
-
|
7221
|
+
if(xp === MODEL.running_experiment) {
|
7222
|
+
rn += xp.active_combination_index;
|
7223
|
+
} else if(xp.chart_combinations.length) {
|
7224
|
+
// Modeler has selected one or more runs in the viewer table.
|
7225
|
+
// FInd the highest number of a selected run that has been performed.
|
7226
|
+
let last = -1;
|
7227
|
+
for(const ccn of xp.chart_combinations) {
|
7228
|
+
if(ccn > last && ccn < xp.runs.length) last = ccn;
|
7229
|
+
}
|
7230
|
+
// If no performed runs are selected, use the last performed run.
|
7231
|
+
if(last < 0) last = xp.runs.length - 1;
|
7232
|
+
rn += last;
|
7233
|
+
} else {
|
7234
|
+
// Choose the run relative to the total number of completed runs.
|
7235
|
+
rn += xp.runs.length - 1;
|
7236
|
+
}
|
7180
7237
|
} else if(rrspec.nr !== false) {
|
7181
7238
|
// Run number inferred from local time step of expression.
|
7182
7239
|
const
|