linny-r 2.0.11 → 2.1.0

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.
@@ -441,9 +441,9 @@ class LinnyRModel {
441
441
  }
442
442
 
443
443
  namedObjectByID(id) {
444
- // NOTE: not only entities, but also equations are "named objects", meaning
444
+ // NOTE: Not only entities, but also equations are "named objects", meaning
445
445
  // that their name must be unique in a model (unlike the titles of charts
446
- // and experiments)
446
+ // and experiments).
447
447
  let obj = this.nodeBoxByID(id);
448
448
  if(obj) return obj;
449
449
  obj = this.actorByID(id);
@@ -453,6 +453,28 @@ class LinnyRModel {
453
453
  return this.equationByID(id);
454
454
  }
455
455
 
456
+ datasetKeysByPrefix(prefix) {
457
+ // Return the list of datasets having the specified prefix.
458
+ const
459
+ pid = UI.nameToID(prefix + UI.PREFIXER),
460
+ kl = [];
461
+ for(const k of Object.keys(this.datasets)) {
462
+ if(k.startsWith(pid)) kl.push(k);
463
+ }
464
+ return kl;
465
+ }
466
+
467
+ equationsByPrefix(prefix) {
468
+ // Return the list of datasets having the specified prefix.
469
+ const
470
+ pid = UI.nameToID(prefix + UI.PREFIXER),
471
+ el = [];
472
+ for(const k of Object.keys(this.equations_dataset.modifiers)) {
473
+ if(k.startsWith(pid)) el.push(this.equations_dataset.modifiers[k]);
474
+ }
475
+ return el;
476
+ }
477
+
456
478
  chartByID(id) {
457
479
  if(!id.startsWith(this.chart_id_prefix)) return null;
458
480
  const n = parseInt(endsWithDigits(id));
@@ -460,6 +482,17 @@ class LinnyRModel {
460
482
  return this.charts[n];
461
483
  }
462
484
 
485
+ chartsByPrefix(prefix) {
486
+ // Return the list of charts having a title with the specified prefix.
487
+ const
488
+ pid = prefix + UI.PREFIXER,
489
+ cl = [];
490
+ for(const c of this.charts) {
491
+ if(c.title.startsWith(pid)) cl.push(c);
492
+ }
493
+ return cl;
494
+ }
495
+
463
496
  objectByID(id) {
464
497
  let obj = this.namedObjectByID(id);
465
498
  if(obj) return obj;
@@ -526,15 +559,16 @@ class LinnyRModel {
526
559
  }
527
560
 
528
561
  setByType(type) {
529
- // Return a "dictionary" object with entities of the specified types
530
- if(type === 'Process') return this.processes;
531
- if(type === 'Product') return this.products;
532
- if(type === 'Cluster') return this.clusters;
533
- // NOTE: the returned "dictionary" also contains the equations dataset
534
- if(type === 'Dataset') return this.datasets;
535
- if(type === 'Link') return this.links;
536
- if(type === 'Constraint') return this.constraints;
537
- if(type === 'Actor') return this.actors;
562
+ // Return a "dictionary" object with entities of the specified types.
563
+ type = type.toLowerCase();
564
+ if(type === 'process') return this.processes;
565
+ if(type === 'product') return this.products;
566
+ if(type === 'cluster') return this.clusters;
567
+ // NOTE: The returned "dictionary" also contains the equations dataset.
568
+ if(type === 'dataset') return this.datasets;
569
+ if(type === 'link') return this.links;
570
+ if(type === 'constraint') return this.constraints;
571
+ if(type === 'actor') return this.actors;
538
572
  return {};
539
573
  }
540
574
 
@@ -563,6 +597,24 @@ class LinnyRModel {
563
597
  return list;
564
598
  }
565
599
 
600
+ get includedModules() {
601
+ // Returns a look-up object {name: count} for modules that
602
+ // have been included.
603
+ const im = {};
604
+ for(const k of Object.keys(this.clusters)) {
605
+ const c = this.clusters[k];
606
+ if(c.module) {
607
+ const n = c.module.name;
608
+ if(im.hasOwnProperty(n)) {
609
+ im[n].push(c);
610
+ } else {
611
+ im[n] = [c];
612
+ }
613
+ }
614
+ }
615
+ return im;
616
+ }
617
+
566
618
  endsWithMethod(name) {
567
619
  // Return method (instance of DatasetModifier) if `name` ends with
568
620
  // ":(whitespace)m" for some method having selector ":m".
@@ -1583,7 +1635,7 @@ class LinnyRModel {
1583
1635
  // If chart with given title exists, do not add a new instance.
1584
1636
  const ci = this.indexOfChart(title);
1585
1637
  if(ci >= 0) return this.charts[ci];
1586
- // Otherwise, add it. NOTE: unlike datasets, charts are not "entities".
1638
+ // Otherwise, add it. NOTE: Unlike datasets, charts are not "entities".
1587
1639
  let c = new Chart();
1588
1640
  c.title = title;
1589
1641
  if(node) c.initFromXML(node);
@@ -1599,6 +1651,26 @@ class LinnyRModel {
1599
1651
  return c;
1600
1652
  }
1601
1653
 
1654
+ deleteChart(index) {
1655
+ // Delete chart from model.
1656
+ if(index < 0 || index >= this.charts.length) return false;
1657
+ const c = this.charts[index];
1658
+ // NOTE: Do not delete the default chart, but clear it instead.
1659
+ if(c.title === CHART_MANAGER.new_chart_title) {
1660
+ c.reset();
1661
+ return false;
1662
+ }
1663
+ // Remove chart from all experiments.
1664
+ for(const x of this.experiments) {
1665
+ const index = x.charts.indexOf(c);
1666
+ if(index >= 0) x.charts.splice(index, 1);
1667
+ }
1668
+ // Remove chart from model.
1669
+ this.charts.splice(index, 1);
1670
+ CHART_MANAGER.chart_index = -1;
1671
+ return true;
1672
+ }
1673
+
1602
1674
  addExperiment(title, node=null) {
1603
1675
  // If experiment with given title exists, do not add a new instance.
1604
1676
  title = title.trim();
@@ -1769,9 +1841,9 @@ class LinnyRModel {
1769
1841
 
1770
1842
  deselect(obj) {
1771
1843
  obj.selected = false;
1772
- let i = this.selection.indexOf(obj);
1773
- if(i >= 0) {
1774
- this.selection.splice(i, 1);
1844
+ const index = this.selection.indexOf(obj);
1845
+ if(index >= 0) {
1846
+ this.selection.splice(index, 1);
1775
1847
  this.selection_related_arrows.length = 0;
1776
1848
  }
1777
1849
  UI.drawObject(obj);
@@ -2386,8 +2458,8 @@ class LinnyRModel {
2386
2458
  UI.removeShape(node.shape);
2387
2459
  if(node instanceof Process) {
2388
2460
  // Remove process from the cluster containing it
2389
- const i = node.cluster.processes.indexOf(node);
2390
- if(i >= 0) node.cluster.processes.splice(i, 1);
2461
+ const index = node.cluster.processes.indexOf(node);
2462
+ if(index >= 0) node.cluster.processes.splice(index, 1);
2391
2463
  delete this.processes[node.identifier];
2392
2464
  } else {
2393
2465
  // Remove product from parameter lists.
@@ -2410,11 +2482,11 @@ class LinnyRModel {
2410
2482
  return;
2411
2483
  }
2412
2484
  // First remove link from outputs list of its FROM node.
2413
- let i = link.from_node.outputs.indexOf(link);
2414
- if(i >= 0) link.from_node.outputs.splice(i, 1);
2485
+ const oi = link.from_node.outputs.indexOf(link);
2486
+ if(oi >= 0) link.from_node.outputs.splice(oi, 1);
2415
2487
  // Also remove link from inputs list of its TO node.
2416
- i = link.to_node.inputs.indexOf(link);
2417
- if(i >= 0) link.to_node.inputs.splice(i, 1);
2488
+ const ii = link.to_node.inputs.indexOf(link);
2489
+ if(ii >= 0) link.to_node.inputs.splice(ii, 1);
2418
2490
  // Prepare for redraw
2419
2491
  link.from_node.cluster.clearAllProcesses();
2420
2492
  link.to_node.cluster.clearAllProcesses();
@@ -2467,8 +2539,8 @@ class LinnyRModel {
2467
2539
  this.deleteCluster(c.sub_clusters[i], false);
2468
2540
  }
2469
2541
  // Remove the cluster from its parent's subcluster list.
2470
- let i = c.cluster.sub_clusters.indexOf(c);
2471
- if(i >= 0) c.cluster.sub_clusters.splice(i, 1);
2542
+ const index = c.cluster.sub_clusters.indexOf(c);
2543
+ if(index >= 0) c.cluster.sub_clusters.splice(index, 1);
2472
2544
  UI.removeShape(c.shape);
2473
2545
  // Finally, remove the cluster from the model.
2474
2546
  delete this.clusters[c.identifier];
@@ -2798,146 +2870,120 @@ class LinnyRModel {
2798
2870
  } // END IF *not* including a model
2799
2871
 
2800
2872
  // Declare some local variables that will be used a lot.
2801
- let i,
2802
- c,
2803
- name,
2873
+ let name,
2804
2874
  actor,
2805
2875
  fn,
2806
2876
  tn,
2807
2877
  n = childNodeByTag(node, 'scaleunits');
2808
- // Scale units are not "entities", and can be included "as is"
2809
- if(n && n.childNodes) {
2810
- for(i = 0; i < n.childNodes.length; i++) {
2811
- c = n.childNodes[i];
2812
- if(c.nodeName === 'scaleunit') {
2813
- this.addScaleUnit(xmlDecoded(nodeContentByTag(c, 'name')),
2814
- nodeContentByTag(c, 'scalar'),
2815
- xmlDecoded(nodeContentByTag(c, 'base-unit')));
2816
- }
2878
+ // Scale units are not "entities", and can be included "as is".
2879
+ if(n) {
2880
+ for(const c of n.childNodes) if(c.nodeName === 'scaleunit') {
2881
+ this.addScaleUnit(xmlDecoded(nodeContentByTag(c, 'name')),
2882
+ nodeContentByTag(c, 'scalar'),
2883
+ xmlDecoded(nodeContentByTag(c, 'base-unit')));
2817
2884
  }
2818
2885
  }
2819
- // Power grids are not "entities", and can be included "as is"
2886
+ // Power grids are not "entities", and can be included "as is".
2820
2887
  n = childNodeByTag(node, 'powergrids');
2821
- if(n && n.childNodes) {
2822
- for(i = 0; i < n.childNodes.length; i++) {
2823
- c = n.childNodes[i];
2824
- if(c.nodeName === 'grid') {
2825
- this.addPowerGrid(nodeContentByTag(c, 'id'), c);
2826
- }
2888
+ if(n) {
2889
+ for(const c of n.childNodes) if(c.nodeName === 'grid') {
2890
+ this.addPowerGrid(nodeContentByTag(c, 'id'), c);
2827
2891
  }
2828
2892
  }
2829
- // When including a model, actors may be bound to an existing actor
2893
+ // When including a model, actors may be bound to an existing actor.
2830
2894
  n = childNodeByTag(node, 'actors');
2831
- if(n && n.childNodes) {
2832
- for(i = 0; i < n.childNodes.length; i++) {
2833
- c = n.childNodes[i];
2834
- if(c.nodeName === 'actor') {
2835
- name = xmlDecoded(nodeContentByTag(c, 'name'));
2836
- if(IO_CONTEXT) name = IO_CONTEXT.actualName(name);
2837
- this.addActor(name, c);
2838
- }
2895
+ if(n) {
2896
+ for(const c of n.childNodes) if(c.nodeName === 'actor') {
2897
+ name = xmlDecoded(nodeContentByTag(c, 'name'));
2898
+ if(IO_CONTEXT) name = IO_CONTEXT.actualName(name);
2899
+ this.addActor(name, c);
2839
2900
  }
2840
2901
  }
2841
- // When including a model, processes MUST be prefixed
2902
+ // When including a model, processes MUST be prefixed.
2842
2903
  n = childNodeByTag(node, 'processes');
2843
- if(n && n.childNodes) {
2844
- for(i = 0; i < n.childNodes.length; i++) {
2845
- c = n.childNodes[i];
2846
- if(c.nodeName === 'process') {
2847
- name = xmlDecoded(nodeContentByTag(c, 'name'));
2848
- actor = xmlDecoded(nodeContentByTag(c, 'owner'));
2849
- if(IO_CONTEXT) {
2850
- actor = IO_CONTEXT.actualName(actor);
2851
- name = IO_CONTEXT.actualName(name, actor);
2852
- }
2853
- this.addProcess(name, actor, c);
2904
+ if(n) {
2905
+ for(const c of n.childNodes) if(c.nodeName === 'process') {
2906
+ name = xmlDecoded(nodeContentByTag(c, 'name'));
2907
+ actor = xmlDecoded(nodeContentByTag(c, 'owner'));
2908
+ if(IO_CONTEXT) {
2909
+ actor = IO_CONTEXT.actualName(actor);
2910
+ name = IO_CONTEXT.actualName(name, actor);
2854
2911
  }
2912
+ this.addProcess(name, actor, c);
2855
2913
  }
2856
2914
  }
2857
- // When including a model, products may be bound to an existing product
2915
+ // When including a model, products may be bound to an existing product.
2858
2916
  n = childNodeByTag(node, 'products');
2859
- if(n && n.childNodes) {
2860
- for(i = 0; i < n.childNodes.length; i++) {
2861
- c = n.childNodes[i];
2862
- if(c.nodeName === 'product') {
2863
- name = xmlDecoded(nodeContentByTag(c, 'name'));
2864
- if(IO_CONTEXT) name = IO_CONTEXT.actualName(name);
2865
- this.addProduct(name, c);
2866
- }
2917
+ if(n) {
2918
+ for(const c of n.childNodes) if(c.nodeName === 'product') {
2919
+ name = xmlDecoded(nodeContentByTag(c, 'name'));
2920
+ if(IO_CONTEXT) name = IO_CONTEXT.actualName(name);
2921
+ this.addProduct(name, c);
2867
2922
  }
2868
2923
  }
2869
- // When including a model, link nodes may be bound to existing nodes
2924
+ // When including a model, link nodes may be bound to existing nodes.
2870
2925
  n = childNodeByTag(node, 'links');
2871
- if(n && n.childNodes) {
2872
- for(i = 0; i < n.childNodes.length; i++) {
2873
- c = n.childNodes[i];
2874
- if(c.nodeName === 'link') {
2875
- name = xmlDecoded(nodeContentByTag(c, 'from-name'));
2876
- actor = xmlDecoded(nodeContentByTag(c, 'from-owner'));
2926
+ if(n) {
2927
+ for(const c of n.childNodes) if(c.nodeName === 'link') {
2928
+ name = xmlDecoded(nodeContentByTag(c, 'from-name'));
2929
+ actor = xmlDecoded(nodeContentByTag(c, 'from-owner'));
2930
+ if(IO_CONTEXT) {
2931
+ actor = IO_CONTEXT.actualName(actor);
2932
+ name = IO_CONTEXT.actualName(name, actor);
2933
+ }
2934
+ if(actor != UI.NO_ACTOR) name += ` (${actor})`;
2935
+ fn = this.nodeBoxByID(UI.nameToID(name));
2936
+ if(fn) {
2937
+ name = xmlDecoded(nodeContentByTag(c, 'to-name'));
2938
+ actor = xmlDecoded(nodeContentByTag(c, 'to-owner'));
2877
2939
  if(IO_CONTEXT) {
2878
2940
  actor = IO_CONTEXT.actualName(actor);
2879
2941
  name = IO_CONTEXT.actualName(name, actor);
2880
2942
  }
2881
2943
  if(actor != UI.NO_ACTOR) name += ` (${actor})`;
2882
- fn = this.nodeBoxByID(UI.nameToID(name));
2883
- if(fn) {
2884
- name = xmlDecoded(nodeContentByTag(c, 'to-name'));
2885
- actor = xmlDecoded(nodeContentByTag(c, 'to-owner'));
2886
- if(IO_CONTEXT) {
2887
- actor = IO_CONTEXT.actualName(actor);
2888
- name = IO_CONTEXT.actualName(name, actor);
2889
- }
2890
- if(actor != UI.NO_ACTOR) name += ` (${actor})`;
2891
- tn = this.nodeBoxByID(UI.nameToID(name));
2892
- if(tn) this.addLink(fn, tn, c);
2893
- }
2944
+ tn = this.nodeBoxByID(UI.nameToID(name));
2945
+ if(tn) this.addLink(fn, tn, c);
2894
2946
  }
2895
2947
  }
2896
2948
  }
2897
- // When including a model, constraint nodes may be bound to existing nodes
2949
+ // When including a model, constraint nodes may be bound to existing nodes.
2898
2950
  n = childNodeByTag(node, 'constraints');
2899
- if(n && n.childNodes) {
2900
- for(i = 0; i < n.childNodes.length; i++) {
2901
- c = n.childNodes[i];
2902
- if(c.nodeName === 'constraint') {
2903
- name = xmlDecoded(nodeContentByTag(c, 'from-name'));
2904
- actor = xmlDecoded(nodeContentByTag(c, 'from-owner'));
2951
+ if(n) {
2952
+ for(const c of n.childNodes) if(c.nodeName === 'constraint') {
2953
+ name = xmlDecoded(nodeContentByTag(c, 'from-name'));
2954
+ actor = xmlDecoded(nodeContentByTag(c, 'from-owner'));
2955
+ if(IO_CONTEXT) {
2956
+ actor = IO_CONTEXT.actualName(actor);
2957
+ name = IO_CONTEXT.actualName(name, actor);
2958
+ }
2959
+ if(actor != UI.NO_ACTOR) name += ` (${actor})`;
2960
+ fn = this.nodeBoxByID(UI.nameToID(name));
2961
+ if(fn) {
2962
+ name = xmlDecoded(nodeContentByTag(c, 'to-name'));
2963
+ actor = xmlDecoded(nodeContentByTag(c, 'to-owner'));
2905
2964
  if(IO_CONTEXT) {
2906
2965
  actor = IO_CONTEXT.actualName(actor);
2907
2966
  name = IO_CONTEXT.actualName(name, actor);
2908
2967
  }
2909
2968
  if(actor != UI.NO_ACTOR) name += ` (${actor})`;
2910
- fn = this.nodeBoxByID(UI.nameToID(name));
2911
- if(fn) {
2912
- name = xmlDecoded(nodeContentByTag(c, 'to-name'));
2913
- actor = xmlDecoded(nodeContentByTag(c, 'to-owner'));
2914
- if(IO_CONTEXT) {
2915
- actor = IO_CONTEXT.actualName(actor);
2916
- name = IO_CONTEXT.actualName(name, actor);
2917
- }
2918
- if(actor != UI.NO_ACTOR) name += ` (${actor})`;
2919
- tn = this.nodeBoxByID(UI.nameToID(name));
2920
- if(tn) this.addConstraint(fn, tn, c);
2921
- }
2969
+ tn = this.nodeBoxByID(UI.nameToID(name));
2970
+ if(tn) this.addConstraint(fn, tn, c);
2922
2971
  }
2923
2972
  }
2924
2973
  }
2925
2974
  n = childNodeByTag(node, 'clusters');
2926
- if(n && n.childNodes) {
2927
- for(i = 0; i < n.childNodes.length; i++) {
2928
- c = n.childNodes[i];
2929
- if(c.nodeName === 'cluster') {
2930
- name = xmlDecoded(nodeContentByTag(c, 'name'));
2931
- actor = xmlDecoded(nodeContentByTag(c, 'owner'));
2932
- // When including a model, clusters MUST be prefixed
2933
- if(IO_CONTEXT) {
2934
- actor = IO_CONTEXT.actualName(actor);
2935
- // NOTE: actualName will rename the top cluster of an included
2936
- // model to just the prefix
2937
- name = IO_CONTEXT.actualName(name, actor);
2938
- }
2939
- this.addCluster(name, actor, c);
2975
+ if(n) {
2976
+ for(const c of n.childNodes) if(c.nodeName === 'cluster') {
2977
+ name = xmlDecoded(nodeContentByTag(c, 'name'));
2978
+ actor = xmlDecoded(nodeContentByTag(c, 'owner'));
2979
+ // When including a model, clusters MUST be prefixed
2980
+ if(IO_CONTEXT) {
2981
+ actor = IO_CONTEXT.actualName(actor);
2982
+ // NOTE: actualName will rename the top cluster of an included
2983
+ // model to just the prefix
2984
+ name = IO_CONTEXT.actualName(name, actor);
2940
2985
  }
2986
+ this.addCluster(name, actor, c);
2941
2987
  }
2942
2988
  }
2943
2989
  // Clear the default (empty) equations dataset, or it will block adding it
@@ -2949,7 +2995,7 @@ class LinnyRModel {
2949
2995
  this.loading_datasets.length = 0;
2950
2996
  this.max_time_to_load = 0;
2951
2997
  n = childNodeByTag(node, 'datasets');
2952
- if(n && n.childNodes) {
2998
+ if(n) {
2953
2999
  for(const c of n.childNodes) if(c.nodeName === 'dataset') {
2954
3000
  name = xmlDecoded(nodeContentByTag(c, 'name'));
2955
3001
  // NOTE: when including a module, dataset parameters may be bound to
@@ -2961,7 +3007,7 @@ class LinnyRModel {
2961
3007
  if(IO_CONTEXT) {
2962
3008
  if(name === UI.EQUATIONS_DATASET_NAME) {
2963
3009
  const mn = childNodeByTag(c, 'modifiers');
2964
- if(mn && mn.childNodes) {
3010
+ if(mn) {
2965
3011
  for(const cc of mn.childNodes) if(cc.nodeName === 'modifier') {
2966
3012
  this.equations_dataset.addModifier(
2967
3013
  xmlDecoded(nodeContentByTag(cc, 'selector')),
@@ -2980,23 +3026,20 @@ class LinnyRModel {
2980
3026
  if(!this.equations_dataset){
2981
3027
  this.equations_dataset = this.addDataset(UI.EQUATIONS_DATASET_NAME);
2982
3028
  }
2983
- // NOTE: when including a model, charts MUST be prefixed
3029
+ // NOTE: When including a model, charts MUST be prefixed.
2984
3030
  n = childNodeByTag(node, 'charts');
2985
- if(n && n.childNodes) {
2986
- for(i = 0; i < n.childNodes.length; i++) {
2987
- c = n.childNodes[i];
2988
- if(c.nodeName === 'chart') {
2989
- name = xmlDecoded(nodeContentByTag(c, 'title'));
2990
- if(IO_CONTEXT) {
2991
- // NOTE: only include charts with one or more variables
2992
- const vn = childNodeByTag(c, 'variables');
2993
- if(vn && vn.childNodes && vn.childNodes.length > 0) {
2994
- name = IO_CONTEXT.actualName(name);
2995
- this.addChart(name, c);
2996
- }
2997
- } else {
3031
+ if(n) {
3032
+ for(const c of n.childNodes) if(c.nodeName === 'chart') {
3033
+ name = xmlDecoded(nodeContentByTag(c, 'title'));
3034
+ if(IO_CONTEXT) {
3035
+ // NOTE: Only include charts with one or more variables.
3036
+ const vn = childNodeByTag(c, 'variables');
3037
+ if(vn && vn.childNodes && vn.childNodes.length > 0) {
3038
+ name = IO_CONTEXT.actualName(name);
2998
3039
  this.addChart(name, c);
2999
3040
  }
3041
+ } else {
3042
+ this.addChart(name, c);
3000
3043
  }
3001
3044
  }
3002
3045
  }
@@ -3013,27 +3056,21 @@ class LinnyRModel {
3013
3056
  this.base_case_selectors = xmlDecoded(
3014
3057
  nodeContentByTag(node, 'base-case-selectors'));
3015
3058
  n = childNodeByTag(node, 'sensitivity-parameters');
3016
- if(n && n.childNodes) {
3017
- for(i = 0; i < n.childNodes.length; i++) {
3018
- c = n.childNodes[i];
3019
- if(c.nodeName === 'sa-parameter') {
3020
- this.sensitivity_parameters.push(xmlDecoded(nodeContent(c)));
3021
- }
3059
+ if(n) {
3060
+ for(const c of n.childNodes) if(c.nodeName === 'sa-parameter') {
3061
+ this.sensitivity_parameters.push(xmlDecoded(nodeContent(c)));
3022
3062
  }
3023
3063
  }
3024
3064
  n = childNodeByTag(node, 'sensitivity-outcomes');
3025
- if(n && n.childNodes) {
3026
- for(i = 0; i < n.childNodes.length; i++) {
3027
- c = n.childNodes[i];
3028
- if(c.nodeName === 'sa-outcome') {
3029
- this.sensitivity_outcomes.push(xmlDecoded(nodeContent(c)));
3030
- }
3065
+ if(n) {
3066
+ for(const c of n.childNodes) if(c.nodeName === 'sa-outcome') {
3067
+ this.sensitivity_outcomes.push(xmlDecoded(nodeContent(c)));
3031
3068
  }
3032
3069
  }
3033
3070
  this.sensitivity_delta = safeStrToFloat(
3034
3071
  nodeContentByTag(node, 'sensitivity-delta'));
3035
3072
  n = childNodeByTag(node, 'sensitivity-runs');
3036
- if(n && n.childNodes) {
3073
+ if(n) {
3037
3074
  // NOTE: Use a "dummy experiment object" as parent for SA runs.
3038
3075
  const dummy = {title: SENSITIVITY_ANALYSIS.experiment_title};
3039
3076
  for(const c of n.childNodes) if(c.nodeName === 'experiment-run') {
@@ -3043,17 +3080,17 @@ class LinnyRModel {
3043
3080
  }
3044
3081
  }
3045
3082
  n = childNodeByTag(node, 'experiments');
3046
- if(n && n.childNodes) {
3083
+ if(n) {
3047
3084
  for(const c of n.childNodes) if(c.nodeName === 'experiment') {
3048
3085
  this.addExperiment(xmlDecoded(nodeContentByTag(c, 'title')), c);
3049
3086
  }
3050
3087
  }
3051
3088
  n = childNodeByTag(node, 'imports');
3052
- if(n && n.childNodes) {
3089
+ if(n) {
3053
3090
  for(const c of n.childNodes) if(c.nodeName === 'import') this.addImport(c);
3054
3091
  }
3055
3092
  n = childNodeByTag(node, 'exports');
3056
- if(n && n.childNodes) {
3093
+ if(n) {
3057
3094
  for(const c of n.childNodes) if(c.nodeName === 'export') this.addExport(c);
3058
3095
  }
3059
3096
  // Add the default chart (will add it only if absent).
@@ -3061,7 +3098,7 @@ class LinnyRModel {
3061
3098
  // Infer dimensions of experimental design space.
3062
3099
  this.inferDimensions();
3063
3100
  // Set the current time step (if specified).
3064
- let s = nodeParameterValue(node, 'current');
3101
+ const s = nodeParameterValue(node, 'current');
3065
3102
  if(s) {
3066
3103
  this.current_time_step = Math.min(this.end_period,
3067
3104
  Math.max(this.start_period, safeStrToInt(s)));
@@ -3074,8 +3111,8 @@ class LinnyRModel {
3074
3111
  // to minimize conversion effort, set SoC for SINGLE links OUT of processes
3075
3112
  // to 100%.
3076
3113
  if(legacy_model) {
3077
- for(let l in this.links) if(this.links.hasOwnProperty(l)) {
3078
- l = this.links[l];
3114
+ for(let k in this.links) if(this.links.hasOwnProperty(k)) {
3115
+ const l = this.links[k];
3079
3116
  // NOTE: Preserve non-zero SoC values, as these have been specified
3080
3117
  // by the modeler.
3081
3118
  if(l.from_node instanceof Process &&
@@ -3085,8 +3122,10 @@ class LinnyRModel {
3085
3122
  }
3086
3123
  }
3087
3124
  }
3088
- // Recompile expressions so that level-based properties are set
3089
- this.compileExpressions();
3125
+ // Recompile expressions so that level-based properties are set.
3126
+ // NOTE: When a series of modules is included, skip this step until
3127
+ // the last inclusion.
3128
+ if(!IO_CONTEXT || IO_CONTEXT.recompile) this.compileExpressions();
3090
3129
  }
3091
3130
 
3092
3131
  get asXML() {
@@ -4358,7 +4397,7 @@ class IOBinding {
4358
4397
  this.is_data = data;
4359
4398
  this.name_in_module = n;
4360
4399
  if(iot === 2) {
4361
- // For export parameters, the actual name IS the formal name
4400
+ // For export parameters, the actual name IS the formal name.
4362
4401
  this.actual_id = this.id;
4363
4402
  this.actual_name = n;
4364
4403
  } else {
@@ -4386,8 +4425,16 @@ class IOBinding {
4386
4425
  throw `Invalid binding: "${an}" is not of type ${this.entity_type}`;
4387
4426
  }
4388
4427
 
4428
+ get asXML() {
4429
+ // Return an XML string that encodes this binding.
4430
+ return ['<iob type="', this.io_type, '" name="', xmlEncoded(this.name_in_module),
4431
+ '" entity="', VM.entity_letter_codes[this.entity_type.toLowerCase()],
4432
+ (this.is_data ? ' data="1"' : ''), '">',
4433
+ xmlEncoded(this.actual_name), '</iob>'].join('');
4434
+ }
4435
+
4389
4436
  get asHTML() {
4390
- // Returns an HTML string that represents the table rows for this binding
4437
+ // Return an HTML string that represents the table rows for this binding.
4391
4438
  if(this.io_type === 0) return '';
4392
4439
  const
4393
4440
  ioc = ['no', 'i', 'o'],
@@ -4433,7 +4480,7 @@ class IOBinding {
4433
4480
  // CLASS IOContext
4434
4481
  class IOContext {
4435
4482
  constructor(repo='', file='', node=null) {
4436
- // Get the import/export interface of the model to be included
4483
+ // Get the import/export interface of the model to be included.
4437
4484
  this.prefix = '';
4438
4485
  this.bindings = {};
4439
4486
  // Keep track which entities are superseded by "exports"
@@ -4441,16 +4488,19 @@ class IOContext {
4441
4488
  // Keep track which entities are added or superseded (to select them)
4442
4489
  this.added_nodes = [];
4443
4490
  this.added_links = [];
4444
- // Count number of replaced entities in expressions
4491
+ // Count number of replaced entities in expressions.
4445
4492
  this.replace_count = 0;
4446
4493
  this.expression_count = 0;
4447
- // IOContext can be "dummy" when used to rename expression variables
4494
+ // NOTE: IOContext can be "dummy" when used to rename expression variables.
4448
4495
  if(!repo || !file || !node) return;
4496
+ // When updating, set `recompile` to false for all but the last include
4497
+ // so as to prevent compiler warnings due to missing datasets.
4498
+ this.recompile = true;
4449
4499
  this.xml = node;
4450
4500
  this.repo_name = repo;
4451
4501
  this.file_name = file;
4452
4502
  let n = childNodeByTag(node, 'imports');
4453
- if(n && n.childNodes) {
4503
+ if(n) {
4454
4504
  for(const c of n.childNodes) if(c.nodeName === 'import') {
4455
4505
  // NOTE: IO type 1 indicates import.
4456
4506
  this.addBinding(1, xmlDecoded(nodeContentByTag(c, 'type')),
@@ -4459,7 +4509,7 @@ class IOContext {
4459
4509
  }
4460
4510
  }
4461
4511
  n = childNodeByTag(node, 'exports');
4462
- if(n && n.childNodes) {
4512
+ if(n) {
4463
4513
  for(const c of n.childNodes) if(c.nodeName === 'export') {
4464
4514
  // NOTE: IO type 2 indicates export.
4465
4515
  this.addBinding(2, xmlDecoded(nodeContentByTag(c, 'type')),
@@ -4470,14 +4520,14 @@ class IOContext {
4470
4520
  }
4471
4521
 
4472
4522
  addBinding(iot, et, data, n) {
4473
- // Adds a new binding (IO type, entity type, is-data, formal name)
4474
- // to this context
4523
+ // Add a new binding (IO type, entity type, is-data, formal name)
4524
+ // to this context.
4475
4525
  this.bindings[UI.nameToID(n)] = new IOBinding(iot, et, data, n);
4476
4526
  }
4477
4527
 
4478
4528
  bind(fn, an) {
4479
- // Binds the formal name `fn` of an entity in a module to the actual
4480
- // name `an` it will have in the current model
4529
+ // Bind the formal name `fn` of an entity in a module to the actual
4530
+ // name `an` it will have in the current model.
4481
4531
  const id = UI.nameToID(fn);
4482
4532
  if(this.bindings.hasOwnProperty(id)) {
4483
4533
  this.bindings[id].bind(an);
@@ -4487,10 +4537,32 @@ class IOContext {
4487
4537
  }
4488
4538
 
4489
4539
  isBound(n) {
4540
+ // Return the IO type of the binding if name `n` is a module parameter.
4490
4541
  const id = UI.nameToID(n);
4491
4542
  if(this.bindings.hasOwnProperty(id)) return this.bindings[id].io_type;
4492
4543
  return 0;
4493
4544
  }
4545
+
4546
+ isBinding(obj) {
4547
+ // Return the binding if `obj` is bound by this IOContext, otherwise NULL.
4548
+ const
4549
+ an = obj.displayName,
4550
+ et = obj.type;
4551
+ for(const k of Object.keys(this.bindings)) {
4552
+ const iob = this.bindings[k];
4553
+ if(iob.entity_type === et && iob.actual_name === an) return iob;
4554
+ }
4555
+ return null;
4556
+ }
4557
+
4558
+ get copyOfBindings() {
4559
+ // Return a deep copy of the bindings object.
4560
+ const copy = {};
4561
+ for(const k of Object.keys(this.bindings)) {
4562
+ copy[k] = Object.assign({}, this.bindings[k]);
4563
+ }
4564
+ return copy;
4565
+ }
4494
4566
 
4495
4567
  actualName(n, an='') {
4496
4568
  // Return the actual name for a parameter with formal name `n`
@@ -4511,23 +4583,23 @@ class IOContext {
4511
4583
  }
4512
4584
  const id = UI.nameToID(n + an);
4513
4585
  if(this.bindings.hasOwnProperty(id)) {
4514
- // NOTE: return actual name WITHOUT the actor name
4586
+ // NOTE: Return actual name WITHOUT the actor name.
4515
4587
  n = this.bindings[id].actual_name;
4516
4588
  if(an) n = n.slice(0, n.length - an.length);
4517
4589
  return n;
4518
4590
  }
4519
- // All other entities are prefixed
4591
+ // All other entities are prefixed.
4520
4592
  return (this.prefix ? this.prefix + UI.PREFIXER : '') + n;
4521
4593
  }
4522
4594
 
4523
4595
  get clusterName() {
4524
- // Returns full cluster name, i.e., prefix plus actor name if specified
4596
+ // Return full cluster name, i.e., prefix plus actor name if specified.
4525
4597
  if(this.actor_name) return `${this.prefix} (${this.actor_name})`;
4526
4598
  return this.prefix;
4527
4599
  }
4528
4600
 
4529
4601
  get parameterTable() {
4530
- // Returns the HTML for the parameter binding table in the include dialog
4602
+ // Return the HTML for the parameter binding table in the include dialog.
4531
4603
  if(Object.keys(this.bindings).length === 0) {
4532
4604
  return '<div style="margin-top:2px"><em>This module has no parameters.</em></div>';
4533
4605
  }
@@ -4540,9 +4612,9 @@ class IOContext {
4540
4612
  }
4541
4613
 
4542
4614
  bindParameters() {
4543
- // Binds parameters as specified in the INCLUDE MODULE dialog
4615
+ // Bind parameters as specified in the INCLUDE MODULE dialog.
4544
4616
  const pref = (this.prefix ? this.prefix + UI.PREFIXER : '');
4545
- // Compute sum of (x, y) of imported products
4617
+ // Compute sum of (x, y) of imported products.
4546
4618
  let np = 0,
4547
4619
  x = 0,
4548
4620
  y = 0,
@@ -4552,17 +4624,17 @@ class IOContext {
4552
4624
  for(let id in this.bindings) if(this.bindings.hasOwnProperty(id)) {
4553
4625
  const b = this.bindings[id];
4554
4626
  if(b.io_type === 1) {
4555
- // Get the selector for this parameter
4627
+ // Get the selector for this parameter.
4556
4628
  // NOTE: IO_CONTEXT is instantiated *exclusively* by the Repository
4557
- // browser, so that GUI dialog will exist when IO_CONTEXT is not NULL
4629
+ // browser, so that GUI dialog will exist when IO_CONTEXT is not NULL.
4558
4630
  const e = REPOSITORY_BROWSER.parameterBinding(b.id);
4559
4631
  if(e && e.selectedIndex >= 0) {
4560
- // Modeler has selected the actual parameter => set its name
4632
+ // Modeler has selected the actual parameter => set its name.
4561
4633
  const v = e.options[e.selectedIndex].value;
4562
4634
  if(v !== '_CLUSTER') {
4563
4635
  b.actual_name = e.options[e.selectedIndex].text;
4564
4636
  b.actual_id = v;
4565
- // If imported product, add its (x, y) to the centroid (x, y)
4637
+ // If imported product, add its (x, y) to the centroid (x, y).
4566
4638
  if(b.entity_type === 'Product') {
4567
4639
  const p = MODEL.products[v];
4568
4640
  if(p) {
@@ -4583,13 +4655,13 @@ class IOContext {
4583
4655
  }
4584
4656
  }
4585
4657
  if(b.actual_id === '') {
4586
- // By default, bind import parameter to itself (create a local entity)
4658
+ // By default, bind import parameter to itself (create a local entity).
4587
4659
  b.actual_name = pref + b.name_in_module;
4588
4660
  b.actual_id = UI.nameToID(b.actual_name);
4589
4661
  }
4590
4662
  }
4591
4663
  }
4592
- // NOTE: calculate centroid of non-data products if possible
4664
+ // NOTE: Calculate centroid of non-data products if possible.
4593
4665
  if(np > 1) {
4594
4666
  this.centroid_x = Math.round(x / np);
4595
4667
  this.centroid_y = Math.round(y / np);
@@ -4600,7 +4672,7 @@ class IOContext {
4600
4672
  this.centroid_x = Math.round(x + dx + 50);
4601
4673
  this.centroid_y = Math.round(y + dy + 50);
4602
4674
  } else {
4603
- // Position new cluster in upper-left quadrant of view
4675
+ // Position new cluster in upper-left quadrant of view.
4604
4676
  const cp = UI.pointInViewport(0.25, 0.25);
4605
4677
  this.centroid_x = cp[0];
4606
4678
  this.centroid_y = cp[1];
@@ -5114,7 +5186,7 @@ class Note extends ObjectWithXYWH {
5114
5186
  constructor(cluster) {
5115
5187
  super(cluster);
5116
5188
  const dt = new Date();
5117
- // NOTE: use timestamp in msec to generate a unique identifier
5189
+ // NOTE: Use timestamp in msec to generate a unique identifier.
5118
5190
  this.timestamp = dt.getTime();
5119
5191
  this.contents = '';
5120
5192
  this.lines = [];
@@ -5132,7 +5204,7 @@ class Note extends ObjectWithXYWH {
5132
5204
  }
5133
5205
 
5134
5206
  get clusterPrefix() {
5135
- // Returns the name of the cluster containing this note, followed
5207
+ // Return the name of the cluster containing this note, followed
5136
5208
  // by a colon+space, except when this cluster is the top cluster.
5137
5209
  if(this.cluster === MODEL.top_cluster) return '';
5138
5210
  return this.cluster.displayName + UI.PREFIXER;
@@ -5146,8 +5218,8 @@ class Note extends ObjectWithXYWH {
5146
5218
  }
5147
5219
 
5148
5220
  get number() {
5149
- // Returns the number of this note if specified (e.g. as #123).
5150
- // NOTE: this only applies to notes having note fields.
5221
+ // Return the number of this note if specified (e.g. as #123).
5222
+ // NOTE: This only applies to notes having note fields.
5151
5223
  const m = this.contents.replace(/\s+/g, ' ')
5152
5224
  .match(/^[^\]]*#(\d+).*\[\[[^\]]+\]\]/);
5153
5225
  if(m) return m[1];
@@ -5155,7 +5227,7 @@ class Note extends ObjectWithXYWH {
5155
5227
  }
5156
5228
 
5157
5229
  get numberContext() {
5158
- // Returns the string to be used to evaluate #. For notes this is
5230
+ // Return the string to be used to evaluate #. For notes, this is
5159
5231
  // their note number if specified, otherwise the number context of a
5160
5232
  // nearby node, and otherwise the number context of their cluster.
5161
5233
  let n = this.number;
@@ -5166,7 +5238,7 @@ class Note extends ObjectWithXYWH {
5166
5238
  }
5167
5239
 
5168
5240
  get nearbyNode() {
5169
- // Returns a node in the cluster of this note that is closest to this
5241
+ // Return a node in the cluster of this note that is closest to this
5170
5242
  // note (Euclidian distance between center points), but with at most
5171
5243
  // 30 pixel units between their rims.
5172
5244
  const
@@ -5585,14 +5657,13 @@ class NodeBox extends ObjectWithXYWH {
5585
5657
  n = `<em>${this.type}:</em> ${n}`;
5586
5658
  // For clusters, add how many processes and products they contain.
5587
5659
  if(this instanceof Cluster) {
5588
- let d = '';
5660
+ let dl = [];
5589
5661
  if(this.all_processes) {
5590
- const dl = [];
5591
- dl.push(pluralS(this.all_processes.length, 'process'));
5592
- dl.push(pluralS(this.all_products.length, 'product'));
5593
- d = dl.join(', ').toLowerCase();
5662
+ dl.push(pluralS(this.all_processes.length, 'process').toLowerCase());
5663
+ dl.push(pluralS(this.all_products.length, 'product').toLowerCase());
5594
5664
  }
5595
- if(d) n += `<span class="node-details">${d}</span>`;
5665
+ if(this.module) dl.push(`included from <span class="mod-name">${this.module.name}</span>`);
5666
+ if(dl.length) n += `<span class="node-details">${dl.join(', ')}</span>`;
5596
5667
  }
5597
5668
  if(!MODEL.solved) return n;
5598
5669
  const g = this.grid;
@@ -6055,11 +6126,11 @@ class Arrow {
6055
6126
 
6056
6127
  } // END of class Arrow
6057
6128
 
6058
-
6059
6129
  // CLASS Cluster
6060
6130
  class Cluster extends NodeBox {
6061
6131
  constructor(cluster, name, actor) {
6062
6132
  super(cluster, name, actor);
6133
+ this.module = null;
6063
6134
  this.processes = [];
6064
6135
  this.product_positions = [];
6065
6136
  this.sub_clusters = [];
@@ -6170,6 +6241,16 @@ class Cluster extends NodeBox {
6170
6241
  // Clusters have no attribute expressions => always return null.
6171
6242
  return null;
6172
6243
  }
6244
+
6245
+ get moduleAsXML() {
6246
+ if(!this.module) return '';
6247
+ const xml = ['<module name="', xmlEncoded(this.module.name), '">'];
6248
+ for(const k of Object.keys(this.module.bindings)) {
6249
+ xml.push(this.module.bindings[k].asXML);
6250
+ }
6251
+ xml.push('</module>');
6252
+ return xml.join('');
6253
+ }
6173
6254
 
6174
6255
  get asXML() {
6175
6256
  let xml;
@@ -6181,7 +6262,8 @@ class Cluster extends NodeBox {
6181
6262
  (this.toBeBlackBoxed ? ' is-black-boxed="1"' : '');
6182
6263
  xml = ['<cluster', flags, '><name>', xmlEncoded(this.blackBoxName),
6183
6264
  '</name><owner>', xmlEncoded(this.actor.name),
6184
- '</owner><x-coord>', this.x,
6265
+ '</owner>', this.moduleAsXML,
6266
+ '<x-coord>', this.x,
6185
6267
  '</x-coord><y-coord>', this.y,
6186
6268
  '</y-coord><comments>', cmnts,
6187
6269
  '</comments><process-set>'].join('');
@@ -6220,18 +6302,34 @@ class Cluster extends NodeBox {
6220
6302
  this.black_box = nodeParameterValue(node, 'black-box') === '1';
6221
6303
  this.is_black_boxed = nodeParameterValue(node, 'is-black-boxed') === '1';
6222
6304
 
6223
- // NOTE: to compensate for a shameful bug in an earlier version, look
6224
- // for "product-positions" node and for "notes" node in the process-set,
6225
- // as it may have been put there instead of in the cluster node itself
6226
6305
  let name,
6227
6306
  actor,
6228
- n = childNodeByTag(node, 'process-set');
6307
+ n = childNodeByTag(node, 'module');
6308
+ if(n) {
6309
+ this.module = {
6310
+ name: xmlDecoded(nodeParameterValue(n, 'name')),
6311
+ bindings: {}
6312
+ };
6313
+ for(const c of n.childNodes) if(c.nodeName === 'iob') {
6314
+ const
6315
+ iot = parseInt(nodeParameterValue(c, 'type')),
6316
+ et = capitalized(VM.entity_names[nodeParameterValue(c, 'entity')]),
6317
+ iob = new IOBinding(iot, et,
6318
+ nodeParameterValue(c, 'data') === '1',
6319
+ xmlDecoded(nodeParameterValue(c, 'name')));
6320
+ iob.actual_name = nodeContent(c);
6321
+ this.module.bindings[iob.id] = iob;
6322
+ }
6323
+ }
6324
+ n = childNodeByTag(node, 'process-set');
6325
+ // NOTE: To compensate for a shameful bug in an earlier version, look
6326
+ // for "product-positions" node and for "notes" node in the process-set,
6327
+ // as it may have been put there instead of in the cluster node itself.
6229
6328
  const
6230
6329
  hidden_pp = childNodeByTag(n, 'product-positions'),
6231
6330
  hidden_notes = childNodeByTag(n, 'notes');
6232
- // (if they exist, these nodes will be used a bit further down)
6233
-
6234
- if(n && n.childNodes) {
6331
+ // If they exist, these nodes will be used a bit further down.
6332
+ if(n) {
6235
6333
  for(const c of n.childNodes) if(c.nodeName === 'process-name') {
6236
6334
  name = xmlDecoded(nodeContent(c));
6237
6335
  if(IO_CONTEXT) {
@@ -6257,7 +6355,7 @@ class Cluster extends NodeBox {
6257
6355
  }
6258
6356
  }
6259
6357
  n = childNodeByTag(node, 'sub-clusters');
6260
- if(n && n.childNodes) {
6358
+ if(n) {
6261
6359
  for(const c of n.childNodes) if(c.nodeName === 'cluster') {
6262
6360
  // Refocus on this cluster because addCluster may change focus if it
6263
6361
  // contains subclusters.
@@ -6275,7 +6373,7 @@ class Cluster extends NodeBox {
6275
6373
  }
6276
6374
  // NOTE: the part " || hidden_pp" is to compensate for a bug -- see earlier note.
6277
6375
  n = childNodeByTag(node, 'product-positions') || hidden_pp;
6278
- if(n && n.childNodes) {
6376
+ if(n) {
6279
6377
  for(const c of n.childNodes) if(c.nodeName === 'product-position') {
6280
6378
  name = xmlDecoded(nodeContentByTag(c, 'product-name'));
6281
6379
  if(IO_CONTEXT) name = IO_CONTEXT.actualName(name);
@@ -6284,7 +6382,7 @@ class Cluster extends NodeBox {
6284
6382
  }
6285
6383
  }
6286
6384
  n = childNodeByTag(node, 'notes') || hidden_notes;
6287
- if(n && n.childNodes) {
6385
+ if(n) {
6288
6386
  for(const c of n.childNodes) if(c.nodeName === 'note') {
6289
6387
  const note = new Note(this);
6290
6388
  note.initFromXML(c);
@@ -6696,35 +6794,6 @@ class Cluster extends NodeBox {
6696
6794
  }
6697
6795
  }
6698
6796
 
6699
- /* DISABLED -- idea was OK but this results in many additional links
6700
- that clutter the diagram; representing these lines by block arrows
6701
- produces better results
6702
-
6703
- // Special case: P1 --> Q with process Q outside this cluster that
6704
- // produces some other product P2 which has a position in this cluster
6705
- if(p instanceof Product && q instanceof Process && cq === null) {
6706
- let p2 = null,
6707
- i = 0,
6708
- ll = (p_to_q ? q.outputs : q.inputs);
6709
- while(!p2 && i < ll.length) {
6710
- const n = (p_to_q ? ll[i].to_node : ll[i].from_node);
6711
- if(this.indexOfProduct(n) >= 0) {
6712
- p2 = n;
6713
- } else {
6714
- i++;
6715
- }
6716
- }
6717
- if(p2) {
6718
- if(p_to_q) {
6719
- this.addArrow(lnk, p, p2);
6720
- } else {
6721
- this.addArrow(lnk, p2, p);
6722
- }
6723
- return;
6724
- }
6725
- }
6726
- */
6727
-
6728
6797
  // If P and Q are both processes, while either one is not visible,
6729
6798
  // the arrow will be unique (as each process is in only ONE cluster)
6730
6799
  // and connect either a process node to a cluster node, or two
@@ -6855,28 +6924,29 @@ class Cluster extends NodeBox {
6855
6924
  deleteProduct(p, with_xml=true) {
6856
6925
  // Remove "placeholder" of product `p` from this cluster, and
6857
6926
  // remove `p` from the model if there are no other clusters
6858
- // containing a "placeholder" for `p`
6927
+ // containing a "placeholder" for `p`.
6859
6928
  // Always set "selected" attribute to FALSE (or the product will
6860
- // still be drawn in red)
6929
+ // still be drawn in red).
6861
6930
  p.selected = false;
6862
6931
  let i = this.indexOfProduct(p);
6863
6932
  if(i < 0) return false;
6864
6933
  // Append XML for product positions unlesss deleting from a cluster
6865
- // that is being deleted
6934
+ // that is being deleted.
6866
6935
  if(with_xml) UNDO_STACK.addXML(this.product_positions[i].asXML);
6867
- // Remove product position of `p` in this cluster
6936
+ // Remove product position of `p` in this cluster.
6868
6937
  this.product_positions.splice(i, 1);
6869
- // Do not delete product from this cluster unless it has NO links to
6870
- // processes in other clusters
6871
- if(!p.allLinksInCluster(this)) {
6872
- // NOTE: removing only the product position DOES affect the
6873
- // diagram, so prepare for redraw
6938
+ // Do not delete product from this cluster if it has links to
6939
+ // processes in other clusters, of if this cluster is updating
6940
+ // and binds the product as parameter.
6941
+ if(!p.allLinksInCluster(this) || (IO_CONTEXT && IO_CONTEXT.isBinding(p))) {
6942
+ // NOTE: Removing only the product position DOES affect the
6943
+ // diagram, so prepare for redraw.
6874
6944
  this.clearAllProcesses();
6875
6945
  return false;
6876
6946
  }
6877
6947
  // If no clusters contain `p`, delete it from the model entirely
6878
- // (incl. all links to and from `p`). NOTE: such deletions WILL
6879
- // append their undo XML
6948
+ // (incl. all links to and from `p`). NOTE: Such deletions WILL
6949
+ // append their undo XML.
6880
6950
  MODEL.deleteNode(p);
6881
6951
  return true;
6882
6952
  }
@@ -9513,7 +9583,7 @@ class Dataset {
9513
9583
  this.unpackDataString(xmlDecoded(nodeContentByTag(node, 'data')));
9514
9584
  }
9515
9585
  const n = childNodeByTag(node, 'modifiers');
9516
- if(n && n.childNodes) {
9586
+ if(n) {
9517
9587
  for(const c of n.childNodes) if(c.nodeName === 'modifier') {
9518
9588
  this.addModifier(xmlDecoded(nodeContentByTag(c, 'selector')), c);
9519
9589
  }
@@ -9636,6 +9706,12 @@ class ChartVariable {
9636
9706
  this.sorted = sort;
9637
9707
  }
9638
9708
 
9709
+ get type() {
9710
+ // NOTE: Charts are not entities, but the dialogs may inquire their type
9711
+ // for sorting and presentation (e.g., to determine icon name).
9712
+ return 'Chart';
9713
+ }
9714
+
9639
9715
  get displayName() {
9640
9716
  // Returns the display name for this variable. This is the name of
9641
9717
  // the Linny-R entity and its attribute, followed by its scale factor
@@ -10002,7 +10078,7 @@ class Chart {
10002
10078
  this.legend_position = nodeContentByTag(node, 'legend-position');
10003
10079
  this.variables.length = 0;
10004
10080
  const n = childNodeByTag(node, 'variables');
10005
- if(n && n.childNodes) {
10081
+ if(n) {
10006
10082
  for(const c of n.childNodes) if(c.nodeName === 'chart-variable') {
10007
10083
  const v = new ChartVariable(this);
10008
10084
  // NOTE: Variable may refer to deleted entity => do not add.
@@ -11424,13 +11500,13 @@ class ExperimentRun {
11424
11500
  this.time_steps = safeStrToInt(nodeContentByTag(node, 'time-steps'));
11425
11501
  this.time_step_duration = safeStrToFloat(nodeContentByTag(node, 'delta-t'));
11426
11502
  let n = childNodeByTag(node, 'results');
11427
- if(n && n.childNodes) {
11503
+ if(n) {
11428
11504
  for(const c of n.childNodes) if(c.nodeName === 'run-result') {
11429
11505
  this.results.push(new ExperimentRunResult(this, c));
11430
11506
  }
11431
11507
  }
11432
11508
  n = childNodeByTag(node, 'messages');
11433
- if(n && n.childNodes) {
11509
+ if(n) {
11434
11510
  for(const c of n.childNodes) if(c.nodeName === 'block-msg') {
11435
11511
  this.block_messages.push(new BlockMessages(c));
11436
11512
  }
@@ -11861,7 +11937,7 @@ class Experiment {
11861
11937
  this.title = xmlDecoded(nodeContentByTag(node, 'title'));
11862
11938
  this.comments = xmlDecoded(nodeContentByTag(node, 'notes'));
11863
11939
  let n = childNodeByTag(node, 'dimensions');
11864
- if(n && n.childNodes) {
11940
+ if(n) {
11865
11941
  for(const c of n.childNodes) if(c.nodeName === 'dim') {
11866
11942
  this.dimensions.push(xmlDecoded(nodeContent(c)).split(','));
11867
11943
  }
@@ -11875,7 +11951,7 @@ class Experiment {
11875
11951
  }
11876
11952
  }
11877
11953
  n = childNodeByTag(node, 'chart-titles');
11878
- if(n && n.childNodes) {
11954
+ if(n) {
11879
11955
  for(const c of n.childNodes) if(c.nodeName === 'chart-title') {
11880
11956
  const ci = MODEL.indexOfChart(xmlDecoded(nodeContent(c)));
11881
11957
  // Double-check: only add existing charts.
@@ -11883,31 +11959,31 @@ class Experiment {
11883
11959
  }
11884
11960
  }
11885
11961
  n = childNodeByTag(node, 'settings-selectors');
11886
- if(n && n.childNodes) {
11962
+ if(n) {
11887
11963
  for(const c of n.childNodes) if(c.nodeName === 'ssel') {
11888
11964
  this.settings_selectors.push(xmlDecoded(nodeContent(c)));
11889
11965
  }
11890
11966
  }
11891
11967
  n = childNodeByTag(node, 'settings-dimensions');
11892
- if(n && n.childNodes) {
11968
+ if(n) {
11893
11969
  for(const c of n.childNodes) if(c.nodeName === 'sdim') {
11894
11970
  this.settings_dimensions.push(xmlDecoded(nodeContent(c)).split(','));
11895
11971
  }
11896
11972
  }
11897
11973
  n = childNodeByTag(node, 'combination-selectors');
11898
- if(n && n.childNodes) {
11974
+ if(n) {
11899
11975
  for(const c of n.childNodes) if(c.nodeName === 'csel') {
11900
11976
  this.combination_selectors.push(xmlDecoded(nodeContent(c)));
11901
11977
  }
11902
11978
  }
11903
11979
  n = childNodeByTag(node, 'combination-dimensions');
11904
- if(n && n.childNodes) {
11980
+ if(n) {
11905
11981
  for(const c of n.childNodes) if(c.nodeName === 'cdim') {
11906
11982
  this.combination_dimensions.push(xmlDecoded(nodeContent(c)).split(','));
11907
11983
  }
11908
11984
  }
11909
11985
  n = childNodeByTag(node, 'actor-selectors');
11910
- if(n && n.childNodes) {
11986
+ if(n) {
11911
11987
  for(const c of n.childNodes) if(c.nodeName === 'asel') {
11912
11988
  const as = new ActorSelector();
11913
11989
  as.initFromXML(c);
@@ -11916,7 +11992,7 @@ class Experiment {
11916
11992
  }
11917
11993
  this.excluded_selectors = xmlDecoded(nodeContentByTag(node, 'excluded-selectors'));
11918
11994
  n = childNodeByTag(node, 'clusters-to-ignore');
11919
- if(n && n.childNodes) {
11995
+ if(n) {
11920
11996
  for(const c of n.childNodes) if(c.nodeName === 'cluster-to-ignore') {
11921
11997
  const
11922
11998
  cdn = xmlDecoded(nodeContentByTag(c, 'cluster')),
@@ -11931,7 +12007,7 @@ class Experiment {
11931
12007
  }
11932
12008
  }
11933
12009
  n = childNodeByTag(node, 'runs');
11934
- if(n && n.childNodes) {
12010
+ if(n) {
11935
12011
  let r = 0;
11936
12012
  for(const c of n.childNodes) if(c.nodeName === 'experiment-run') {
11937
12013
  const xr = new ExperimentRun(this, r);
@@ -12360,7 +12436,7 @@ class Experiment {
12360
12436
  const rr = this.runs[rnr].results[vi];
12361
12437
  if(rr) {
12362
12438
  // NOTE: Only experiment variables have vector data.
12363
- if(rr.x_variable && i <= rr.N) {
12439
+ if(rr.x_variable && vi <= rr.N) {
12364
12440
  row.push(numval(rr.vector[t], prec));
12365
12441
  } else {
12366
12442
  row.push('');
@@ -12685,7 +12761,7 @@ class BoundLine {
12685
12761
  xmlDecoded(nodeContentByTag(node, 'point-data')));
12686
12762
  }
12687
12763
  const n = childNodeByTag(node, 'selectors');
12688
- if(n && n.childNodes && n.childNodes.length) {
12764
+ if(n.length) {
12689
12765
  // NOTE: Only overwrite default selector if XML specifies selectors.
12690
12766
  this.selectors.length = 0;
12691
12767
  for(const c of n.childNodes) if(c.nodeName === 'boundline-selector') {
@@ -12990,7 +13066,7 @@ class Constraint {
12990
13066
  this.soc_direction = safeStrToInt(
12991
13067
  nodeParameterValue(node, 'soc-direction'), 1);
12992
13068
  const n = childNodeByTag(node, 'bound-lines');
12993
- if(n && n.childNodes) {
13069
+ if(n) {
12994
13070
  // NOTE: only overwrite default lines if XML specifies bound lines
12995
13071
  this.bound_lines.length = 0;
12996
13072
  for(const c of n.childNodes) if(c.nodeName === 'bound-line') {