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