linny-r 2.0.9 → 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/package.json +6 -2
- 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 +79 -0
- package/static/linny-r.css +240 -7
- package/static/scripts/linny-r-ctrl.js +76 -13
- package/static/scripts/linny-r-gui-chart-manager.js +13 -12
- package/static/scripts/linny-r-gui-constraint-editor.js +4 -4
- package/static/scripts/linny-r-gui-controller.js +390 -30
- package/static/scripts/linny-r-gui-dataset-manager.js +48 -31
- package/static/scripts/linny-r-gui-experiment-manager.js +22 -12
- package/static/scripts/linny-r-gui-expression-editor.js +26 -12
- package/static/scripts/linny-r-gui-finder.js +190 -5
- package/static/scripts/linny-r-model.js +112 -38
- package/static/scripts/linny-r-vm.js +99 -93
@@ -32,7 +32,7 @@ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
32
32
|
SOFTWARE.
|
33
33
|
*/
|
34
34
|
|
35
|
-
// CLASS GUIDatasetManager provides the dataset dialog functionality
|
35
|
+
// CLASS GUIDatasetManager provides the dataset dialog functionality.
|
36
36
|
class GUIDatasetManager extends DatasetManager {
|
37
37
|
constructor() {
|
38
38
|
super();
|
@@ -44,7 +44,7 @@ class GUIDatasetManager extends DatasetManager {
|
|
44
44
|
'click', (event) => UI.toggleDialog(event));
|
45
45
|
document.getElementById('ds-new-btn').addEventListener(
|
46
46
|
// Shift-click on New button => add prefix of selected dataset
|
47
|
-
// (if any) to the name field of the dialog
|
47
|
+
// (if any) to the name field of the dialog.
|
48
48
|
'click', () => DATASET_MANAGER.promptForDataset(event.shiftKey));
|
49
49
|
document.getElementById('ds-data-btn').addEventListener(
|
50
50
|
'click', () => DATASET_MANAGER.editData());
|
@@ -58,14 +58,14 @@ class GUIDatasetManager extends DatasetManager {
|
|
58
58
|
'click', () => DATASET_MANAGER.deleteDataset());
|
59
59
|
document.getElementById('ds-filter-btn').addEventListener(
|
60
60
|
'click', () => DATASET_MANAGER.toggleFilter());
|
61
|
-
// Update when filter input text changes
|
61
|
+
// Update when filter input text changes.
|
62
62
|
this.filter_text = document.getElementById('ds-filter-text');
|
63
63
|
this.filter_text.addEventListener(
|
64
64
|
'input', () => DATASET_MANAGER.changeFilter());
|
65
65
|
this.dataset_table = document.getElementById('dataset-table');
|
66
|
-
// Data properties pane
|
66
|
+
// Data properties pane.
|
67
67
|
this.properties = document.getElementById('dataset-properties');
|
68
|
-
// Toggle buttons at bottom of dialog
|
68
|
+
// Toggle buttons at bottom of dialog.
|
69
69
|
this.blackbox = document.getElementById('dataset-blackbox');
|
70
70
|
this.blackbox.addEventListener(
|
71
71
|
'click', () => DATASET_MANAGER.toggleBlackBox());
|
@@ -75,7 +75,7 @@ class GUIDatasetManager extends DatasetManager {
|
|
75
75
|
this.io_box = document.getElementById('dataset-io');
|
76
76
|
this.io_box.addEventListener(
|
77
77
|
'click', () => DATASET_MANAGER.toggleImportExport());
|
78
|
-
// Modifier pane buttons
|
78
|
+
// Modifier pane buttons.
|
79
79
|
document.getElementById('ds-add-modif-btn').addEventListener(
|
80
80
|
'click', () => DATASET_MANAGER.promptForSelector('new'));
|
81
81
|
document.getElementById('ds-rename-modif-btn').addEventListener(
|
@@ -86,9 +86,9 @@ class GUIDatasetManager extends DatasetManager {
|
|
86
86
|
'click', () => DATASET_MANAGER.deleteModifier());
|
87
87
|
document.getElementById('ds-convert-modif-btn').addEventListener(
|
88
88
|
'click', () => DATASET_MANAGER.promptToConvertModifiers());
|
89
|
-
// Modifier table
|
89
|
+
// Modifier table.
|
90
90
|
this.modifier_table = document.getElementById('dataset-modif-table');
|
91
|
-
// Modal dialogs
|
91
|
+
// Modal dialogs.
|
92
92
|
this.new_modal = new ModalDialog('new-dataset');
|
93
93
|
this.new_modal.ok.addEventListener(
|
94
94
|
'click', () => DATASET_MANAGER.newDataset());
|
@@ -170,7 +170,8 @@ class GUIDatasetManager extends DatasetManager {
|
|
170
170
|
}
|
171
171
|
|
172
172
|
enterKey() {
|
173
|
-
// Open "edit" dialog for the selected dataset or modifier expression
|
173
|
+
// Open "edit" dialog for the selected dataset or modifier expression.
|
174
|
+
if(!this.focal_table) this.focal_table = this.dataset_table;
|
174
175
|
const srl = this.focal_table.getElementsByClassName('sel-set');
|
175
176
|
if(srl.length > 0) {
|
176
177
|
const r = this.focal_table.rows[srl[0].rowIndex];
|
@@ -190,7 +191,8 @@ class GUIDatasetManager extends DatasetManager {
|
|
190
191
|
}
|
191
192
|
|
192
193
|
upDownKey(dir) {
|
193
|
-
// Select row above or below the selected one (if possible)
|
194
|
+
// Select row above or below the selected one (if possible).
|
195
|
+
if(!this.focal_table) this.focal_table = this.dataset_table;
|
194
196
|
const srl = this.focal_table.getElementsByClassName('sel-set');
|
195
197
|
if(srl.length > 0) {
|
196
198
|
let r = this.focal_table.rows[srl[0].rowIndex + dir];
|
@@ -206,11 +208,24 @@ class GUIDatasetManager extends DatasetManager {
|
|
206
208
|
}
|
207
209
|
}
|
208
210
|
|
211
|
+
expandToShow(name) {
|
212
|
+
// Expand all prefix rows for dataset having `name` so as to make it visible.
|
213
|
+
const pn = UI.prefixesAndName(name);
|
214
|
+
if(pn.length > 1) {
|
215
|
+
pn.pop();
|
216
|
+
while(pn.length) {
|
217
|
+
addDistinct(pn.join(UI.PREFIXER).toLowerCase(), this.expanded_rows);
|
218
|
+
pn.pop();
|
219
|
+
}
|
220
|
+
this.updateDialog();
|
221
|
+
}
|
222
|
+
}
|
223
|
+
|
209
224
|
hideCollapsedRows() {
|
210
|
-
//
|
225
|
+
// Hide all rows except top level and immediate children of expanded.
|
211
226
|
for(const row of this.dataset_table.rows) {
|
212
227
|
const
|
213
|
-
// Get the first DIV in the first TD of this row
|
228
|
+
// Get the first DIV in the first TD of this row.
|
214
229
|
first_div = row.firstChild.firstElementChild,
|
215
230
|
btn = first_div.dataset.prefix === 'x';
|
216
231
|
let p = row.dataset.prefix,
|
@@ -218,18 +233,18 @@ class GUIDatasetManager extends DatasetManager {
|
|
218
233
|
show = !p || x;
|
219
234
|
if(btn) {
|
220
235
|
const btn_div = row.getElementsByClassName('tree-btn')[0];
|
221
|
-
// Special expand/collapse row
|
236
|
+
// Special expand/collapse row.
|
222
237
|
if(show) {
|
223
|
-
// Set triangle to point down
|
238
|
+
// Set triangle to point down.
|
224
239
|
btn_div.innerText = '\u25BC';
|
225
240
|
} else {
|
226
|
-
// Set triangle to point right
|
241
|
+
// Set triangle to point right.
|
227
242
|
btn_div.innerText = '\u25BA';
|
228
|
-
// See whether "parent prefix" is expanded
|
243
|
+
// See whether "parent prefix" is expanded.
|
229
244
|
p = p.split(UI.PREFIXER);
|
230
245
|
p.pop();
|
231
246
|
p = p.join(UI.PREFIXER);
|
232
|
-
// If so, then also show the row
|
247
|
+
// If so, then also show the row.
|
233
248
|
show = (!p || this.expanded_rows.indexOf(p) >= 0);
|
234
249
|
}
|
235
250
|
}
|
@@ -238,7 +253,7 @@ class GUIDatasetManager extends DatasetManager {
|
|
238
253
|
}
|
239
254
|
|
240
255
|
togglePrefixRow(e) {
|
241
|
-
//
|
256
|
+
// Show list items of the next prefix level.
|
242
257
|
let r = e.target;
|
243
258
|
while(r.tagName !== 'TR') r = r.parentNode;
|
244
259
|
const
|
@@ -246,7 +261,7 @@ class GUIDatasetManager extends DatasetManager {
|
|
246
261
|
i = this.expanded_rows.indexOf(p);
|
247
262
|
if(i >= 0) {
|
248
263
|
this.expanded_rows.splice(i, 1);
|
249
|
-
// Also remove all prefixes that have `p` as prefix
|
264
|
+
// Also remove all prefixes that have `p` as prefix.
|
250
265
|
for(let j = this.expanded_rows.length - 1; j >= 0; j--) {
|
251
266
|
if(this.expanded_rows[j].startsWith(p + UI.PREFIXER)) {
|
252
267
|
this.expanded_rows.splice(j, 1);
|
@@ -259,7 +274,7 @@ class GUIDatasetManager extends DatasetManager {
|
|
259
274
|
}
|
260
275
|
|
261
276
|
rowByPrefix(prefix) {
|
262
|
-
//
|
277
|
+
// Return the first table row with the specified prefix.
|
263
278
|
if(!prefix) return null;
|
264
279
|
let lcp = prefix.toLowerCase(),
|
265
280
|
pl = lcp.split(': ');
|
@@ -278,12 +293,12 @@ class GUIDatasetManager extends DatasetManager {
|
|
278
293
|
}
|
279
294
|
|
280
295
|
selectPrefixRow(e) {
|
281
|
-
//
|
296
|
+
// Select expand/collapse prefix row.
|
282
297
|
this.focal_table = this.dataset_table;
|
283
|
-
// NOTE: `e` can also be a string specifying the prefix to select
|
298
|
+
// NOTE: `e` can also be a string specifying the prefix to select.
|
284
299
|
let r = e.target || this.rowByPrefix(e);
|
285
300
|
if(!r) return;
|
286
|
-
// Modeler may have clicked on the expand/collapse triangle
|
301
|
+
// Modeler may have clicked on the expand/collapse triangle.
|
287
302
|
const toggle = r.classList.contains('tree-btn');
|
288
303
|
while(r.tagName !== 'TR') r = r.parentNode;
|
289
304
|
this.selected_prefix_row = r;
|
@@ -1047,7 +1062,7 @@ class GUIDatasetManager extends DatasetManager {
|
|
1047
1062
|
}
|
1048
1063
|
|
1049
1064
|
editData() {
|
1050
|
-
// Show the Edit time series dialog
|
1065
|
+
// Show the Edit time series dialog.
|
1051
1066
|
const
|
1052
1067
|
ds = this.selected_dataset,
|
1053
1068
|
md = this.series_modal,
|
@@ -1057,7 +1072,7 @@ class GUIDatasetManager extends DatasetManager {
|
|
1057
1072
|
md.element('unit').value = ds.scale_unit;
|
1058
1073
|
cover.style.display = (ds.array ? 'block' : 'none');
|
1059
1074
|
md.element('time-scale').value = VM.sig4Dig(ds.time_scale);
|
1060
|
-
// Add options for time unit selector
|
1075
|
+
// Add options for time unit selector.
|
1061
1076
|
const ol = [];
|
1062
1077
|
for(let u in VM.time_unit_shorthand) {
|
1063
1078
|
if(VM.time_unit_shorthand.hasOwnProperty(u)) {
|
@@ -1067,7 +1082,7 @@ class GUIDatasetManager extends DatasetManager {
|
|
1067
1082
|
}
|
1068
1083
|
}
|
1069
1084
|
md.element('time-unit').innerHTML = ol.join('');
|
1070
|
-
// Add options for(dis)aggregation method selector
|
1085
|
+
// Add options for(dis)aggregation method selector.
|
1071
1086
|
ol.length = 0;
|
1072
1087
|
for(let i = 0; i < this.methods.length; i++) {
|
1073
1088
|
ol.push(['<option value="', this.methods[i],
|
@@ -1075,12 +1090,12 @@ class GUIDatasetManager extends DatasetManager {
|
|
1075
1090
|
'">', this.method_names[i], '</option>'].join(''));
|
1076
1091
|
}
|
1077
1092
|
md.element('method').innerHTML = ol.join('');
|
1078
|
-
// Update the "periodic" box
|
1093
|
+
// Update the "periodic" box.
|
1079
1094
|
UI.setBox('series-periodic', ds.periodic);
|
1080
|
-
// Update the "array" box
|
1095
|
+
// Update the "array" box.
|
1081
1096
|
UI.setBox('series-array', ds.array);
|
1082
1097
|
md.element('url').value = ds.url;
|
1083
|
-
// Show data as decimal numbers (JS default notation) on separate lines
|
1098
|
+
// Show data as decimal numbers (JS default notation) on separate lines.
|
1084
1099
|
this.series_data.value = ds.data.join('\n');
|
1085
1100
|
md.show('default');
|
1086
1101
|
}
|
@@ -1203,7 +1218,8 @@ class GUIDatasetManager extends DatasetManager {
|
|
1203
1218
|
UI.warn(`Invalid default value "${v}" in column ${i}`);
|
1204
1219
|
return false;
|
1205
1220
|
} else {
|
1206
|
-
|
1221
|
+
// Push empty list, as this will become the actual dataset without default value.
|
1222
|
+
dsa.push([]);
|
1207
1223
|
}
|
1208
1224
|
}
|
1209
1225
|
for(let i = 2; i < n; i++) {
|
@@ -1212,7 +1228,7 @@ class GUIDatasetManager extends DatasetManager {
|
|
1212
1228
|
UI.warn(`Number of values (${dsv.length}) on line ${i} does not match number of dataset names (${ncol})`);
|
1213
1229
|
return false;
|
1214
1230
|
}
|
1215
|
-
for(let j = 0; j <
|
1231
|
+
for(let j = 0; j < ncol; j++) {
|
1216
1232
|
const
|
1217
1233
|
v = dsv[j].trim(),
|
1218
1234
|
sf = safeStrToFloat(v, '');
|
@@ -1220,6 +1236,7 @@ class GUIDatasetManager extends DatasetManager {
|
|
1220
1236
|
UI.warn(`Invalid numerical value "${v}" for <strong>${dsn[j]}</strong> on line ${i}`);
|
1221
1237
|
return false;
|
1222
1238
|
}
|
1239
|
+
dsa[j].push(sf);
|
1223
1240
|
}
|
1224
1241
|
}
|
1225
1242
|
// Add or update datasets.
|
@@ -441,19 +441,25 @@ class GUIExperimentManager extends ExperimentManager {
|
|
441
441
|
}
|
442
442
|
|
443
443
|
newExperiment() {
|
444
|
-
|
445
|
-
|
446
|
-
|
447
|
-
|
448
|
-
|
449
|
-
|
444
|
+
// NOTE: Title must be a "clean" name: no \ or | and spacing reduced to
|
445
|
+
// a single space to permit using it unambiguously in experiment result
|
446
|
+
// specifiers of variable names.
|
447
|
+
const n = UI.cleanName(this.new_modal.element('name').value);
|
448
|
+
if(n) {
|
449
|
+
const x = MODEL.addExperiment(n);
|
450
|
+
if(x) {
|
451
|
+
this.new_modal.hide();
|
452
|
+
this.selected_experiment = x;
|
453
|
+
this.updateDialog();
|
454
|
+
}
|
455
|
+
} else {
|
456
|
+
this.new_modal.element('name').focus();
|
457
|
+
return;
|
450
458
|
}
|
451
459
|
}
|
452
460
|
|
453
461
|
promptForName() {
|
454
462
|
if(this.selected_experiment) {
|
455
|
-
this.rename_modal.element('former-name').innerHTML =
|
456
|
-
this.selected_experiment.title;
|
457
463
|
this.rename_modal.element('name').value = '';
|
458
464
|
this.rename_modal.show('name');
|
459
465
|
}
|
@@ -464,12 +470,16 @@ class GUIExperimentManager extends ExperimentManager {
|
|
464
470
|
const
|
465
471
|
nel = this.rename_modal.element('name'),
|
466
472
|
n = UI.cleanName(nel.value);
|
467
|
-
// Show modeler the "cleaned" new name
|
473
|
+
// Show modeler the "cleaned" new name.
|
468
474
|
nel.value = n;
|
469
|
-
// Keep prompt open if title is empty string
|
475
|
+
// Keep prompt open if cleaned title is empty string, or identifies
|
476
|
+
// an existing experiment.
|
477
|
+
nel.focus();
|
470
478
|
if(n) {
|
471
|
-
// Warn modeler if name already in use for some experiment
|
472
|
-
|
479
|
+
// Warn modeler if name already in use for some experiment, other than
|
480
|
+
// the selected experiment (as upper/lower case changes must be possible).
|
481
|
+
if(MODEL.indexOfExperiment(n) >= 0 &&
|
482
|
+
n.toLowerCase() !== this.selected_experiment.title.toLowerCase()) {
|
473
483
|
UI.warn(`An experiment with title "${n}" already exists`);
|
474
484
|
} else {
|
475
485
|
this.selected_experiment.title = n;
|
@@ -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)
|
@@ -256,8 +256,8 @@ class Finder {
|
|
256
256
|
}
|
257
257
|
}
|
258
258
|
// Also allow search for scale unit names.
|
259
|
-
if(et
|
260
|
-
imgs
|
259
|
+
if(et === 'U') {
|
260
|
+
imgs = '<img src="images/scale.png">';
|
261
261
|
for(let k in MODEL.products) if(MODEL.products.hasOwnProperty(k)) {
|
262
262
|
if(fp && !k.startsWith(UI.BLACK_BOX) && patternMatch(
|
263
263
|
MODEL.products[k].scale_unit, this.filter_pattern)) {
|
@@ -266,6 +266,37 @@ class Finder {
|
|
266
266
|
addDistinct('Q', this.filtered_types);
|
267
267
|
}
|
268
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
|
+
}
|
269
300
|
}
|
270
301
|
// Also allow search for link multiplier symbols.
|
271
302
|
if(et.indexOf('M') >= 0) {
|
@@ -341,11 +372,11 @@ class Finder {
|
|
341
372
|
|
342
373
|
get entityGroup() {
|
343
374
|
// Returns the list of filtered entities if all are of the same type,
|
344
|
-
// while excluding (no actor), (top cluster),
|
375
|
+
// while excluding (no actor), (top cluster), and equations.
|
345
376
|
const
|
346
377
|
eg = [],
|
347
378
|
ft = this.filtered_types[0];
|
348
|
-
if(this.filtered_types.length === 1 &&
|
379
|
+
if(this.filtered_types.length === 1 && ft !== 'E') {
|
349
380
|
for(const e of this.entities) {
|
350
381
|
// Exclude "no actor" and top cluster.
|
351
382
|
if(e.name && e.name !== '(no_actor)' && e.name !== '(top_cluster)' &&
|
@@ -601,7 +632,7 @@ class Finder {
|
|
601
632
|
// look only for the entity types denoted by these letters.
|
602
633
|
let ft = this.filter_input.value,
|
603
634
|
et = VM.entity_letters;
|
604
|
-
if(/^(\*|U|M|[ABCDELPQ]+)\?/i.test(ft)) {
|
635
|
+
if(/^(\*|U|M|S|[ABCDELPQ]+)\?/i.test(ft)) {
|
605
636
|
ft = ft.split('?');
|
606
637
|
// NOTE: *? denotes "all entity types except constraints".
|
607
638
|
et = (ft[0] === '*' ? 'ACDELPQ' : ft[0].toUpperCase());
|
@@ -644,6 +675,7 @@ class Finder {
|
|
644
675
|
if(UI.hidden('dataset-dlg')) {
|
645
676
|
UI.buttons.dataset.dispatchEvent(new Event('click'));
|
646
677
|
}
|
678
|
+
DATASET_MANAGER.expandToShow(obj.name);
|
647
679
|
DATASET_MANAGER.selected_dataset = obj;
|
648
680
|
DATASET_MANAGER.updateDialog();
|
649
681
|
} else if(obj instanceof DatasetModifier) {
|
@@ -777,9 +809,162 @@ class Finder {
|
|
777
809
|
UI.showLinkPropertiesDialog(e, 'R', false, group);
|
778
810
|
} else if(e instanceof Cluster) {
|
779
811
|
UI.showClusterPropertiesDialog(e, group);
|
812
|
+
} else if(e instanceof Dataset) {
|
813
|
+
this.showDatasetGroupDialog(e, group);
|
780
814
|
}
|
781
815
|
}
|
782
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
|
+
|
783
968
|
copyAttributesToClipboard(shift) {
|
784
969
|
// Copy relevant entity attributes as tab-separated text to clipboard.
|
785
970
|
// NOTE: All entity types have "get" method `attributes` that returns an
|