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.
Files changed (36) hide show
  1. package/README.md +3 -40
  2. package/package.json +6 -2
  3. package/server.js +19 -157
  4. package/static/images/solve-not-same-changed.png +0 -0
  5. package/static/images/solve-not-same-not-changed.png +0 -0
  6. package/static/images/solve-same-changed.png +0 -0
  7. package/static/images/solve-same-not-changed.png +0 -0
  8. package/static/index.html +137 -20
  9. package/static/linny-r.css +260 -23
  10. package/static/scripts/iro.min.js +7 -7
  11. package/static/scripts/linny-r-ctrl.js +126 -85
  12. package/static/scripts/linny-r-gui-actor-manager.js +23 -33
  13. package/static/scripts/linny-r-gui-chart-manager.js +56 -53
  14. package/static/scripts/linny-r-gui-constraint-editor.js +10 -14
  15. package/static/scripts/linny-r-gui-controller.js +644 -260
  16. package/static/scripts/linny-r-gui-dataset-manager.js +64 -66
  17. package/static/scripts/linny-r-gui-documentation-manager.js +11 -17
  18. package/static/scripts/linny-r-gui-equation-manager.js +22 -22
  19. package/static/scripts/linny-r-gui-experiment-manager.js +124 -141
  20. package/static/scripts/linny-r-gui-expression-editor.js +26 -12
  21. package/static/scripts/linny-r-gui-file-manager.js +42 -48
  22. package/static/scripts/linny-r-gui-finder.js +294 -55
  23. package/static/scripts/linny-r-gui-model-autosaver.js +2 -4
  24. package/static/scripts/linny-r-gui-monitor.js +35 -41
  25. package/static/scripts/linny-r-gui-paper.js +42 -70
  26. package/static/scripts/linny-r-gui-power-grid-manager.js +31 -34
  27. package/static/scripts/linny-r-gui-receiver.js +1 -2
  28. package/static/scripts/linny-r-gui-repository-browser.js +44 -46
  29. package/static/scripts/linny-r-gui-scale-unit-manager.js +32 -32
  30. package/static/scripts/linny-r-gui-sensitivity-analysis.js +61 -67
  31. package/static/scripts/linny-r-gui-undo-redo.js +94 -95
  32. package/static/scripts/linny-r-milp.js +20 -24
  33. package/static/scripts/linny-r-model.js +1932 -2274
  34. package/static/scripts/linny-r-utils.js +27 -27
  35. package/static/scripts/linny-r-vm.js +900 -998
  36. 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').innerHTML +
200
+ n = document.getElementById('link-from-name').innerText +
201
201
  UI.LINK_ARROW +
202
- document.getElementById('link-to-name').innerHTML;
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 && DATASET_MANAGER.edited_expression) {
261
- own = DATASET_MANAGER.selected_dataset;
262
- sel = DATASET_MANAGER.selected_modifier.selector;
263
- } else if(!this.edited_input_id && EQUATION_MANAGER.edited_expression) {
264
- own = MODEL.equations_dataset;
265
- sel = EQUATION_MANAGER.selected_modifier.selector;
266
- } else if(!this.edited_input_id && CONSTRAINT_EDITOR.edited_expression) {
267
- own = CONSTRAINT_EDITOR.selected;
268
- sel = CONSTRAINT_EDITOR.selected_selector;
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 letters = ['i', 'j', 'k'];
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: REPOSITORY_BROWSER.asFileName(
365
- (MODEL.name || 'no-name') + '_by_' +
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
- renderDiagramAsPNG(tight) {
408
- // When `tight` is TRUE, add no whitespace around the diagram.
409
- window.localStorage.removeItem('png-url');
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
- this.renderSVGAsPNG(UI.paper.opaqueSVG);
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.fitToSize();
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', (event) => FINDER.editAttributes());
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.indexOf('U') >= 0) {
250
- imgs += '<img src="images/scale.png">';
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), datasets and equations.
375
+ // while excluding (no actor), (top cluster), and equations.
318
376
  const
319
377
  eg = [],
320
- n = this.entities.length;
321
- if(n > 0) {
322
- const ft = this.filtered_types[0];
323
- if(this.filtered_types.length === 1 && 'DE'.indexOf(ft) < 0) {
324
- for(let i = 0; i < n; i++) {
325
- const e = this.entities[i];
326
- // Exclude "no actor" and top cluster.
327
- if(e.name && e.name !== '(no_actor)' && e.name !== '(top_cluster)' &&
328
- // Also exclude actor cash flow data products because
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 cl = se.productPositionClusters;
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 j = 0; j < MODEL.charts.length; j++) {
378
- const c = MODEL.charts[j];
379
- for(let k = 0; k < c.variables.length; k++) {
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 + j);
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(let i = 0; i < c.notes.length; i++) {
441
- const n = c.notes[i];
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(let j = 0; j < bl.selectors.length; j++) {
467
- if(re.test(bl.selectors[j].expression.text)) {
468
- xal.push('I' + (i + 1));
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
- let e = this.selected_entity;
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(let i = 0; i < this.entities.length; i++) {
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(let i = 0; i < seq.length; i++) {
985
+ for(const etl of seq) {
742
986
  const
743
- etl = seq[i],
744
- ead = ea_dict[etl];
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(let i = 0; i < ead.length; i++) {
759
- const
760
- ea = ead[i],
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();