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.
- package/package.json +1 -1
- package/static/images/update.png +0 -0
- package/static/index.html +34 -3
- package/static/linny-r.css +42 -3
- package/static/scripts/linny-r-ctrl.js +6 -0
- package/static/scripts/linny-r-gui-actor-manager.js +6 -6
- package/static/scripts/linny-r-gui-chart-manager.js +54 -32
- package/static/scripts/linny-r-gui-controller.js +52 -35
- package/static/scripts/linny-r-gui-dataset-manager.js +7 -16
- package/static/scripts/linny-r-gui-monitor.js +3 -2
- package/static/scripts/linny-r-gui-paper.js +10 -1
- package/static/scripts/linny-r-gui-repository-browser.js +304 -17
- package/static/scripts/linny-r-gui-undo-redo.js +41 -52
- package/static/scripts/linny-r-model.js +429 -308
- package/static/scripts/linny-r-utils.js +21 -23
- package/static/scripts/linny-r-vm.js +103 -17
@@ -441,9 +441,9 @@ class LinnyRModel {
|
|
441
441
|
}
|
442
442
|
|
443
443
|
namedObjectByID(id) {
|
444
|
-
// NOTE:
|
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
|
-
|
531
|
-
if(type === '
|
532
|
-
if(type === '
|
533
|
-
|
534
|
-
|
535
|
-
if(type === '
|
536
|
-
if(type === '
|
537
|
-
if(type === '
|
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:
|
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
|
-
|
1773
|
-
if(
|
1774
|
-
this.selection.splice(
|
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
|
2390
|
-
if(
|
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
|
-
|
2414
|
-
if(
|
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
|
-
|
2417
|
-
if(
|
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
|
-
|
2471
|
-
if(
|
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
|
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
|
2810
|
-
for(
|
2811
|
-
c
|
2812
|
-
|
2813
|
-
|
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
|
2822
|
-
for(
|
2823
|
-
c
|
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
|
2832
|
-
for(
|
2833
|
-
|
2834
|
-
if(
|
2835
|
-
|
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
|
2844
|
-
for(
|
2845
|
-
|
2846
|
-
|
2847
|
-
|
2848
|
-
actor =
|
2849
|
-
|
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
|
2860
|
-
for(
|
2861
|
-
|
2862
|
-
if(
|
2863
|
-
|
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
|
2872
|
-
for(
|
2873
|
-
|
2874
|
-
|
2875
|
-
|
2876
|
-
actor =
|
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
|
-
|
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
|
2900
|
-
for(
|
2901
|
-
|
2902
|
-
|
2903
|
-
|
2904
|
-
actor =
|
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
|
-
|
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
|
2927
|
-
for(
|
2928
|
-
|
2929
|
-
|
2930
|
-
|
2931
|
-
|
2932
|
-
|
2933
|
-
|
2934
|
-
|
2935
|
-
|
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
|
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
|
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:
|
3031
|
+
// NOTE: When including a model, charts MUST be prefixed.
|
2984
3032
|
n = childNodeByTag(node, 'charts');
|
2985
|
-
if(n
|
2986
|
-
for(
|
2987
|
-
|
2988
|
-
if(
|
2989
|
-
|
2990
|
-
|
2991
|
-
|
2992
|
-
|
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
|
3017
|
-
for(
|
3018
|
-
c
|
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
|
3026
|
-
for(
|
3027
|
-
c
|
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
|
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
|
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
|
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
|
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
|
-
|
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
|
3078
|
-
l = this.links[
|
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
|
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
|
-
//
|
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
|
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
|
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
|
-
//
|
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
|
-
//
|
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:
|
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
|
-
//
|
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
|
-
//
|
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
|
-
//
|
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:
|
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,
|
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
|
-
//
|
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:
|
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
|
-
//
|
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
|
-
//
|
5150
|
-
// NOTE:
|
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
|
-
//
|
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
|
-
//
|
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
|
-
|
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:
|
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
|
5683
|
+
let dl = [];
|
5589
5684
|
if(this.all_processes) {
|
5590
|
-
|
5591
|
-
dl.push(pluralS(this.
|
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(
|
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
|
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, '
|
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
|
-
//
|
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
|
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
|
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
|
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
|
6870
|
-
// processes in other clusters
|
6871
|
-
|
6872
|
-
|
6873
|
-
//
|
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:
|
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
|
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
|
-
|
9654
|
-
|
9655
|
-
|
9656
|
-
|
9657
|
-
|
9658
|
-
|
9659
|
-
|
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:
|
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
|
-
//
|
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
|
-
//
|
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:
|
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:
|
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:
|
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.
|
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
|
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 &&
|
10053
|
-
// Special case: for wildcard equations
|
10054
|
-
//
|
10055
|
-
//
|
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
|
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
|
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
|
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
|
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
|
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
|
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
|
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
|
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
|
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
|
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
|
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 &&
|
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
|
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
|
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') {
|