linny-r 2.0.12 → 2.1.1

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);
@@ -2089,7 +2161,9 @@ class LinnyRModel {
2089
2161
  // Move all selected nodes to cluster `c`.
2090
2162
  // NOTE: The relative position of the selected notes is presserved,
2091
2163
  // but the nodes are positioned to the right of the diagram of `c`
2092
- // with a margin of 50 pixels.
2164
+ // with a margin of 50 pixels.
2165
+ // NOTE: No dropping if the selection contains one note.
2166
+ if(this.selection.length === 1 && this.selection[0] instanceof Note) return;
2093
2167
  const
2094
2168
  space = 50,
2095
2169
  rmx = c.rightMarginX + space,
@@ -2386,8 +2460,8 @@ class LinnyRModel {
2386
2460
  UI.removeShape(node.shape);
2387
2461
  if(node instanceof Process) {
2388
2462
  // 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);
2463
+ const index = node.cluster.processes.indexOf(node);
2464
+ if(index >= 0) node.cluster.processes.splice(index, 1);
2391
2465
  delete this.processes[node.identifier];
2392
2466
  } else {
2393
2467
  // Remove product from parameter lists.
@@ -2410,11 +2484,11 @@ class LinnyRModel {
2410
2484
  return;
2411
2485
  }
2412
2486
  // 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);
2487
+ const oi = link.from_node.outputs.indexOf(link);
2488
+ if(oi >= 0) link.from_node.outputs.splice(oi, 1);
2415
2489
  // 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);
2490
+ const ii = link.to_node.inputs.indexOf(link);
2491
+ if(ii >= 0) link.to_node.inputs.splice(ii, 1);
2418
2492
  // Prepare for redraw
2419
2493
  link.from_node.cluster.clearAllProcesses();
2420
2494
  link.to_node.cluster.clearAllProcesses();
@@ -2467,8 +2541,8 @@ class LinnyRModel {
2467
2541
  this.deleteCluster(c.sub_clusters[i], false);
2468
2542
  }
2469
2543
  // 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);
2544
+ const index = c.cluster.sub_clusters.indexOf(c);
2545
+ if(index >= 0) c.cluster.sub_clusters.splice(index, 1);
2472
2546
  UI.removeShape(c.shape);
2473
2547
  // Finally, remove the cluster from the model.
2474
2548
  delete this.clusters[c.identifier];
@@ -2798,146 +2872,120 @@ class LinnyRModel {
2798
2872
  } // END IF *not* including a model
2799
2873
 
2800
2874
  // Declare some local variables that will be used a lot.
2801
- let i,
2802
- c,
2803
- name,
2875
+ let name,
2804
2876
  actor,
2805
2877
  fn,
2806
2878
  tn,
2807
2879
  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
- }
2880
+ // Scale units are not "entities", and can be included "as is".
2881
+ if(n) {
2882
+ for(const c of n.childNodes) if(c.nodeName === 'scaleunit') {
2883
+ this.addScaleUnit(xmlDecoded(nodeContentByTag(c, 'name')),
2884
+ nodeContentByTag(c, 'scalar'),
2885
+ xmlDecoded(nodeContentByTag(c, 'base-unit')));
2817
2886
  }
2818
2887
  }
2819
- // Power grids are not "entities", and can be included "as is"
2888
+ // Power grids are not "entities", and can be included "as is".
2820
2889
  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
- }
2890
+ if(n) {
2891
+ for(const c of n.childNodes) if(c.nodeName === 'grid') {
2892
+ this.addPowerGrid(nodeContentByTag(c, 'id'), c);
2827
2893
  }
2828
2894
  }
2829
- // When including a model, actors may be bound to an existing actor
2895
+ // When including a model, actors may be bound to an existing actor.
2830
2896
  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
- }
2897
+ if(n) {
2898
+ for(const c of n.childNodes) if(c.nodeName === 'actor') {
2899
+ name = xmlDecoded(nodeContentByTag(c, 'name'));
2900
+ if(IO_CONTEXT) name = IO_CONTEXT.actualName(name);
2901
+ this.addActor(name, c);
2839
2902
  }
2840
2903
  }
2841
- // When including a model, processes MUST be prefixed
2904
+ // When including a model, processes MUST be prefixed.
2842
2905
  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);
2906
+ if(n) {
2907
+ for(const c of n.childNodes) if(c.nodeName === 'process') {
2908
+ name = xmlDecoded(nodeContentByTag(c, 'name'));
2909
+ actor = xmlDecoded(nodeContentByTag(c, 'owner'));
2910
+ if(IO_CONTEXT) {
2911
+ actor = IO_CONTEXT.actualName(actor);
2912
+ name = IO_CONTEXT.actualName(name, actor);
2854
2913
  }
2914
+ this.addProcess(name, actor, c);
2855
2915
  }
2856
2916
  }
2857
- // When including a model, products may be bound to an existing product
2917
+ // When including a model, products may be bound to an existing product.
2858
2918
  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
- }
2919
+ if(n) {
2920
+ for(const c of n.childNodes) if(c.nodeName === 'product') {
2921
+ name = xmlDecoded(nodeContentByTag(c, 'name'));
2922
+ if(IO_CONTEXT) name = IO_CONTEXT.actualName(name);
2923
+ this.addProduct(name, c);
2867
2924
  }
2868
2925
  }
2869
- // When including a model, link nodes may be bound to existing nodes
2926
+ // When including a model, link nodes may be bound to existing nodes.
2870
2927
  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'));
2928
+ if(n) {
2929
+ for(const c of n.childNodes) if(c.nodeName === 'link') {
2930
+ name = xmlDecoded(nodeContentByTag(c, 'from-name'));
2931
+ actor = xmlDecoded(nodeContentByTag(c, 'from-owner'));
2932
+ if(IO_CONTEXT) {
2933
+ actor = IO_CONTEXT.actualName(actor);
2934
+ name = IO_CONTEXT.actualName(name, actor);
2935
+ }
2936
+ if(actor != UI.NO_ACTOR) name += ` (${actor})`;
2937
+ fn = this.nodeBoxByID(UI.nameToID(name));
2938
+ if(fn) {
2939
+ name = xmlDecoded(nodeContentByTag(c, 'to-name'));
2940
+ actor = xmlDecoded(nodeContentByTag(c, 'to-owner'));
2877
2941
  if(IO_CONTEXT) {
2878
2942
  actor = IO_CONTEXT.actualName(actor);
2879
2943
  name = IO_CONTEXT.actualName(name, actor);
2880
2944
  }
2881
2945
  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
- }
2946
+ tn = this.nodeBoxByID(UI.nameToID(name));
2947
+ if(tn) this.addLink(fn, tn, c);
2894
2948
  }
2895
2949
  }
2896
2950
  }
2897
- // When including a model, constraint nodes may be bound to existing nodes
2951
+ // When including a model, constraint nodes may be bound to existing nodes.
2898
2952
  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'));
2953
+ if(n) {
2954
+ for(const c of n.childNodes) if(c.nodeName === 'constraint') {
2955
+ name = xmlDecoded(nodeContentByTag(c, 'from-name'));
2956
+ actor = xmlDecoded(nodeContentByTag(c, 'from-owner'));
2957
+ if(IO_CONTEXT) {
2958
+ actor = IO_CONTEXT.actualName(actor);
2959
+ name = IO_CONTEXT.actualName(name, actor);
2960
+ }
2961
+ if(actor != UI.NO_ACTOR) name += ` (${actor})`;
2962
+ fn = this.nodeBoxByID(UI.nameToID(name));
2963
+ if(fn) {
2964
+ name = xmlDecoded(nodeContentByTag(c, 'to-name'));
2965
+ actor = xmlDecoded(nodeContentByTag(c, 'to-owner'));
2905
2966
  if(IO_CONTEXT) {
2906
2967
  actor = IO_CONTEXT.actualName(actor);
2907
2968
  name = IO_CONTEXT.actualName(name, actor);
2908
2969
  }
2909
2970
  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
- }
2971
+ tn = this.nodeBoxByID(UI.nameToID(name));
2972
+ if(tn) this.addConstraint(fn, tn, c);
2922
2973
  }
2923
2974
  }
2924
2975
  }
2925
2976
  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);
2977
+ if(n) {
2978
+ for(const c of n.childNodes) if(c.nodeName === 'cluster') {
2979
+ name = xmlDecoded(nodeContentByTag(c, 'name'));
2980
+ actor = xmlDecoded(nodeContentByTag(c, 'owner'));
2981
+ // When including a model, clusters MUST be prefixed
2982
+ if(IO_CONTEXT) {
2983
+ actor = IO_CONTEXT.actualName(actor);
2984
+ // NOTE: actualName will rename the top cluster of an included
2985
+ // model to just the prefix
2986
+ name = IO_CONTEXT.actualName(name, actor);
2940
2987
  }
2988
+ this.addCluster(name, actor, c);
2941
2989
  }
2942
2990
  }
2943
2991
  // Clear the default (empty) equations dataset, or it will block adding it
@@ -2949,7 +2997,7 @@ class LinnyRModel {
2949
2997
  this.loading_datasets.length = 0;
2950
2998
  this.max_time_to_load = 0;
2951
2999
  n = childNodeByTag(node, 'datasets');
2952
- if(n && n.childNodes) {
3000
+ if(n) {
2953
3001
  for(const c of n.childNodes) if(c.nodeName === 'dataset') {
2954
3002
  name = xmlDecoded(nodeContentByTag(c, 'name'));
2955
3003
  // NOTE: when including a module, dataset parameters may be bound to
@@ -2961,7 +3009,7 @@ class LinnyRModel {
2961
3009
  if(IO_CONTEXT) {
2962
3010
  if(name === UI.EQUATIONS_DATASET_NAME) {
2963
3011
  const mn = childNodeByTag(c, 'modifiers');
2964
- if(mn && mn.childNodes) {
3012
+ if(mn) {
2965
3013
  for(const cc of mn.childNodes) if(cc.nodeName === 'modifier') {
2966
3014
  this.equations_dataset.addModifier(
2967
3015
  xmlDecoded(nodeContentByTag(cc, 'selector')),
@@ -2980,23 +3028,20 @@ class LinnyRModel {
2980
3028
  if(!this.equations_dataset){
2981
3029
  this.equations_dataset = this.addDataset(UI.EQUATIONS_DATASET_NAME);
2982
3030
  }
2983
- // NOTE: when including a model, charts MUST be prefixed
3031
+ // NOTE: When including a model, charts MUST be prefixed.
2984
3032
  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 {
3033
+ if(n) {
3034
+ for(const c of n.childNodes) if(c.nodeName === 'chart') {
3035
+ name = xmlDecoded(nodeContentByTag(c, 'title'));
3036
+ if(IO_CONTEXT) {
3037
+ // NOTE: Only include charts with one or more variables.
3038
+ const vn = childNodeByTag(c, 'variables');
3039
+ if(vn && vn.childNodes && vn.childNodes.length > 0) {
3040
+ name = IO_CONTEXT.actualName(name);
2998
3041
  this.addChart(name, c);
2999
3042
  }
3043
+ } else {
3044
+ this.addChart(name, c);
3000
3045
  }
3001
3046
  }
3002
3047
  }
@@ -3013,27 +3058,21 @@ class LinnyRModel {
3013
3058
  this.base_case_selectors = xmlDecoded(
3014
3059
  nodeContentByTag(node, 'base-case-selectors'));
3015
3060
  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
- }
3061
+ if(n) {
3062
+ for(const c of n.childNodes) if(c.nodeName === 'sa-parameter') {
3063
+ this.sensitivity_parameters.push(xmlDecoded(nodeContent(c)));
3022
3064
  }
3023
3065
  }
3024
3066
  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
- }
3067
+ if(n) {
3068
+ for(const c of n.childNodes) if(c.nodeName === 'sa-outcome') {
3069
+ this.sensitivity_outcomes.push(xmlDecoded(nodeContent(c)));
3031
3070
  }
3032
3071
  }
3033
3072
  this.sensitivity_delta = safeStrToFloat(
3034
3073
  nodeContentByTag(node, 'sensitivity-delta'));
3035
3074
  n = childNodeByTag(node, 'sensitivity-runs');
3036
- if(n && n.childNodes) {
3075
+ if(n) {
3037
3076
  // NOTE: Use a "dummy experiment object" as parent for SA runs.
3038
3077
  const dummy = {title: SENSITIVITY_ANALYSIS.experiment_title};
3039
3078
  for(const c of n.childNodes) if(c.nodeName === 'experiment-run') {
@@ -3043,17 +3082,17 @@ class LinnyRModel {
3043
3082
  }
3044
3083
  }
3045
3084
  n = childNodeByTag(node, 'experiments');
3046
- if(n && n.childNodes) {
3085
+ if(n) {
3047
3086
  for(const c of n.childNodes) if(c.nodeName === 'experiment') {
3048
3087
  this.addExperiment(xmlDecoded(nodeContentByTag(c, 'title')), c);
3049
3088
  }
3050
3089
  }
3051
3090
  n = childNodeByTag(node, 'imports');
3052
- if(n && n.childNodes) {
3091
+ if(n) {
3053
3092
  for(const c of n.childNodes) if(c.nodeName === 'import') this.addImport(c);
3054
3093
  }
3055
3094
  n = childNodeByTag(node, 'exports');
3056
- if(n && n.childNodes) {
3095
+ if(n) {
3057
3096
  for(const c of n.childNodes) if(c.nodeName === 'export') this.addExport(c);
3058
3097
  }
3059
3098
  // Add the default chart (will add it only if absent).
@@ -3061,7 +3100,7 @@ class LinnyRModel {
3061
3100
  // Infer dimensions of experimental design space.
3062
3101
  this.inferDimensions();
3063
3102
  // Set the current time step (if specified).
3064
- let s = nodeParameterValue(node, 'current');
3103
+ const s = nodeParameterValue(node, 'current');
3065
3104
  if(s) {
3066
3105
  this.current_time_step = Math.min(this.end_period,
3067
3106
  Math.max(this.start_period, safeStrToInt(s)));
@@ -3074,8 +3113,8 @@ class LinnyRModel {
3074
3113
  // to minimize conversion effort, set SoC for SINGLE links OUT of processes
3075
3114
  // to 100%.
3076
3115
  if(legacy_model) {
3077
- for(let l in this.links) if(this.links.hasOwnProperty(l)) {
3078
- l = this.links[l];
3116
+ for(let k in this.links) if(this.links.hasOwnProperty(k)) {
3117
+ const l = this.links[k];
3079
3118
  // NOTE: Preserve non-zero SoC values, as these have been specified
3080
3119
  // by the modeler.
3081
3120
  if(l.from_node instanceof Process &&
@@ -3085,8 +3124,10 @@ class LinnyRModel {
3085
3124
  }
3086
3125
  }
3087
3126
  }
3088
- // Recompile expressions so that level-based properties are set
3089
- this.compileExpressions();
3127
+ // Recompile expressions so that level-based properties are set.
3128
+ // NOTE: When a series of modules is included, skip this step until
3129
+ // the last inclusion.
3130
+ if(!IO_CONTEXT || IO_CONTEXT.recompile) this.compileExpressions();
3090
3131
  }
3091
3132
 
3092
3133
  get asXML() {
@@ -4358,7 +4399,7 @@ class IOBinding {
4358
4399
  this.is_data = data;
4359
4400
  this.name_in_module = n;
4360
4401
  if(iot === 2) {
4361
- // For export parameters, the actual name IS the formal name
4402
+ // For export parameters, the actual name IS the formal name.
4362
4403
  this.actual_id = this.id;
4363
4404
  this.actual_name = n;
4364
4405
  } else {
@@ -4386,8 +4427,16 @@ class IOBinding {
4386
4427
  throw `Invalid binding: "${an}" is not of type ${this.entity_type}`;
4387
4428
  }
4388
4429
 
4430
+ get asXML() {
4431
+ // Return an XML string that encodes this binding.
4432
+ return ['<iob type="', this.io_type, '" name="', xmlEncoded(this.name_in_module),
4433
+ '" entity="', VM.entity_letter_codes[this.entity_type.toLowerCase()],
4434
+ (this.is_data ? ' data="1"' : ''), '">',
4435
+ xmlEncoded(this.actual_name), '</iob>'].join('');
4436
+ }
4437
+
4389
4438
  get asHTML() {
4390
- // Returns an HTML string that represents the table rows for this binding
4439
+ // Return an HTML string that represents the table rows for this binding.
4391
4440
  if(this.io_type === 0) return '';
4392
4441
  const
4393
4442
  ioc = ['no', 'i', 'o'],
@@ -4433,7 +4482,7 @@ class IOBinding {
4433
4482
  // CLASS IOContext
4434
4483
  class IOContext {
4435
4484
  constructor(repo='', file='', node=null) {
4436
- // Get the import/export interface of the model to be included
4485
+ // Get the import/export interface of the model to be included.
4437
4486
  this.prefix = '';
4438
4487
  this.bindings = {};
4439
4488
  // Keep track which entities are superseded by "exports"
@@ -4441,16 +4490,19 @@ class IOContext {
4441
4490
  // Keep track which entities are added or superseded (to select them)
4442
4491
  this.added_nodes = [];
4443
4492
  this.added_links = [];
4444
- // Count number of replaced entities in expressions
4493
+ // Count number of replaced entities in expressions.
4445
4494
  this.replace_count = 0;
4446
4495
  this.expression_count = 0;
4447
- // IOContext can be "dummy" when used to rename expression variables
4496
+ // NOTE: IOContext can be "dummy" when used to rename expression variables.
4448
4497
  if(!repo || !file || !node) return;
4498
+ // When updating, set `recompile` to false for all but the last include
4499
+ // so as to prevent compiler warnings due to missing datasets.
4500
+ this.recompile = true;
4449
4501
  this.xml = node;
4450
4502
  this.repo_name = repo;
4451
4503
  this.file_name = file;
4452
4504
  let n = childNodeByTag(node, 'imports');
4453
- if(n && n.childNodes) {
4505
+ if(n) {
4454
4506
  for(const c of n.childNodes) if(c.nodeName === 'import') {
4455
4507
  // NOTE: IO type 1 indicates import.
4456
4508
  this.addBinding(1, xmlDecoded(nodeContentByTag(c, 'type')),
@@ -4459,7 +4511,7 @@ class IOContext {
4459
4511
  }
4460
4512
  }
4461
4513
  n = childNodeByTag(node, 'exports');
4462
- if(n && n.childNodes) {
4514
+ if(n) {
4463
4515
  for(const c of n.childNodes) if(c.nodeName === 'export') {
4464
4516
  // NOTE: IO type 2 indicates export.
4465
4517
  this.addBinding(2, xmlDecoded(nodeContentByTag(c, 'type')),
@@ -4470,14 +4522,14 @@ class IOContext {
4470
4522
  }
4471
4523
 
4472
4524
  addBinding(iot, et, data, n) {
4473
- // Adds a new binding (IO type, entity type, is-data, formal name)
4474
- // to this context
4525
+ // Add a new binding (IO type, entity type, is-data, formal name)
4526
+ // to this context.
4475
4527
  this.bindings[UI.nameToID(n)] = new IOBinding(iot, et, data, n);
4476
4528
  }
4477
4529
 
4478
4530
  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
4531
+ // Bind the formal name `fn` of an entity in a module to the actual
4532
+ // name `an` it will have in the current model.
4481
4533
  const id = UI.nameToID(fn);
4482
4534
  if(this.bindings.hasOwnProperty(id)) {
4483
4535
  this.bindings[id].bind(an);
@@ -4487,10 +4539,32 @@ class IOContext {
4487
4539
  }
4488
4540
 
4489
4541
  isBound(n) {
4542
+ // Return the IO type of the binding if name `n` is a module parameter.
4490
4543
  const id = UI.nameToID(n);
4491
4544
  if(this.bindings.hasOwnProperty(id)) return this.bindings[id].io_type;
4492
4545
  return 0;
4493
4546
  }
4547
+
4548
+ isBinding(obj) {
4549
+ // Return the binding if `obj` is bound by this IOContext, otherwise NULL.
4550
+ const
4551
+ an = obj.displayName,
4552
+ et = obj.type;
4553
+ for(const k of Object.keys(this.bindings)) {
4554
+ const iob = this.bindings[k];
4555
+ if(iob.entity_type === et && iob.actual_name === an) return iob;
4556
+ }
4557
+ return null;
4558
+ }
4559
+
4560
+ get copyOfBindings() {
4561
+ // Return a deep copy of the bindings object.
4562
+ const copy = {};
4563
+ for(const k of Object.keys(this.bindings)) {
4564
+ copy[k] = Object.assign({}, this.bindings[k]);
4565
+ }
4566
+ return copy;
4567
+ }
4494
4568
 
4495
4569
  actualName(n, an='') {
4496
4570
  // Return the actual name for a parameter with formal name `n`
@@ -4511,23 +4585,23 @@ class IOContext {
4511
4585
  }
4512
4586
  const id = UI.nameToID(n + an);
4513
4587
  if(this.bindings.hasOwnProperty(id)) {
4514
- // NOTE: return actual name WITHOUT the actor name
4588
+ // NOTE: Return actual name WITHOUT the actor name.
4515
4589
  n = this.bindings[id].actual_name;
4516
4590
  if(an) n = n.slice(0, n.length - an.length);
4517
4591
  return n;
4518
4592
  }
4519
- // All other entities are prefixed
4593
+ // All other entities are prefixed.
4520
4594
  return (this.prefix ? this.prefix + UI.PREFIXER : '') + n;
4521
4595
  }
4522
4596
 
4523
4597
  get clusterName() {
4524
- // Returns full cluster name, i.e., prefix plus actor name if specified
4598
+ // Return full cluster name, i.e., prefix plus actor name if specified.
4525
4599
  if(this.actor_name) return `${this.prefix} (${this.actor_name})`;
4526
4600
  return this.prefix;
4527
4601
  }
4528
4602
 
4529
4603
  get parameterTable() {
4530
- // Returns the HTML for the parameter binding table in the include dialog
4604
+ // Return the HTML for the parameter binding table in the include dialog.
4531
4605
  if(Object.keys(this.bindings).length === 0) {
4532
4606
  return '<div style="margin-top:2px"><em>This module has no parameters.</em></div>';
4533
4607
  }
@@ -4540,9 +4614,9 @@ class IOContext {
4540
4614
  }
4541
4615
 
4542
4616
  bindParameters() {
4543
- // Binds parameters as specified in the INCLUDE MODULE dialog
4617
+ // Bind parameters as specified in the INCLUDE MODULE dialog.
4544
4618
  const pref = (this.prefix ? this.prefix + UI.PREFIXER : '');
4545
- // Compute sum of (x, y) of imported products
4619
+ // Compute sum of (x, y) of imported products.
4546
4620
  let np = 0,
4547
4621
  x = 0,
4548
4622
  y = 0,
@@ -4552,17 +4626,17 @@ class IOContext {
4552
4626
  for(let id in this.bindings) if(this.bindings.hasOwnProperty(id)) {
4553
4627
  const b = this.bindings[id];
4554
4628
  if(b.io_type === 1) {
4555
- // Get the selector for this parameter
4629
+ // Get the selector for this parameter.
4556
4630
  // NOTE: IO_CONTEXT is instantiated *exclusively* by the Repository
4557
- // browser, so that GUI dialog will exist when IO_CONTEXT is not NULL
4631
+ // browser, so that GUI dialog will exist when IO_CONTEXT is not NULL.
4558
4632
  const e = REPOSITORY_BROWSER.parameterBinding(b.id);
4559
4633
  if(e && e.selectedIndex >= 0) {
4560
- // Modeler has selected the actual parameter => set its name
4634
+ // Modeler has selected the actual parameter => set its name.
4561
4635
  const v = e.options[e.selectedIndex].value;
4562
4636
  if(v !== '_CLUSTER') {
4563
4637
  b.actual_name = e.options[e.selectedIndex].text;
4564
4638
  b.actual_id = v;
4565
- // If imported product, add its (x, y) to the centroid (x, y)
4639
+ // If imported product, add its (x, y) to the centroid (x, y).
4566
4640
  if(b.entity_type === 'Product') {
4567
4641
  const p = MODEL.products[v];
4568
4642
  if(p) {
@@ -4583,13 +4657,13 @@ class IOContext {
4583
4657
  }
4584
4658
  }
4585
4659
  if(b.actual_id === '') {
4586
- // By default, bind import parameter to itself (create a local entity)
4660
+ // By default, bind import parameter to itself (create a local entity).
4587
4661
  b.actual_name = pref + b.name_in_module;
4588
4662
  b.actual_id = UI.nameToID(b.actual_name);
4589
4663
  }
4590
4664
  }
4591
4665
  }
4592
- // NOTE: calculate centroid of non-data products if possible
4666
+ // NOTE: Calculate centroid of non-data products if possible.
4593
4667
  if(np > 1) {
4594
4668
  this.centroid_x = Math.round(x / np);
4595
4669
  this.centroid_y = Math.round(y / np);
@@ -4600,7 +4674,7 @@ class IOContext {
4600
4674
  this.centroid_x = Math.round(x + dx + 50);
4601
4675
  this.centroid_y = Math.round(y + dy + 50);
4602
4676
  } else {
4603
- // Position new cluster in upper-left quadrant of view
4677
+ // Position new cluster in upper-left quadrant of view.
4604
4678
  const cp = UI.pointInViewport(0.25, 0.25);
4605
4679
  this.centroid_x = cp[0];
4606
4680
  this.centroid_y = cp[1];
@@ -5058,38 +5132,41 @@ class ObjectWithXYWH {
5058
5132
 
5059
5133
  // CLASS NoteField: numeric value of "field" [[variable]] in note text
5060
5134
  class NoteField {
5061
- constructor(n, f, o, u='1', m=1, w=false) {
5135
+ constructor(n, f, o, u='1', m=1, w=false, p='') {
5062
5136
  // `n` is the note that "owns" this note field
5063
5137
  // `f` holds the unmodified tag string [[dataset]] to be replaced by
5064
5138
  // the value of vector or expression `o` for the current time step;
5065
5139
  // if specified, `u` is the unit of the value to be displayed,
5066
- // `m` is the multiplier for the value to be displayed, and `w` is
5067
- // the wildcard number to use in a wildcard equation
5140
+ // `m` is the multiplier for the value to be displayed, `w` is
5141
+ // the wildcard number to use in a wildcard equation, and `p` is
5142
+ // the prefix to use for a method equation.
5068
5143
  this.note = n;
5069
5144
  this.field = f;
5070
5145
  this.object = o;
5071
5146
  this.unit = u;
5072
5147
  this.multiplier = m;
5073
5148
  this.wildcard_number = (w ? parseInt(w) : false);
5149
+ this.prefix = p;
5074
5150
  }
5075
5151
 
5076
5152
  get value() {
5077
- // Returns the numeric value of this note field as a numeric string
5078
- // followed by its unit (unless this is 1)
5079
- // If object is the note, this means field [[#]] (note number context)
5080
- // If this is undefined (empty string) display a double question mark
5153
+ // Return the numeric value of this note field as a numeric string
5154
+ // followed by its unit (unless this is 1).
5155
+ // If object is the note, this means field [[#]] (note number context).
5156
+ // If this is undefined (empty string) display a double question mark.
5081
5157
  if(this.object === this.note) return this.note.numberContext || '\u2047';
5082
5158
  let v = VM.UNDEFINED;
5083
5159
  const t = MODEL.t;
5084
5160
  if(Array.isArray(this.object)) {
5085
- // Object is a vector
5161
+ // Object is a vector.
5086
5162
  if(t < this.object.length) v = this.object[t];
5087
5163
  } else if(this.object.hasOwnProperty('c') &&
5088
5164
  this.object.hasOwnProperty('u')) {
5089
- // Object holds link lists for cluster balance computation
5165
+ // Object holds link lists for cluster balance computation.
5090
5166
  v = MODEL.flowBalance(this.object, t);
5091
5167
  } else if(this.object instanceof Expression) {
5092
- // Object is an expression
5168
+ // Object is an expression.
5169
+ this.object.method_object_prefix = this.prefix;
5093
5170
  v = this.object.result(t, this.wildcard_number);
5094
5171
  } else if(typeof this.object === 'number') {
5095
5172
  v = this.object;
@@ -5114,7 +5191,7 @@ class Note extends ObjectWithXYWH {
5114
5191
  constructor(cluster) {
5115
5192
  super(cluster);
5116
5193
  const dt = new Date();
5117
- // NOTE: use timestamp in msec to generate a unique identifier
5194
+ // NOTE: Use timestamp in msec to generate a unique identifier.
5118
5195
  this.timestamp = dt.getTime();
5119
5196
  this.contents = '';
5120
5197
  this.lines = [];
@@ -5132,7 +5209,7 @@ class Note extends ObjectWithXYWH {
5132
5209
  }
5133
5210
 
5134
5211
  get clusterPrefix() {
5135
- // Returns the name of the cluster containing this note, followed
5212
+ // Return the name of the cluster containing this note, followed
5136
5213
  // by a colon+space, except when this cluster is the top cluster.
5137
5214
  if(this.cluster === MODEL.top_cluster) return '';
5138
5215
  return this.cluster.displayName + UI.PREFIXER;
@@ -5146,8 +5223,8 @@ class Note extends ObjectWithXYWH {
5146
5223
  }
5147
5224
 
5148
5225
  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.
5226
+ // Return the number of this note if specified (e.g. as #123).
5227
+ // NOTE: This only applies to notes having note fields.
5151
5228
  const m = this.contents.replace(/\s+/g, ' ')
5152
5229
  .match(/^[^\]]*#(\d+).*\[\[[^\]]+\]\]/);
5153
5230
  if(m) return m[1];
@@ -5155,7 +5232,7 @@ class Note extends ObjectWithXYWH {
5155
5232
  }
5156
5233
 
5157
5234
  get numberContext() {
5158
- // Returns the string to be used to evaluate #. For notes this is
5235
+ // Return the string to be used to evaluate #. For notes, this is
5159
5236
  // their note number if specified, otherwise the number context of a
5160
5237
  // nearby node, and otherwise the number context of their cluster.
5161
5238
  let n = this.number;
@@ -5165,8 +5242,16 @@ class Note extends ObjectWithXYWH {
5165
5242
  return this.cluster.numberContext;
5166
5243
  }
5167
5244
 
5245
+ get methodPrefix() {
5246
+ // Return the most likely candidate prefix to be used for method fields.
5247
+ const n = this.nearbyNode;
5248
+ if(n instanceof Cluster) return n.name;
5249
+ if(this.cluster === MODEL.top_cluster) return '';
5250
+ return this.cluster.name;
5251
+ }
5252
+
5168
5253
  get nearbyNode() {
5169
- // Returns a node in the cluster of this note that is closest to this
5254
+ // Return a node in the cluster of this note that is closest to this
5170
5255
  // note (Euclidian distance between center points), but with at most
5171
5256
  // 30 pixel units between their rims.
5172
5257
  const
@@ -5317,12 +5402,22 @@ class Note extends ObjectWithXYWH {
5317
5402
  if(!obj) {
5318
5403
  const m = MODEL.equations_dataset.modifiers[UI.nameToID(ena[0])];
5319
5404
  if(m) {
5320
- UI.warn('Methods cannot be evaluated without prefix');
5405
+ const mp = this.methodPrefix;
5406
+ if(mp) {
5407
+ if(m.expression.isEligible(mp)) {
5408
+ this.fields.push(new NoteField(this, tag, m.expression, to_unit,
5409
+ multiplier, false, mp));
5410
+ } else {
5411
+ UI.warn(`Prefix "${mp}" is not eligible for method "${m.selector}"`);
5412
+ }
5413
+ } else {
5414
+ UI.warn('Methods cannot be evaluated without prefix');
5415
+ }
5321
5416
  } else {
5322
5417
  UI.warn(`Unknown model entity "${en}"`);
5323
5418
  }
5324
5419
  } else if(obj instanceof DatasetModifier) {
5325
- // NOTE: equations are (for now) dimenssonless => unit '1'.
5420
+ // NOTE: Equations are (for now) dimensionless => unit '1'.
5326
5421
  if(obj.dataset !== MODEL.equations_dataset) {
5327
5422
  from_unit = obj.dataset.scale_unit;
5328
5423
  multiplier = MODEL.unitConversionMultiplier(from_unit, to_unit);
@@ -5585,14 +5680,13 @@ class NodeBox extends ObjectWithXYWH {
5585
5680
  n = `<em>${this.type}:</em> ${n}`;
5586
5681
  // For clusters, add how many processes and products they contain.
5587
5682
  if(this instanceof Cluster) {
5588
- let d = '';
5683
+ let dl = [];
5589
5684
  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();
5685
+ dl.push(pluralS(this.all_processes.length, 'process').toLowerCase());
5686
+ dl.push(pluralS(this.all_products.length, 'product').toLowerCase());
5594
5687
  }
5595
- if(d) n += `<span class="node-details">${d}</span>`;
5688
+ if(this.module) dl.push(`included from <span class="mod-name">${this.module.name}</span>`);
5689
+ if(dl.length) n += `<span class="node-details">${dl.join(', ')}</span>`;
5596
5690
  }
5597
5691
  if(!MODEL.solved) return n;
5598
5692
  const g = this.grid;
@@ -6055,11 +6149,11 @@ class Arrow {
6055
6149
 
6056
6150
  } // END of class Arrow
6057
6151
 
6058
-
6059
6152
  // CLASS Cluster
6060
6153
  class Cluster extends NodeBox {
6061
6154
  constructor(cluster, name, actor) {
6062
6155
  super(cluster, name, actor);
6156
+ this.module = null;
6063
6157
  this.processes = [];
6064
6158
  this.product_positions = [];
6065
6159
  this.sub_clusters = [];
@@ -6170,6 +6264,16 @@ class Cluster extends NodeBox {
6170
6264
  // Clusters have no attribute expressions => always return null.
6171
6265
  return null;
6172
6266
  }
6267
+
6268
+ get moduleAsXML() {
6269
+ if(!this.module) return '';
6270
+ const xml = ['<module name="', xmlEncoded(this.module.name), '">'];
6271
+ for(const k of Object.keys(this.module.bindings)) {
6272
+ xml.push(this.module.bindings[k].asXML);
6273
+ }
6274
+ xml.push('</module>');
6275
+ return xml.join('');
6276
+ }
6173
6277
 
6174
6278
  get asXML() {
6175
6279
  let xml;
@@ -6181,7 +6285,8 @@ class Cluster extends NodeBox {
6181
6285
  (this.toBeBlackBoxed ? ' is-black-boxed="1"' : '');
6182
6286
  xml = ['<cluster', flags, '><name>', xmlEncoded(this.blackBoxName),
6183
6287
  '</name><owner>', xmlEncoded(this.actor.name),
6184
- '</owner><x-coord>', this.x,
6288
+ '</owner>', this.moduleAsXML,
6289
+ '<x-coord>', this.x,
6185
6290
  '</x-coord><y-coord>', this.y,
6186
6291
  '</y-coord><comments>', cmnts,
6187
6292
  '</comments><process-set>'].join('');
@@ -6220,18 +6325,34 @@ class Cluster extends NodeBox {
6220
6325
  this.black_box = nodeParameterValue(node, 'black-box') === '1';
6221
6326
  this.is_black_boxed = nodeParameterValue(node, 'is-black-boxed') === '1';
6222
6327
 
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
6328
  let name,
6227
6329
  actor,
6228
- n = childNodeByTag(node, 'process-set');
6330
+ n = childNodeByTag(node, 'module');
6331
+ if(n) {
6332
+ this.module = {
6333
+ name: xmlDecoded(nodeParameterValue(n, 'name')),
6334
+ bindings: {}
6335
+ };
6336
+ for(const c of n.childNodes) if(c.nodeName === 'iob') {
6337
+ const
6338
+ iot = parseInt(nodeParameterValue(c, 'type')),
6339
+ et = capitalized(VM.entity_names[nodeParameterValue(c, 'entity')]),
6340
+ iob = new IOBinding(iot, et,
6341
+ nodeParameterValue(c, 'data') === '1',
6342
+ xmlDecoded(nodeParameterValue(c, 'name')));
6343
+ iob.actual_name = nodeContent(c);
6344
+ this.module.bindings[iob.id] = iob;
6345
+ }
6346
+ }
6347
+ n = childNodeByTag(node, 'process-set');
6348
+ // NOTE: To compensate for a shameful bug in an earlier version, look
6349
+ // for "product-positions" node and for "notes" node in the process-set,
6350
+ // as it may have been put there instead of in the cluster node itself.
6229
6351
  const
6230
6352
  hidden_pp = childNodeByTag(n, 'product-positions'),
6231
6353
  hidden_notes = childNodeByTag(n, 'notes');
6232
- // (if they exist, these nodes will be used a bit further down)
6233
-
6234
- if(n && n.childNodes) {
6354
+ // If they exist, these nodes will be used a bit further down.
6355
+ if(n) {
6235
6356
  for(const c of n.childNodes) if(c.nodeName === 'process-name') {
6236
6357
  name = xmlDecoded(nodeContent(c));
6237
6358
  if(IO_CONTEXT) {
@@ -6257,7 +6378,7 @@ class Cluster extends NodeBox {
6257
6378
  }
6258
6379
  }
6259
6380
  n = childNodeByTag(node, 'sub-clusters');
6260
- if(n && n.childNodes) {
6381
+ if(n) {
6261
6382
  for(const c of n.childNodes) if(c.nodeName === 'cluster') {
6262
6383
  // Refocus on this cluster because addCluster may change focus if it
6263
6384
  // contains subclusters.
@@ -6275,7 +6396,7 @@ class Cluster extends NodeBox {
6275
6396
  }
6276
6397
  // NOTE: the part " || hidden_pp" is to compensate for a bug -- see earlier note.
6277
6398
  n = childNodeByTag(node, 'product-positions') || hidden_pp;
6278
- if(n && n.childNodes) {
6399
+ if(n) {
6279
6400
  for(const c of n.childNodes) if(c.nodeName === 'product-position') {
6280
6401
  name = xmlDecoded(nodeContentByTag(c, 'product-name'));
6281
6402
  if(IO_CONTEXT) name = IO_CONTEXT.actualName(name);
@@ -6284,7 +6405,7 @@ class Cluster extends NodeBox {
6284
6405
  }
6285
6406
  }
6286
6407
  n = childNodeByTag(node, 'notes') || hidden_notes;
6287
- if(n && n.childNodes) {
6408
+ if(n) {
6288
6409
  for(const c of n.childNodes) if(c.nodeName === 'note') {
6289
6410
  const note = new Note(this);
6290
6411
  note.initFromXML(c);
@@ -6696,35 +6817,6 @@ class Cluster extends NodeBox {
6696
6817
  }
6697
6818
  }
6698
6819
 
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
6820
  // If P and Q are both processes, while either one is not visible,
6729
6821
  // the arrow will be unique (as each process is in only ONE cluster)
6730
6822
  // and connect either a process node to a cluster node, or two
@@ -6855,28 +6947,29 @@ class Cluster extends NodeBox {
6855
6947
  deleteProduct(p, with_xml=true) {
6856
6948
  // Remove "placeholder" of product `p` from this cluster, and
6857
6949
  // remove `p` from the model if there are no other clusters
6858
- // containing a "placeholder" for `p`
6950
+ // containing a "placeholder" for `p`.
6859
6951
  // Always set "selected" attribute to FALSE (or the product will
6860
- // still be drawn in red)
6952
+ // still be drawn in red).
6861
6953
  p.selected = false;
6862
6954
  let i = this.indexOfProduct(p);
6863
6955
  if(i < 0) return false;
6864
6956
  // Append XML for product positions unlesss deleting from a cluster
6865
- // that is being deleted
6957
+ // that is being deleted.
6866
6958
  if(with_xml) UNDO_STACK.addXML(this.product_positions[i].asXML);
6867
- // Remove product position of `p` in this cluster
6959
+ // Remove product position of `p` in this cluster.
6868
6960
  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
6961
+ // Do not delete product from this cluster if it has links to
6962
+ // processes in other clusters, of if this cluster is updating
6963
+ // and binds the product as parameter.
6964
+ if(!p.allLinksInCluster(this) || (IO_CONTEXT && IO_CONTEXT.isBinding(p))) {
6965
+ // NOTE: Removing only the product position DOES affect the
6966
+ // diagram, so prepare for redraw.
6874
6967
  this.clearAllProcesses();
6875
6968
  return false;
6876
6969
  }
6877
6970
  // 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
6971
+ // (incl. all links to and from `p`). NOTE: Such deletions WILL
6972
+ // append their undo XML.
6880
6973
  MODEL.deleteNode(p);
6881
6974
  return true;
6882
6975
  }
@@ -9513,7 +9606,7 @@ class Dataset {
9513
9606
  this.unpackDataString(xmlDecoded(nodeContentByTag(node, 'data')));
9514
9607
  }
9515
9608
  const n = childNodeByTag(node, 'modifiers');
9516
- if(n && n.childNodes) {
9609
+ if(n) {
9517
9610
  for(const c of n.childNodes) if(c.nodeName === 'modifier') {
9518
9611
  this.addModifier(xmlDecoded(nodeContentByTag(c, 'selector')), c);
9519
9612
  }
@@ -9636,6 +9729,12 @@ class ChartVariable {
9636
9729
  this.sorted = sort;
9637
9730
  }
9638
9731
 
9732
+ get type() {
9733
+ // NOTE: Charts are not entities, but the dialogs may inquire their type
9734
+ // for sorting and presentation (e.g., to determine icon name).
9735
+ return 'Chart';
9736
+ }
9737
+
9639
9738
  get displayName() {
9640
9739
  // Returns the display name for this variable. This is the name of
9641
9740
  // the Linny-R entity and its attribute, followed by its scale factor
@@ -9650,21 +9749,13 @@ class ChartVariable {
9650
9749
  // this indicates that it is a Wildcard selector or a method, and
9651
9750
  // that the specified result vector should be used.
9652
9751
  if(this.wildcard_index !== false) {
9653
- // NOTE: A wildcard index (a number) can also indicate that this
9654
- // variable is a method, so check for a leading colon.
9655
- if(eqn.startsWith(':')) {
9656
- // For methods, use "entity name or prefix: method" as variable
9657
- // name, so first get the method object prefix, expand it if
9658
- // it identifies a specific model entity, and then append the
9659
- // method name (leading colon replaced by the prefixer ": ").
9660
- const
9661
- mop = this.object.expression.method_object_list[this.wildcard_index],
9662
- obj = MODEL.objectByID(mop);
9663
- eqn = (obj ? obj.displayName : (mop || '')) +
9664
- UI.PREFIXER + eqn.substring(1);
9665
- } else {
9666
- eqn = eqn.replace('??', this.wildcard_index);
9667
- }
9752
+ eqn = eqn.replace('??', this.wildcard_index);
9753
+ } else if(eqn.startsWith(':')) {
9754
+ // For methods, use "entity name or prefix: method" as variable
9755
+ // name, so first get the method object prefix, expand it if
9756
+ // it identifies a specific model entity, and then append the
9757
+ // method name (leading colon replaced by the prefixer ": ").
9758
+ eqn = this.chart.prefix + UI.PREFIXER + eqn.substring(1);
9668
9759
  }
9669
9760
  return eqn + sf;
9670
9761
  }
@@ -9683,8 +9774,8 @@ class ChartVariable {
9683
9774
  }
9684
9775
 
9685
9776
  get asXML() {
9686
- // NOTE: a "black-boxed" model can comprise charts showing "anonymous"
9687
- // entities, so the IDs of these entities must then be changed
9777
+ // NOTE: A "black-boxed" model can comprise charts showing "anonymous"
9778
+ // entities, so the IDs of these entities must then be changed.
9688
9779
  let id = this.object.identifier;
9689
9780
  if(MODEL.black_box_entities.hasOwnProperty(id)) {
9690
9781
  id = UI.nameToID(MODEL.black_box_entities[id]);
@@ -9704,7 +9795,7 @@ class ChartVariable {
9704
9795
  }
9705
9796
 
9706
9797
  get lowestValueInVector() {
9707
- // Returns the computed statistical minimum OR vector[0] (if valid & lower)
9798
+ // Return the computed statistical minimum OR vector[0] (if valid & lower).
9708
9799
  let v = this.minimum;
9709
9800
  if(this.vector.length > 0) v = this.vector[0];
9710
9801
  if(v < VM.MINUS_INFINITY || v > VM.PLUS_INFINITY || v > this.minimum) {
@@ -9714,7 +9805,7 @@ class ChartVariable {
9714
9805
  }
9715
9806
 
9716
9807
  get highestValueInVector() {
9717
- // Returns the computed statistical maximum OR vector[0] (if valid & higher)
9808
+ // Return the computed statistical maximum OR vector[0] (if valid & higher).
9718
9809
  let v = this.maximum;
9719
9810
  if(this.vector.length > 0) v = this.vector[0];
9720
9811
  if(v < VM.MINUS_INFINITY || v > VM.PLUS_INFINITY || v < this.maximum) {
@@ -9725,12 +9816,12 @@ class ChartVariable {
9725
9816
 
9726
9817
  initFromXML(node) {
9727
9818
  let id = xmlDecoded(nodeContentByTag(node, 'object-id'));
9728
- // NOTE: automatic conversion of former top cluster name
9819
+ // NOTE: Automatic conversion of former top cluster name.
9729
9820
  if(id === UI.FORMER_TOP_CLUSTER_NAME.toLowerCase()) {
9730
9821
  id = UI.nameToID(UI.TOP_CLUSTER_NAME);
9731
9822
  }
9732
9823
  if(IO_CONTEXT) {
9733
- // NOTE: actualName also works for entity IDs
9824
+ // NOTE: actualName also works for entity IDs.
9734
9825
  id = UI.nameToID(IO_CONTEXT.actualName(id));
9735
9826
  }
9736
9827
  const obj = MODEL.objectByID(id);
@@ -9771,7 +9862,7 @@ class ChartVariable {
9771
9862
  }
9772
9863
  // Compute vector and statistics only if vector is still empty.
9773
9864
  if(this.vector.length > 0) return;
9774
- // NOTE: expression vectors start at t = 0 with initial values that
9865
+ // NOTE: Expression vectors start at t = 0 with initial values that
9775
9866
  // should not be included in statistics.
9776
9867
  let v,
9777
9868
  av = null,
@@ -9792,7 +9883,7 @@ class ChartVariable {
9792
9883
  this.chart.time_scale, tsteps, 1);
9793
9884
  t_end = tsteps;
9794
9885
  } else {
9795
- // Get the variable's own value (number, vector or expression)
9886
+ // Get the variable's own value (number, vector or expression).
9796
9887
  if(this.object instanceof Dataset) {
9797
9888
  if(this.attribute) {
9798
9889
  av = this.object.attributeExpression(this.attribute);
@@ -9810,7 +9901,7 @@ class ChartVariable {
9810
9901
  }
9811
9902
  t_end = MODEL.end_period - MODEL.start_period + 1;
9812
9903
  }
9813
- // NOTE: when a chart combines run results with dataset vectors, the
9904
+ // NOTE: When a chart combines run results with dataset vectors, the
9814
9905
  // latter may be longer than the # of time steps displayed in the chart.
9815
9906
  t_end = Math.min(t_end, this.chart.total_time_steps);
9816
9907
  this.N = t_end;
@@ -9826,7 +9917,9 @@ class ChartVariable {
9826
9917
  } else if(av instanceof Expression) {
9827
9918
  // Attribute value is an expression. If this chart variable has
9828
9919
  // its wildcard vector index set, evaluate the expression with
9829
- // this index as context number.
9920
+ // this index as context number. Likewise, set the method object
9921
+ // prefix.
9922
+ av.method_object_prefix = this.chart.prefix;
9830
9923
  v = av.result(t, this.wildcard_index);
9831
9924
  } else {
9832
9925
  // Attribute value must be a number.
@@ -9964,7 +10057,8 @@ class Chart {
9964
10057
  this.value_range = 0;
9965
10058
  this.show_title = true;
9966
10059
  this.legend_position = 'none';
9967
- this.variables = [];
10060
+ this.preferred_prefix = '';
10061
+ this.variables = [];
9968
10062
  // SVG string to display the chart
9969
10063
  this.svg = '';
9970
10064
  // Properties of rectangular chart area
@@ -9976,8 +10070,35 @@ class Chart {
9976
10070
  }
9977
10071
 
9978
10072
  get displayName() {
10073
+ // Charts are identified by their title.
9979
10074
  return this.title;
9980
10075
  }
10076
+
10077
+ get prefix() {
10078
+ // The prefix is used to further specify method variables.
10079
+ if(this.preferred_prefix) return this.preferred_prefix;
10080
+ const parts = this.title.split(UI.PREFIXER);
10081
+ parts.pop();
10082
+ // Now `parts` is empty when the title contains no colon+space.
10083
+ return parts.join(UI.PREFIXER);
10084
+ }
10085
+
10086
+ get possiblePrefixes() {
10087
+ // Return list of prefixes that are eligible for all method variables.
10088
+ let pp = null;
10089
+ for(const v of this.variables) {
10090
+ if(v.object instanceof DatasetModifier && v.object.selector.startsWith(':')) {
10091
+ if(pp) {
10092
+ pp = intersection(pp, Object.keys(v.object.expression.eligible_prefixes));
10093
+ } else {
10094
+ pp = Object.keys(v.object.expression.eligible_prefixes);
10095
+ }
10096
+ }
10097
+ }
10098
+ if(pp) pp.sort();
10099
+ // Always return a list.
10100
+ return pp || [];
10101
+ }
9981
10102
 
9982
10103
  get asXML() {
9983
10104
  let xml = '';
@@ -10002,7 +10123,7 @@ class Chart {
10002
10123
  this.legend_position = nodeContentByTag(node, 'legend-position');
10003
10124
  this.variables.length = 0;
10004
10125
  const n = childNodeByTag(node, 'variables');
10005
- if(n && n.childNodes) {
10126
+ if(n) {
10006
10127
  for(const c of n.childNodes) if(c.nodeName === 'chart-variable') {
10007
10128
  const v = new ChartVariable(this);
10008
10129
  // NOTE: Variable may refer to deleted entity => do not add.
@@ -10049,10 +10170,10 @@ class Chart {
10049
10170
  const eq = obj instanceof DatasetModifier;
10050
10171
  // No equation and no attribute specified? Then assume default.
10051
10172
  if(!eq && a === '') a = obj.defaultAttribute;
10052
- if(eq && (n.indexOf('??') >= 0 || obj.expression.isMethod)) {
10053
- // Special case: for wildcard equations and methods, prompt the
10054
- // modeler which wildcard possibilities to add UNLESS this is an
10055
- // untitled "dummy" chart used to report outcomes.
10173
+ if(eq && n.indexOf('??') >= 0) {
10174
+ // Special case: for wildcard equations, prompt the modeler which
10175
+ // wildcard possibilities to add UNLESS this is an untitled "dummy" chart
10176
+ // used to report outcomes.
10056
10177
  if(this.title) {
10057
10178
  CHART_MANAGER.promptForWildcardIndices(this, obj);
10058
10179
  } else {
@@ -11424,13 +11545,13 @@ class ExperimentRun {
11424
11545
  this.time_steps = safeStrToInt(nodeContentByTag(node, 'time-steps'));
11425
11546
  this.time_step_duration = safeStrToFloat(nodeContentByTag(node, 'delta-t'));
11426
11547
  let n = childNodeByTag(node, 'results');
11427
- if(n && n.childNodes) {
11548
+ if(n) {
11428
11549
  for(const c of n.childNodes) if(c.nodeName === 'run-result') {
11429
11550
  this.results.push(new ExperimentRunResult(this, c));
11430
11551
  }
11431
11552
  }
11432
11553
  n = childNodeByTag(node, 'messages');
11433
- if(n && n.childNodes) {
11554
+ if(n) {
11434
11555
  for(const c of n.childNodes) if(c.nodeName === 'block-msg') {
11435
11556
  this.block_messages.push(new BlockMessages(c));
11436
11557
  }
@@ -11861,7 +11982,7 @@ class Experiment {
11861
11982
  this.title = xmlDecoded(nodeContentByTag(node, 'title'));
11862
11983
  this.comments = xmlDecoded(nodeContentByTag(node, 'notes'));
11863
11984
  let n = childNodeByTag(node, 'dimensions');
11864
- if(n && n.childNodes) {
11985
+ if(n) {
11865
11986
  for(const c of n.childNodes) if(c.nodeName === 'dim') {
11866
11987
  this.dimensions.push(xmlDecoded(nodeContent(c)).split(','));
11867
11988
  }
@@ -11875,7 +11996,7 @@ class Experiment {
11875
11996
  }
11876
11997
  }
11877
11998
  n = childNodeByTag(node, 'chart-titles');
11878
- if(n && n.childNodes) {
11999
+ if(n) {
11879
12000
  for(const c of n.childNodes) if(c.nodeName === 'chart-title') {
11880
12001
  const ci = MODEL.indexOfChart(xmlDecoded(nodeContent(c)));
11881
12002
  // Double-check: only add existing charts.
@@ -11883,31 +12004,31 @@ class Experiment {
11883
12004
  }
11884
12005
  }
11885
12006
  n = childNodeByTag(node, 'settings-selectors');
11886
- if(n && n.childNodes) {
12007
+ if(n) {
11887
12008
  for(const c of n.childNodes) if(c.nodeName === 'ssel') {
11888
12009
  this.settings_selectors.push(xmlDecoded(nodeContent(c)));
11889
12010
  }
11890
12011
  }
11891
12012
  n = childNodeByTag(node, 'settings-dimensions');
11892
- if(n && n.childNodes) {
12013
+ if(n) {
11893
12014
  for(const c of n.childNodes) if(c.nodeName === 'sdim') {
11894
12015
  this.settings_dimensions.push(xmlDecoded(nodeContent(c)).split(','));
11895
12016
  }
11896
12017
  }
11897
12018
  n = childNodeByTag(node, 'combination-selectors');
11898
- if(n && n.childNodes) {
12019
+ if(n) {
11899
12020
  for(const c of n.childNodes) if(c.nodeName === 'csel') {
11900
12021
  this.combination_selectors.push(xmlDecoded(nodeContent(c)));
11901
12022
  }
11902
12023
  }
11903
12024
  n = childNodeByTag(node, 'combination-dimensions');
11904
- if(n && n.childNodes) {
12025
+ if(n) {
11905
12026
  for(const c of n.childNodes) if(c.nodeName === 'cdim') {
11906
12027
  this.combination_dimensions.push(xmlDecoded(nodeContent(c)).split(','));
11907
12028
  }
11908
12029
  }
11909
12030
  n = childNodeByTag(node, 'actor-selectors');
11910
- if(n && n.childNodes) {
12031
+ if(n) {
11911
12032
  for(const c of n.childNodes) if(c.nodeName === 'asel') {
11912
12033
  const as = new ActorSelector();
11913
12034
  as.initFromXML(c);
@@ -11916,7 +12037,7 @@ class Experiment {
11916
12037
  }
11917
12038
  this.excluded_selectors = xmlDecoded(nodeContentByTag(node, 'excluded-selectors'));
11918
12039
  n = childNodeByTag(node, 'clusters-to-ignore');
11919
- if(n && n.childNodes) {
12040
+ if(n) {
11920
12041
  for(const c of n.childNodes) if(c.nodeName === 'cluster-to-ignore') {
11921
12042
  const
11922
12043
  cdn = xmlDecoded(nodeContentByTag(c, 'cluster')),
@@ -11931,7 +12052,7 @@ class Experiment {
11931
12052
  }
11932
12053
  }
11933
12054
  n = childNodeByTag(node, 'runs');
11934
- if(n && n.childNodes) {
12055
+ if(n) {
11935
12056
  let r = 0;
11936
12057
  for(const c of n.childNodes) if(c.nodeName === 'experiment-run') {
11937
12058
  const xr = new ExperimentRun(this, r);
@@ -12360,7 +12481,7 @@ class Experiment {
12360
12481
  const rr = this.runs[rnr].results[vi];
12361
12482
  if(rr) {
12362
12483
  // NOTE: Only experiment variables have vector data.
12363
- if(rr.x_variable && i <= rr.N) {
12484
+ if(rr.x_variable && t <= rr.N) {
12364
12485
  row.push(numval(rr.vector[t], prec));
12365
12486
  } else {
12366
12487
  row.push('');
@@ -12685,7 +12806,7 @@ class BoundLine {
12685
12806
  xmlDecoded(nodeContentByTag(node, 'point-data')));
12686
12807
  }
12687
12808
  const n = childNodeByTag(node, 'selectors');
12688
- if(n && n.childNodes && n.childNodes.length) {
12809
+ if(n.length) {
12689
12810
  // NOTE: Only overwrite default selector if XML specifies selectors.
12690
12811
  this.selectors.length = 0;
12691
12812
  for(const c of n.childNodes) if(c.nodeName === 'boundline-selector') {
@@ -12990,7 +13111,7 @@ class Constraint {
12990
13111
  this.soc_direction = safeStrToInt(
12991
13112
  nodeParameterValue(node, 'soc-direction'), 1);
12992
13113
  const n = childNodeByTag(node, 'bound-lines');
12993
- if(n && n.childNodes) {
13114
+ if(n) {
12994
13115
  // NOTE: only overwrite default lines if XML specifies bound lines
12995
13116
  this.bound_lines.length = 0;
12996
13117
  for(const c of n.childNodes) if(c.nodeName === 'bound-line') {