linny-r 1.3.4 → 1.4.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/index.html +6 -12
- package/static/linny-r.css +28 -2
- package/static/scripts/linny-r-ctrl.js +40 -8
- package/static/scripts/linny-r-gui.js +124 -65
- package/static/scripts/linny-r-model.js +474 -143
- package/static/scripts/linny-r-utils.js +134 -12
- package/static/scripts/linny-r-vm.js +806 -390
@@ -328,6 +328,31 @@ class LinnyRModel {
|
|
328
328
|
return null;
|
329
329
|
}
|
330
330
|
|
331
|
+
wildcardEquationByID(id) {
|
332
|
+
// Returns the tuple [dataset modifier, number] holding the first
|
333
|
+
// wildcard equation for which the ID (e.g., "abc ??") matches with
|
334
|
+
// `id`, or NULL if no match is found.
|
335
|
+
// NOTE: `id` must contain a number, not a wildcard.
|
336
|
+
if(!this.equations_dataset) return null;
|
337
|
+
const ids = Object.keys(this.equations_dataset.modifiers);
|
338
|
+
for(let i = 0; i < ids.length; i++) {
|
339
|
+
// Skip the modifier ID is identical to `id` (see NOTE above).
|
340
|
+
if(ids[i] !== id) {
|
341
|
+
const re = wildcardMatchRegex(ids[i], true);
|
342
|
+
if(re) {
|
343
|
+
const m = [...id.matchAll(re)];
|
344
|
+
if(m.length > 0) {
|
345
|
+
const n = parseInt(m[0][1]);
|
346
|
+
if(n || n === 0) {
|
347
|
+
return [this.equations_dataset.modifiers[ids[i]], n];
|
348
|
+
}
|
349
|
+
}
|
350
|
+
}
|
351
|
+
}
|
352
|
+
}
|
353
|
+
return null;
|
354
|
+
}
|
355
|
+
|
331
356
|
namedObjectByID(id) {
|
332
357
|
// NOTE: not only entities, but also equations are "named objects", meaning
|
333
358
|
// that their name must be unique in a model (unlike the titles of charts
|
@@ -361,8 +386,8 @@ class LinnyRModel {
|
|
361
386
|
}
|
362
387
|
|
363
388
|
objectByName(name) {
|
364
|
-
// Looks up a named object based on its display name
|
365
|
-
// NOTE:
|
389
|
+
// Looks up a named object based on its display name.
|
390
|
+
// NOTE: Top cluster is uniquely identified by its name.
|
366
391
|
if(name === UI.TOP_CLUSTER_NAME || name === UI.FORMER_TOP_CLUSTER_NAME) {
|
367
392
|
return this.clusters[UI.nameToID(UI.TOP_CLUSTER_NAME)];
|
368
393
|
}
|
@@ -389,7 +414,7 @@ class LinnyRModel {
|
|
389
414
|
// No link? then standard conversion to ID
|
390
415
|
return this.namedObjectByID(UI.nameToID(name));
|
391
416
|
}
|
392
|
-
|
417
|
+
|
393
418
|
setByType(type) {
|
394
419
|
// Returns a "dictionary" object with entities of the specified types
|
395
420
|
if(type === 'Process') return this.processes;
|
@@ -450,6 +475,46 @@ class LinnyRModel {
|
|
450
475
|
return list;
|
451
476
|
}
|
452
477
|
|
478
|
+
allMatchingEntities(re, attr='') {
|
479
|
+
// NOTE: this routine is computationally intensive as it performs matches
|
480
|
+
// on the display names of entities while iterating over all relevant sets
|
481
|
+
const
|
482
|
+
me = [],
|
483
|
+
res = re.toString();
|
484
|
+
|
485
|
+
function scan(dict) {
|
486
|
+
// Try to match all entities in `dict`
|
487
|
+
for(let k in dict) if(dict.hasOwnProperty(k)) {
|
488
|
+
const
|
489
|
+
e = dict[k],
|
490
|
+
m = [...e.displayName.matchAll(re)];
|
491
|
+
if(m.length > 0) {
|
492
|
+
// If matches, ensure that the groups have identical values
|
493
|
+
const n = parseInt(m[0][1]);
|
494
|
+
let same = true;
|
495
|
+
for(let i = 1; same && i < m.length; i++) {
|
496
|
+
same = parseInt(m[i][1]) === n;
|
497
|
+
}
|
498
|
+
// If so, add the entity to the set
|
499
|
+
if(same) me.push(e);
|
500
|
+
}
|
501
|
+
}
|
502
|
+
}
|
503
|
+
|
504
|
+
// Links limit the search (constraints have no attributes => skip)
|
505
|
+
if(res.indexOf(UI.LINK_ARROW) >= 0) {
|
506
|
+
scan(this.links);
|
507
|
+
} else {
|
508
|
+
if(!attr || VM.actor_attr.indexOf(attr) >= 0) scan(this.actors);
|
509
|
+
if(!attr || VM.cluster_attr.indexOf(attr) >= 0) scan(this.clusters);
|
510
|
+
if(!attr) scan(this.datasets);
|
511
|
+
if(!attr || VM.process_attr.indexOf(attr) >= 0) scan(this.processes);
|
512
|
+
if(!attr || VM.product_attr.indexOf(attr) >= 0) scan(this.products);
|
513
|
+
if(!attr && this.equations_dataset) scan(this.equations_dataset.modifiers);
|
514
|
+
}
|
515
|
+
return me;
|
516
|
+
}
|
517
|
+
|
453
518
|
get clustersToIgnore() {
|
454
519
|
// Returns a "dictionary" with all clusters that are to be ignored
|
455
520
|
const cti = {};
|
@@ -1221,17 +1286,17 @@ class LinnyRModel {
|
|
1221
1286
|
if(name === UI.EQUATIONS_DATASET_NAME) {
|
1222
1287
|
// When including a module, the current equations must be saved,
|
1223
1288
|
// then the newly parsed dataset must have its modifiers prefixed,
|
1224
|
-
// and then be merged with the original equations dataset
|
1289
|
+
// and then be merged with the original equations dataset.
|
1225
1290
|
if(IO_CONTEXT) eqds = this.equations_dataset;
|
1226
1291
|
// When equations dataset is added, recognize it as such, or its
|
1227
|
-
// modifier selectors may be rejected while initializing from XML
|
1292
|
+
// modifier selectors may be rejected while initializing from XML.
|
1228
1293
|
this.equations_dataset = d;
|
1229
1294
|
}
|
1230
1295
|
if(node) d.initFromXML(node);
|
1231
1296
|
if(eqds) {
|
1232
|
-
// Restore pointer to original equations dataset
|
1297
|
+
// Restore pointer to original equations dataset.
|
1233
1298
|
this.equations_dataset = eqds;
|
1234
|
-
// Return the extended equations dataset
|
1299
|
+
// Return the extended equations dataset.
|
1235
1300
|
return eqds;
|
1236
1301
|
} else {
|
1237
1302
|
this.datasets[id] = d;
|
@@ -1357,9 +1422,9 @@ class LinnyRModel {
|
|
1357
1422
|
if(!this.align_to_grid) return;
|
1358
1423
|
let move = false;
|
1359
1424
|
const fc = this.focal_cluster;
|
1360
|
-
|
1361
|
-
|
1362
|
-
|
1425
|
+
// NOTE: Do not align notes to the grid. This will permit more
|
1426
|
+
// precise positioning, while aligning will not improve the layout
|
1427
|
+
// of the diagram because notes are not connected to arrows.
|
1363
1428
|
for(let i = 0; i < fc.processes.length; i++) {
|
1364
1429
|
move = fc.processes[i].alignToGrid() || move;
|
1365
1430
|
}
|
@@ -1510,7 +1575,8 @@ class LinnyRModel {
|
|
1510
1575
|
}
|
1511
1576
|
|
1512
1577
|
get clusterOrProcessInSelection() {
|
1513
|
-
// Return TRUE if current selection contains at least one cluster
|
1578
|
+
// Return TRUE if current selection contains at least one cluster
|
1579
|
+
// or process.
|
1514
1580
|
for(let i = 0; i < this.selection.length; i++) {
|
1515
1581
|
const obj = this.selection[i];
|
1516
1582
|
if(obj instanceof Cluster || obj instanceof Process) return true;
|
@@ -1519,9 +1585,9 @@ class LinnyRModel {
|
|
1519
1585
|
}
|
1520
1586
|
|
1521
1587
|
moveSelection(dx, dy){
|
1522
|
-
// Move all selected nodes unless cursor was not moved
|
1523
|
-
// NOTE:
|
1524
|
-
// stored on MOUSE DOWN
|
1588
|
+
// Move all selected nodes unless cursor was not moved.
|
1589
|
+
// NOTE: No undo, as moves are incremental; the original positions
|
1590
|
+
// have been stored on MOUSE DOWN.
|
1525
1591
|
if(dx === 0 && dy === 0) return;
|
1526
1592
|
let obj,
|
1527
1593
|
minx = 0,
|
@@ -1587,6 +1653,23 @@ class LinnyRModel {
|
|
1587
1653
|
}
|
1588
1654
|
return true;
|
1589
1655
|
}
|
1656
|
+
|
1657
|
+
eligibleFromToNodes(type) {
|
1658
|
+
// Returns a list of nodes of given type (Process, Product or Data)
|
1659
|
+
// that are visible in the focal cluster
|
1660
|
+
const
|
1661
|
+
fc = this.focal_cluster,
|
1662
|
+
el = [];
|
1663
|
+
if(type === 'Process') {
|
1664
|
+
for(let i = 0; i < fc.processes.length; i++) el.push(fc.processes[i]);
|
1665
|
+
} else {
|
1666
|
+
for(let i = 0; i < fc.product_positions.length; i++) {
|
1667
|
+
const p = fc.product_positions[i].product;
|
1668
|
+
if((type === 'Data' && p.is_data) || !p.is_data) el.push(p);
|
1669
|
+
}
|
1670
|
+
}
|
1671
|
+
return el;
|
1672
|
+
}
|
1590
1673
|
|
1591
1674
|
get selectionAsXML() {
|
1592
1675
|
// Returns XML for the selected entities, and also for the entities
|
@@ -1649,7 +1732,7 @@ class LinnyRModel {
|
|
1649
1732
|
}
|
1650
1733
|
// Only add the XML for notes in the selection
|
1651
1734
|
for(let i = 0; i < entities.Note.length; i++) {
|
1652
|
-
xml.push(
|
1735
|
+
xml.push(entities.Note[i].asXML);
|
1653
1736
|
}
|
1654
1737
|
for(let i = 0; i < entities.Product.length; i++) {
|
1655
1738
|
const p = entities.Product[i];
|
@@ -1705,7 +1788,11 @@ class LinnyRModel {
|
|
1705
1788
|
for(let i = 0; i < from_tos.length; i++) {
|
1706
1789
|
const p = from_tos[i];
|
1707
1790
|
ft_xml.push('<from-to type="', p.type, '" name="', xmlEncoded(p.name));
|
1708
|
-
if(p instanceof Process)
|
1791
|
+
if(p instanceof Process) {
|
1792
|
+
ft_xml.push('" actor-name="', xmlEncoded(p.actor.name));
|
1793
|
+
} else if(p.is_data) {
|
1794
|
+
ft_xml.push('" is-data="1');
|
1795
|
+
}
|
1709
1796
|
ft_xml.push('"></from-to>');
|
1710
1797
|
}
|
1711
1798
|
for(let i = 0; i < extras.length; i++) {
|
@@ -1824,7 +1911,7 @@ class LinnyRModel {
|
|
1824
1911
|
for(let i = 0; i < notes.length; i++) {
|
1825
1912
|
const c = this.addNote();
|
1826
1913
|
if(c) {
|
1827
|
-
c.copyPropertiesFrom(notes[i]);
|
1914
|
+
c.copyPropertiesFrom(notes[i], renumber);
|
1828
1915
|
c.x += 100;
|
1829
1916
|
c.y += 100;
|
1830
1917
|
cloned_selection.push(c);
|
@@ -2188,7 +2275,7 @@ class LinnyRModel {
|
|
2188
2275
|
|
2189
2276
|
get datasetVariables() {
|
2190
2277
|
// Returns list with all ChartVariable objects in this model that
|
2191
|
-
// reference a regular dataset, i.e., not an equation
|
2278
|
+
// reference a regular dataset, i.e., not an equation.
|
2192
2279
|
const vl = [];
|
2193
2280
|
for(let i = 0; i < MODEL.charts.length; i++) {
|
2194
2281
|
const c = MODEL.charts[i];
|
@@ -2354,19 +2441,21 @@ class LinnyRModel {
|
|
2354
2441
|
|
2355
2442
|
parseXML(data) {
|
2356
2443
|
// Parse data string into XML tree
|
2357
|
-
try {
|
2444
|
+
// try {
|
2358
2445
|
// NOTE: Convert %23 back to # (escaped by function saveModel)
|
2359
2446
|
const xml = parseXML(data.replace(/%23/g, '#'));
|
2360
2447
|
// NOTE: loading, not including => make sure that IO context is NULL
|
2361
2448
|
IO_CONTEXT = null;
|
2362
2449
|
this.initFromXML(xml);
|
2363
2450
|
return true;
|
2451
|
+
/*
|
2364
2452
|
} catch(err) {
|
2365
2453
|
// Cursor is set to WAITING when loading starts
|
2366
2454
|
UI.normalCursor();
|
2367
2455
|
UI.alert('Error while parsing model: ' + err);
|
2368
2456
|
return false;
|
2369
2457
|
}
|
2458
|
+
*/
|
2370
2459
|
}
|
2371
2460
|
|
2372
2461
|
initFromXML(node) {
|
@@ -2961,7 +3050,14 @@ class LinnyRModel {
|
|
2961
3050
|
}
|
2962
3051
|
|
2963
3052
|
resetExpressions() {
|
2964
|
-
// Create a new vector for all expression attributes of all model
|
3053
|
+
// Create a new vector for all expression attributes of all model
|
3054
|
+
// entities, using the appropriate default value.
|
3055
|
+
|
3056
|
+
// Ensure that the equations dataset must have default value UNDEFINED
|
3057
|
+
// so the modeler is warned when a wildcard equation fails to obtain
|
3058
|
+
// a valid wildcard number.
|
3059
|
+
this.equations_dataset.default_value = VM.UNDEFINED;
|
3060
|
+
|
2965
3061
|
let obj, l, p;
|
2966
3062
|
for(obj in this.actors) if(this.actors.hasOwnProperty(obj)) {
|
2967
3063
|
p = this.actors[obj];
|
@@ -2979,9 +3075,7 @@ class LinnyRModel {
|
|
2979
3075
|
this.cleanVector(p.cash_in, 0, 0);
|
2980
3076
|
this.cleanVector(p.cash_out, 0, 0);
|
2981
3077
|
// NOTE: note fields also must be reset
|
2982
|
-
|
2983
|
-
p.notes[i].parsed = false;
|
2984
|
-
}
|
3078
|
+
p.resetNoteFields();
|
2985
3079
|
}
|
2986
3080
|
for(obj in this.processes) if(this.processes.hasOwnProperty(obj)) {
|
2987
3081
|
p = this.processes[obj];
|
@@ -4521,7 +4615,8 @@ class ObjectWithXYWH {
|
|
4521
4615
|
}
|
4522
4616
|
|
4523
4617
|
alignToGrid() {
|
4524
|
-
// Align this object to the grid, and return TRUE if this involved
|
4618
|
+
// Align this object to the grid, and return TRUE if this involved
|
4619
|
+
// a move
|
4525
4620
|
const
|
4526
4621
|
ox = this.x,
|
4527
4622
|
oy = this.y,
|
@@ -4542,22 +4637,29 @@ class ObjectWithXYWH {
|
|
4542
4637
|
} // END of CLASS ObjectWithXYWH
|
4543
4638
|
|
4544
4639
|
|
4545
|
-
// CLASS NoteField: numeric value of "field" [[
|
4640
|
+
// CLASS NoteField: numeric value of "field" [[variable]] in note text
|
4546
4641
|
class NoteField {
|
4547
|
-
constructor(f, o, u='1', m=1) {
|
4642
|
+
constructor(n, f, o, u='1', m=1, w=false) {
|
4643
|
+
// `n` is the note that "owns" this note field
|
4548
4644
|
// `f` holds the unmodified tag string [[dataset]] to be replaced by
|
4549
4645
|
// the value of vector or expression `o` for the current time step;
|
4550
|
-
// if specified, `u` is the unit of the value to be displayed,
|
4551
|
-
// `m` is the multiplier for the value to be displayed
|
4646
|
+
// if specified, `u` is the unit of the value to be displayed,
|
4647
|
+
// `m` is the multiplier for the value to be displayed, and `w` is
|
4648
|
+
// the wildcard number to use in a wildcard equation
|
4649
|
+
this.note = n;
|
4552
4650
|
this.field = f;
|
4553
4651
|
this.object = o;
|
4554
4652
|
this.unit = u;
|
4555
4653
|
this.multiplier = m;
|
4654
|
+
this.wildcard_number = (w ? parseInt(w) : false);
|
4556
4655
|
}
|
4557
4656
|
|
4558
4657
|
get value() {
|
4559
4658
|
// Returns the numeric value of this note field as a numeric string
|
4560
4659
|
// followed by its unit (unless this is 1)
|
4660
|
+
// If object is the note, this means field [[#]] (note number context)
|
4661
|
+
// If this is undefined (empty string) display a double question mark
|
4662
|
+
if(this.object === this.note) return this.note.numberContext || '\u2047';
|
4561
4663
|
let v = VM.UNDEFINED;
|
4562
4664
|
const t = MODEL.t;
|
4563
4665
|
if(Array.isArray(this.object)) {
|
@@ -4569,7 +4671,7 @@ class NoteField {
|
|
4569
4671
|
v = MODEL.flowBalance(this.object, t);
|
4570
4672
|
} else if(this.object instanceof Expression) {
|
4571
4673
|
// Object is an expression
|
4572
|
-
v = this.object.result(t);
|
4674
|
+
v = this.object.result(t, this.wildcard_number);
|
4573
4675
|
} else if(typeof this.object === 'number') {
|
4574
4676
|
v = this.object;
|
4575
4677
|
} else {
|
@@ -4594,7 +4696,7 @@ class Note extends ObjectWithXYWH {
|
|
4594
4696
|
super(cluster);
|
4595
4697
|
const dt = new Date();
|
4596
4698
|
// NOTE: use timestamp in msec to generate a unique identifier
|
4597
|
-
this.timestamp = dt.getTime();
|
4699
|
+
this.timestamp = dt.getTime();
|
4598
4700
|
this.contents = '';
|
4599
4701
|
this.lines = [];
|
4600
4702
|
this.fields = [];
|
@@ -4610,17 +4712,68 @@ class Note extends ObjectWithXYWH {
|
|
4610
4712
|
return 'Note';
|
4611
4713
|
}
|
4612
4714
|
|
4715
|
+
get clusterPrefix() {
|
4716
|
+
// Returns the name of the cluster containing this note, followed
|
4717
|
+
// by a colon+space, except when this cluster is the top cluster.
|
4718
|
+
if(this.cluster === MODEL.top_cluster) return '';
|
4719
|
+
return this.cluster.displayName + UI.PREFIXER;
|
4720
|
+
}
|
4721
|
+
|
4613
4722
|
get displayName() {
|
4614
|
-
|
4615
|
-
this.
|
4723
|
+
const
|
4724
|
+
n = this.number,
|
4725
|
+
type = (n ? `Numbered note #${n}` : 'Note');
|
4726
|
+
return `${this.clusterPrefix}${type} at (${this.x}, ${this.y})`;
|
4616
4727
|
}
|
4617
4728
|
|
4618
|
-
get
|
4619
|
-
// Returns the
|
4620
|
-
// NOTE: this
|
4729
|
+
get number() {
|
4730
|
+
// Returns the number of this note if specified (e.g. as #123).
|
4731
|
+
// NOTE: this only applies to notes having note fields.
|
4732
|
+
const m = this.contents.replace(/\s+/g, ' ')
|
4733
|
+
.match(/^[^\]]*#(\d+).*\[\[[^\]]+\]\]/);
|
4734
|
+
if(m) return m[1];
|
4621
4735
|
return '';
|
4622
4736
|
}
|
4623
4737
|
|
4738
|
+
get numberContext() {
|
4739
|
+
// Returns the string to be used to evaluate #. For notes this is
|
4740
|
+
// their note number if specified, otherwise the number context of a
|
4741
|
+
// nearby enode, and otherwise the number context of their cluster.
|
4742
|
+
let n = this.number;
|
4743
|
+
if(n) return n;
|
4744
|
+
n = this.nearbyNode;
|
4745
|
+
if(n) return n.numberContext;
|
4746
|
+
return this.cluster.numberContext;
|
4747
|
+
}
|
4748
|
+
|
4749
|
+
get nearbyNode() {
|
4750
|
+
// Returns a node in the cluster of this note that is closest to this
|
4751
|
+
// note (Euclidian distance between center points), but with at most
|
4752
|
+
// 30 pixel units between their rims.
|
4753
|
+
const
|
4754
|
+
c = this.cluster,
|
4755
|
+
nodes = c.processes.concat(c.product_positions, c.sub_clusters);
|
4756
|
+
let nn = nodes[0] || null;
|
4757
|
+
if(nn) {
|
4758
|
+
let md = 1e+10;
|
4759
|
+
// Find the nearest node
|
4760
|
+
for(let i = 0; i < nodes.length; i++) {
|
4761
|
+
const n = nodes[i];
|
4762
|
+
const
|
4763
|
+
dx = (n.x - this.x),
|
4764
|
+
dy = (n.y - this.y),
|
4765
|
+
d = Math.sqrt(dx*dx + dy*dy);
|
4766
|
+
if(d < md) {
|
4767
|
+
nn = n;
|
4768
|
+
md = d;
|
4769
|
+
}
|
4770
|
+
}
|
4771
|
+
if(Math.abs(nn.x - this.x) < (nn.width + this.width) / 2 + 30 &&
|
4772
|
+
Math.abs(nn.y - this.y) < (nn.height + this.height) / 2 + 30) return nn;
|
4773
|
+
}
|
4774
|
+
return null;
|
4775
|
+
}
|
4776
|
+
|
4624
4777
|
get asXML() {
|
4625
4778
|
return ['<note><timestamp>', this.timestamp,
|
4626
4779
|
'</timestamp><contents>', xmlEncoded(this.contents),
|
@@ -4655,27 +4808,27 @@ class Note extends ObjectWithXYWH {
|
|
4655
4808
|
}
|
4656
4809
|
|
4657
4810
|
setCluster(c) {
|
4658
|
-
// Place this note into the specified cluster `c
|
4811
|
+
// Place this note into the specified cluster `c`.
|
4659
4812
|
if(this.cluster) {
|
4660
|
-
// Remove this note from its current cluster's note list
|
4813
|
+
// Remove this note from its current cluster's note list.
|
4661
4814
|
const i = this.cluster.notes.indexOf(this);
|
4662
4815
|
if(i >= 0) this.cluster.notes.splice(i, 1);
|
4663
4816
|
// Set its new cluster pointer...
|
4664
4817
|
this.cluster = c;
|
4665
|
-
// ... and add it to the new cluster's note list
|
4818
|
+
// ... and add it to the new cluster's note list.
|
4666
4819
|
if(c.notes.indexOf(this) < 0) c.notes.push(this);
|
4667
4820
|
}
|
4668
4821
|
}
|
4669
4822
|
|
4670
4823
|
get tagList() {
|
4671
|
-
// Returns a list of matches for [[...]], or NULL if none
|
4824
|
+
// Returns a list of matches for [[...]], or NULL if none.
|
4672
4825
|
return this.contents.match(/\[\[[^\]]+\]\]/g);
|
4673
4826
|
}
|
4674
4827
|
|
4675
4828
|
parseFields() {
|
4676
|
-
// Fills the list of fields by parsing all [[...]] tags in the text
|
4829
|
+
// Fills the list of fields by parsing all [[...]] tags in the text.
|
4677
4830
|
// NOTE: this does not affect the text itself; tags will be replaced
|
4678
|
-
// by numerical values only when drawing the note
|
4831
|
+
// by numerical values only when drawing the note.
|
4679
4832
|
this.fields.length = 0;
|
4680
4833
|
const tags = this.tagList;
|
4681
4834
|
if(tags) {
|
@@ -4685,19 +4838,25 @@ class Note extends ObjectWithXYWH {
|
|
4685
4838
|
inner = tag.slice(2, tag.length - 2).trim(),
|
4686
4839
|
bar = inner.lastIndexOf('|'),
|
4687
4840
|
arrow = inner.lastIndexOf('->');
|
4688
|
-
//
|
4841
|
+
// Special case: [[#]] denotes the number context of this note.
|
4842
|
+
if(tag.replace(/\s+/, '') === '[[#]]') {
|
4843
|
+
this.fields.push(new NoteField(this, tag, this));
|
4844
|
+
// Done, so move on to the next tag
|
4845
|
+
continue;
|
4846
|
+
}
|
4847
|
+
// Check if a unit conversion scalar was specified.
|
4689
4848
|
let ena,
|
4690
4849
|
from_unit = '1',
|
4691
4850
|
to_unit = '',
|
4692
4851
|
multiplier = 1;
|
4693
4852
|
if(arrow > bar) {
|
4694
|
-
// Now for sure it is entity->unit or entity|attr->unit
|
4853
|
+
// Now for sure it is entity->unit or entity|attr->unit.
|
4695
4854
|
ena = inner.split('->');
|
4696
4855
|
// As example, assume that unit = 'kWh' (so the value of the
|
4697
|
-
// field should be displayed in kilowatthour)
|
4698
|
-
// NOTE: use .trim() instead of UI.cleanName(...) here
|
4699
|
-
//
|
4700
|
-
// renaming of scale units in note fields
|
4856
|
+
// field should be displayed in kilowatthour).
|
4857
|
+
// NOTE: use .trim() instead of UI.cleanName(...) here. This
|
4858
|
+
// forces the modeler to be exact, and that permits proper
|
4859
|
+
// renaming of scale units in note fields.
|
4701
4860
|
to_unit = ena[1].trim();
|
4702
4861
|
ena = ena[0].split('|');
|
4703
4862
|
if(!MODEL.scale_units.hasOwnProperty(to_unit)) {
|
@@ -4707,27 +4866,56 @@ class Note extends ObjectWithXYWH {
|
|
4707
4866
|
} else {
|
4708
4867
|
ena = inner.split('|');
|
4709
4868
|
}
|
4710
|
-
// Look up entity for name and attribute
|
4711
|
-
|
4712
|
-
|
4713
|
-
|
4869
|
+
// Look up entity for name and attribute.
|
4870
|
+
// NOTE: A leading colon denotes "prefix with cluster name".
|
4871
|
+
let en = UI.colonPrefixedName(ena[0].trim(), this.clusterPrefix),
|
4872
|
+
id = UI.nameToID(en),
|
4873
|
+
// First try to match `id` with the IDs of wildcard equations,
|
4874
|
+
// (e.g., "abc 123" would match with "abc ??").
|
4875
|
+
w = MODEL.wildcardEquationByID(id),
|
4876
|
+
obj = null,
|
4877
|
+
wildcard = false;
|
4878
|
+
if(w) {
|
4879
|
+
// If wildcard equation match, w[0] is the equation (instance
|
4880
|
+
// of DatasetModifier), and w[1] the matching number.
|
4881
|
+
obj = w[0];
|
4882
|
+
wildcard = w[1];
|
4883
|
+
} else {
|
4884
|
+
obj = MODEL.objectByID(id);
|
4885
|
+
}
|
4886
|
+
// If not found, this may be due to # wildcards in the name.
|
4887
|
+
if(!obj && en.indexOf('#') >= 0) {
|
4888
|
+
// First try substituting # by the context number.
|
4889
|
+
const numcon = this.numberContext;
|
4890
|
+
obj = MODEL.objectByName(en.replace('#', numcon));
|
4891
|
+
// If no match, check whether the name matches a wildcard equation.
|
4892
|
+
if(!obj) {
|
4893
|
+
obj = MODEL.equationByID(UI.nameToID(en.replace('#', '??')));
|
4894
|
+
if(obj) wildcard = numcon;
|
4895
|
+
}
|
4896
|
+
}
|
4897
|
+
if(!obj) {
|
4898
|
+
UI.warn(`Unknown model entity "${en}"`);
|
4899
|
+
} else if(obj instanceof DatasetModifier) {
|
4900
|
+
// NOTE: equations are (for now) dimensionless => unit '1'.
|
4714
4901
|
if(obj.dataset !== MODEL.equations_dataset) {
|
4715
4902
|
from_unit = obj.dataset.scale_unit;
|
4716
4903
|
multiplier = MODEL.unitConversionMultiplier(from_unit, to_unit);
|
4717
4904
|
}
|
4718
|
-
this.fields.push(new NoteField(tag, obj.expression, to_unit,
|
4905
|
+
this.fields.push(new NoteField(this, tag, obj.expression, to_unit,
|
4906
|
+
multiplier, wildcard));
|
4719
4907
|
} else if(obj) {
|
4720
|
-
// If attribute omitted, use default attribute of entity type
|
4908
|
+
// If attribute omitted, use default attribute of entity type.
|
4721
4909
|
const attr = (ena.length > 1 ? ena[1].trim() : obj.defaultAttribute);
|
4722
4910
|
let val = null;
|
4723
|
-
// NOTE: for datasets, use the active modifier
|
4911
|
+
// NOTE: for datasets, use the active modifier if no attribute.
|
4724
4912
|
if(!attr && obj instanceof Dataset) {
|
4725
4913
|
val = obj.activeModifierExpression;
|
4726
4914
|
} else {
|
4727
|
-
// Variable may specify a vector-type attribute
|
4915
|
+
// Variable may specify a vector-type attribute.
|
4728
4916
|
val = obj.attributeValue(attr);
|
4729
4917
|
}
|
4730
|
-
// If not, it may be a cluster unit balance
|
4918
|
+
// If not, it may be a cluster unit balance.
|
4731
4919
|
if(!val && attr.startsWith('=') && obj instanceof Cluster) {
|
4732
4920
|
val = {c: obj, u: attr.substring(1).trim()};
|
4733
4921
|
from_unit = val.u;
|
@@ -4753,9 +4941,20 @@ class Note extends ObjectWithXYWH {
|
|
4753
4941
|
} else if(attr === 'CI' || attr === 'CO' || attr === 'CF') {
|
4754
4942
|
from_unit = MODEL.currency_unit;
|
4755
4943
|
}
|
4756
|
-
// If
|
4944
|
+
// If still no value, `attr` may be an expression-type attribute.
|
4757
4945
|
if(!val) {
|
4758
4946
|
val = obj.attributeExpression(attr);
|
4947
|
+
// For wildcard expressions, provide the tail number of `attr`
|
4948
|
+
// as number context.
|
4949
|
+
if(val && val.isWildcardExpression) {
|
4950
|
+
const nr = matchingNumber(attr, val.attribute);
|
4951
|
+
if(nr) {
|
4952
|
+
wildcard = nr;
|
4953
|
+
} else {
|
4954
|
+
UI.warn(`Attribute "${attr}" does not provide a number`);
|
4955
|
+
continue;
|
4956
|
+
}
|
4957
|
+
}
|
4759
4958
|
if(obj instanceof Product) {
|
4760
4959
|
if(attr === 'IL' || attr === 'LB' || attr === 'UB') {
|
4761
4960
|
from_unit = obj.scale_unit;
|
@@ -4764,16 +4963,15 @@ class Note extends ObjectWithXYWH {
|
|
4764
4963
|
}
|
4765
4964
|
}
|
4766
4965
|
}
|
4767
|
-
// If no TO unit, add the FROM unit
|
4966
|
+
// If no TO unit, add the FROM unit.
|
4768
4967
|
if(to_unit === '') to_unit = from_unit;
|
4769
4968
|
if(val) {
|
4770
4969
|
multiplier = MODEL.unitConversionMultiplier(from_unit, to_unit);
|
4771
|
-
this.fields.push(new NoteField(tag, val, to_unit,
|
4970
|
+
this.fields.push(new NoteField(this, tag, val, to_unit,
|
4971
|
+
multiplier, wildcard));
|
4772
4972
|
} else {
|
4773
4973
|
UI.warn(`Unknown ${obj.type.toLowerCase()} attribute "${attr}"`);
|
4774
4974
|
}
|
4775
|
-
} else {
|
4776
|
-
UI.warn(`Unknown model entity "${ena[0].trim()}"`);
|
4777
4975
|
}
|
4778
4976
|
}
|
4779
4977
|
}
|
@@ -4781,20 +4979,24 @@ class Note extends ObjectWithXYWH {
|
|
4781
4979
|
}
|
4782
4980
|
|
4783
4981
|
get fieldEntities() {
|
4784
|
-
// Return a list with names of entities used in fields
|
4982
|
+
// Return a list with names of entities used in fields.
|
4785
4983
|
const
|
4786
4984
|
fel = [],
|
4787
4985
|
tags = this.tagList;
|
4788
4986
|
for(let i = 0; i < tags.length; i++) {
|
4789
4987
|
const
|
4790
4988
|
tag = tags[i],
|
4791
|
-
|
4989
|
+
// Trim brackets and padding spaces on both sides, and then
|
4990
|
+
// expand leading colons that denote prefixes.
|
4991
|
+
inner = UI.colonPrefixedName(tag.slice(2, tag.length - 2).trim()),
|
4792
4992
|
vb = inner.lastIndexOf('|'),
|
4793
4993
|
ua = inner.lastIndexOf('->');
|
4794
4994
|
if(vb >= 0) {
|
4995
|
+
// Vertical bar? Then the entity name is the left part.
|
4795
4996
|
addDistinct(inner.slice(0, vb), fel);
|
4796
4997
|
} else if(ua >= 0 &&
|
4797
4998
|
MODEL.scale_units.hasOwnProperty(inner.slice(ua + 2))) {
|
4999
|
+
// Unit arrow? Then trim the "->unit" part.
|
4798
5000
|
addDistinct(inner.slice(0, ua), fel);
|
4799
5001
|
} else {
|
4800
5002
|
addDistinct(inner, fel);
|
@@ -4818,29 +5020,30 @@ class Note extends ObjectWithXYWH {
|
|
4818
5020
|
}
|
4819
5021
|
|
4820
5022
|
rewriteFields(en1, en2) {
|
4821
|
-
// Rename fields that reference entity name `en1` to reference `en2`
|
4822
|
-
//
|
5023
|
+
// Rename fields that reference entity name `en1` to reference `en2`
|
5024
|
+
// instead.
|
5025
|
+
// NOTE: This does not affect the expression code.
|
4823
5026
|
if(en1 === en2) return;
|
4824
5027
|
for(let i = 0; i < this.fields.length; i++) {
|
4825
5028
|
const
|
4826
5029
|
f = this.fields[i],
|
4827
|
-
// Trim the double brackets on both sides
|
4828
|
-
tag = f.field.slice(2, f.field.length - 2);
|
4829
|
-
// Separate tag into variable and attribute + offset string (if any)
|
5030
|
+
// Trim the double brackets and padding spaces on both sides.
|
5031
|
+
tag = f.field.slice(2, f.field.length - 2).trim();
|
5032
|
+
// Separate tag into variable and attribute + offset string (if any).
|
4830
5033
|
let e = tag,
|
4831
5034
|
a = '',
|
4832
5035
|
vb = tag.lastIndexOf('|'),
|
4833
5036
|
ua = tag.lastIndexOf('->');
|
4834
5037
|
if(vb >= 0) {
|
4835
5038
|
e = tag.slice(0, vb);
|
4836
|
-
// NOTE:
|
5039
|
+
// NOTE: Attribute string includes the vertical bar '|'.
|
4837
5040
|
a = tag.slice(vb);
|
4838
5041
|
} else if(ua >= 0 && MODEL.scale_units.hasOwnProperty(tag.slice(ua + 2))) {
|
4839
5042
|
e = tag.slice(0, ua);
|
4840
|
-
// NOTE:
|
5043
|
+
// NOTE: Attribute string includes the unit conversion arrow '->'.
|
4841
5044
|
a = tag.slice(ua);
|
4842
5045
|
}
|
4843
|
-
// Check for match
|
5046
|
+
// Check for match.
|
4844
5047
|
const r = UI.replaceEntity(e, en1, en2);
|
4845
5048
|
if(r) {
|
4846
5049
|
e = `[[${r}${a}]]`;
|
@@ -4851,6 +5054,8 @@ class Note extends ObjectWithXYWH {
|
|
4851
5054
|
}
|
4852
5055
|
|
4853
5056
|
get evaluateFields() {
|
5057
|
+
// Returns the text content of this note with all tags replaced
|
5058
|
+
// by their note field values.
|
4854
5059
|
if(!this.parsed) this.parseFields();
|
4855
5060
|
let txt = this.contents;
|
4856
5061
|
for(let i = 0; i < this.fields.length; i++) {
|
@@ -4861,51 +5066,66 @@ class Note extends ObjectWithXYWH {
|
|
4861
5066
|
}
|
4862
5067
|
|
4863
5068
|
resize() {
|
4864
|
-
// Resizes the note; returns TRUE iff size has changed
|
5069
|
+
// Resizes the note; returns TRUE iff size has changed.
|
4865
5070
|
let txt = this.evaluateFields;
|
4866
5071
|
const
|
4867
5072
|
w = this.width,
|
4868
5073
|
h = this.height,
|
4869
|
-
// Minimumm note width of 10 characters
|
5074
|
+
// Minimumm note width of 10 characters.
|
4870
5075
|
n = Math.max(txt.length, 10),
|
4871
5076
|
fh = UI.textSize('hj').height;
|
4872
|
-
// Approximate the width to obtain a rectangle
|
5077
|
+
// Approximate the width to obtain a rectangle.
|
4873
5078
|
// NOTE: 3:1 may seem exagerated, but characters are higher than wide,
|
4874
|
-
// and there will be more (short) lines due to newlines and wrapping
|
5079
|
+
// and there will be more (short) lines due to newlines and wrapping.
|
4875
5080
|
let tw = Math.ceil(3*Math.sqrt(n)) * fh / 2;
|
4876
5081
|
this.lines = UI.stringToLineArray(txt, tw).join('\n');
|
4877
5082
|
let bb = UI.textSize(this.lines, 8);
|
4878
|
-
//
|
5083
|
+
// Aim to make the shape wider than tall.
|
4879
5084
|
let nw = bb.width,
|
4880
5085
|
nh = bb.height;
|
4881
5086
|
while(bb.width < bb.height * 1.7) {
|
4882
5087
|
tw *= 1.2;
|
4883
5088
|
this.lines = UI.stringToLineArray(txt, tw).join('\n');
|
4884
5089
|
bb = UI.textSize(this.lines, 8);
|
4885
|
-
// Prevent infinite loop
|
5090
|
+
// Prevent infinite loop.
|
4886
5091
|
if(nw <= bb.width || nh > bb.height) break;
|
4887
5092
|
}
|
4888
5093
|
this.height = 1.05 * (bb.height + 6);
|
4889
5094
|
this.width = bb.width + 6;
|
5095
|
+
// Boolean return value indicates whether size has changed.
|
4890
5096
|
return this.width != w || this.height != h;
|
4891
5097
|
}
|
4892
5098
|
|
4893
5099
|
containsPoint(mpx, mpy) {
|
5100
|
+
// Returns TRUE iff given coordinates lie within the note rectangle.
|
4894
5101
|
return (Math.abs(mpx - this.x) <= this.width / 2 &&
|
4895
5102
|
Math.abs(mpy - this.y) <= this.height / 2);
|
4896
5103
|
}
|
4897
5104
|
|
4898
|
-
copyPropertiesFrom(n) {
|
4899
|
-
//
|
5105
|
+
copyPropertiesFrom(n, renumber=false) {
|
5106
|
+
// Sets properties to be identical to those of note `n`.
|
4900
5107
|
this.x = n.x;
|
4901
5108
|
this.y = n.y;
|
4902
|
-
|
5109
|
+
let cont = n.contents;
|
5110
|
+
if(renumber) {
|
5111
|
+
// Renumbering only applies to notes having note fields; then the
|
5112
|
+
// note number must be denoted like #123, and occur before the first
|
5113
|
+
// note field.
|
5114
|
+
const m = cont.match(/^[^\]]*#(\d+).*\[\[[^\]]+\]\]/);
|
5115
|
+
if(m) {
|
5116
|
+
const nn = this.cluster.nextAvailableNoteNumber(m[1]);
|
5117
|
+
cont = cont.replace(/#\d+/, `#${nn}`);
|
5118
|
+
}
|
5119
|
+
}
|
5120
|
+
this.contents = cont;
|
5121
|
+
// NOTE: Renumbering does not affect the note fields or the color
|
5122
|
+
// expression. This is a design choice; the modeler can use wildcards.
|
4903
5123
|
this.color.text = n.color.text;
|
4904
5124
|
this.parsed = false;
|
4905
5125
|
}
|
4906
5126
|
|
4907
5127
|
differences(n) {
|
4908
|
-
// Return "dictionary" of differences, or NULL if none
|
5128
|
+
// Return "dictionary" of differences, or NULL if none.
|
4909
5129
|
const d = differences(this, n, UI.MC.NOTE_PROPS);
|
4910
5130
|
if(Object.keys(d).length > 0) return d;
|
4911
5131
|
return null;
|
@@ -4947,6 +5167,17 @@ class NodeBox extends ObjectWithXYWH {
|
|
4947
5167
|
// NOTE: Display nothing if entity is "black-boxed"
|
4948
5168
|
if(n.startsWith(UI.BLACK_BOX)) return '';
|
4949
5169
|
n = `<em>${this.type}:</em> ${n}`;
|
5170
|
+
// For clusters, add how many processes and products they contain
|
5171
|
+
if(this instanceof Cluster) {
|
5172
|
+
let d = '';
|
5173
|
+
if(this.all_processes) {
|
5174
|
+
const dl = [];
|
5175
|
+
dl.push(pluralS(this.all_processes.length, 'process'));
|
5176
|
+
dl.push(pluralS(this.all_products.length, 'product'));
|
5177
|
+
d = dl.join(', ').toLowerCase();
|
5178
|
+
}
|
5179
|
+
if(d) n += `<span class="node-details">${d}</span>`;
|
5180
|
+
}
|
4950
5181
|
if(DEBUGGING && MODEL.solved) {
|
4951
5182
|
n += ' [';
|
4952
5183
|
if(this instanceof Process || this instanceof Product) {
|
@@ -4971,10 +5202,11 @@ class NodeBox extends ObjectWithXYWH {
|
|
4971
5202
|
}
|
4972
5203
|
|
4973
5204
|
get numberContext() {
|
4974
|
-
// Returns the string to be used to evaluate #, so for clusters,
|
4975
|
-
// and products this is the string of trailing digits
|
4976
|
-
// of the node name, or if that does not end
|
4977
|
-
// digits of the first prefix (from right to
|
5205
|
+
// Returns the string to be used to evaluate #, so for clusters,
|
5206
|
+
// processes and products this is the string of trailing digits
|
5207
|
+
// (or empty if none) of the node name, or if that does not end on
|
5208
|
+
// a number, the trailing digits of the first prefix (from right to
|
5209
|
+
// left) that does end on a number
|
4978
5210
|
const sn = UI.prefixesAndName(this.name);
|
4979
5211
|
let nc = endsWithDigits(sn.pop());
|
4980
5212
|
while(!nc && sn.length > 0) {
|
@@ -4983,33 +5215,50 @@ class NodeBox extends ObjectWithXYWH {
|
|
4983
5215
|
return nc;
|
4984
5216
|
}
|
4985
5217
|
|
5218
|
+
get similarNumberedEntities() {
|
5219
|
+
// Returns a list of nodes of the same type that have a number
|
5220
|
+
// context similar to this node.
|
5221
|
+
const nc = this.numberContext;
|
5222
|
+
if(!nc) return [];
|
5223
|
+
const
|
5224
|
+
re = wildcardMatchRegex(this.displayName.replace(nc, '#')),
|
5225
|
+
nodes = MODEL.setByType(this.type),
|
5226
|
+
similar = [];
|
5227
|
+
for(let id in nodes) if(nodes.hasOwnProperty(id)) {
|
5228
|
+
const n = nodes[id];
|
5229
|
+
if(n.displayName.match(re)) similar.push(n);
|
5230
|
+
}
|
5231
|
+
return similar;
|
5232
|
+
}
|
5233
|
+
|
4986
5234
|
rename(name, actor_name) {
|
4987
|
-
// Changes the name and/or actor name of this process, product
|
4988
|
-
//
|
4989
|
-
//
|
4990
|
-
//
|
5235
|
+
// Changes the name and/or actor name of this node (process, product
|
5236
|
+
// or cluster).
|
5237
|
+
// NOTE: Returns TRUE if rename was successful, FALSE on error, and
|
5238
|
+
// a process, product or cluster if such entity having the new name
|
5239
|
+
// already exists.
|
4991
5240
|
name = UI.cleanName(name);
|
4992
5241
|
if(!UI.validName(name)) {
|
4993
5242
|
UI.warningInvalidName(name);
|
4994
5243
|
return false;
|
4995
5244
|
}
|
4996
|
-
// Compose the full name
|
5245
|
+
// Compose the full name.
|
4997
5246
|
if(actor_name === '') actor_name = UI.NO_ACTOR;
|
4998
5247
|
let fn = name;
|
4999
5248
|
if(actor_name != UI.NO_ACTOR) fn += ` (${actor_name})`;
|
5000
5249
|
// Get the ID (derived from the full name) and check if MODEL already
|
5001
|
-
// contains another entity with this ID
|
5250
|
+
// contains another entity with this ID.
|
5002
5251
|
const
|
5003
5252
|
old_name = this.displayName,
|
5004
5253
|
old_id = this.identifier,
|
5005
5254
|
new_id = UI.nameToID(fn),
|
5006
5255
|
n = MODEL.nodeBoxByID(new_id);
|
5007
|
-
// If so, do NOT rename, but return this object instead
|
5008
|
-
// NOTE:
|
5009
|
-
// a cosmetic name change (upper/lower case) which SHOULD be performed
|
5256
|
+
// If so, do NOT rename, but return this object instead.
|
5257
|
+
// NOTE: If entity with this name is THIS entity, it typically means
|
5258
|
+
// a cosmetic name change (upper/lower case) which SHOULD be performed.
|
5010
5259
|
if(n && n !== this) return n;
|
5011
|
-
// Otherwise, if IDs differ, add this object under its new key, and
|
5012
|
-
// its old entry
|
5260
|
+
// Otherwise, if IDs differ, add this object under its new key, and
|
5261
|
+
// remove its old entry.
|
5013
5262
|
if(old_id != new_id) {
|
5014
5263
|
if(this instanceof Process) {
|
5015
5264
|
MODEL.processes[new_id] = this;
|
@@ -5021,21 +5270,21 @@ class NodeBox extends ObjectWithXYWH {
|
|
5021
5270
|
MODEL.clusters[new_id] = this;
|
5022
5271
|
delete MODEL.clusters[old_id];
|
5023
5272
|
} else {
|
5024
|
-
// NOTE:
|
5273
|
+
// NOTE: This should never happen => report an error.
|
5025
5274
|
UI.alert('Can only rename processes, products and clusters');
|
5026
5275
|
return false;
|
5027
5276
|
}
|
5028
5277
|
}
|
5029
|
-
// Change this object's name and actor
|
5278
|
+
// Change this object's name and actor.
|
5030
5279
|
this.actor = MODEL.addActor(actor_name);
|
5031
5280
|
this.name = name;
|
5032
|
-
// Update actor list in case some actor name is no longer used
|
5281
|
+
// Update actor list in case some actor name is no longer used.
|
5033
5282
|
MODEL.cleanUpActors();
|
5034
5283
|
MODEL.replaceEntityInExpressions(old_name, this.displayName);
|
5035
5284
|
MODEL.inferIgnoredEntities();
|
5036
|
-
// NOTE:
|
5285
|
+
// NOTE: Renaming may affect the node's display size.
|
5037
5286
|
if(this.resize()) this.drawWithLinks();
|
5038
|
-
// NOTE:
|
5287
|
+
// NOTE: Only TRUE indicates a successful (cosmetic) name change.
|
5039
5288
|
return true;
|
5040
5289
|
}
|
5041
5290
|
|
@@ -5484,7 +5733,8 @@ class Cluster extends NodeBox {
|
|
5484
5733
|
}
|
5485
5734
|
|
5486
5735
|
attributeValue(a) {
|
5487
|
-
// Return the computed result for attribute a
|
5736
|
+
// Return the computed result for attribute `a`
|
5737
|
+
// For clusters, this is always a vector
|
5488
5738
|
if(a === 'CF') return this.cash_flow;
|
5489
5739
|
if(a === 'CI') return this.cash_in;
|
5490
5740
|
if(a === 'CO') return this.cash_out;
|
@@ -5751,6 +6001,27 @@ class Cluster extends NodeBox {
|
|
5751
6001
|
return notes;
|
5752
6002
|
}
|
5753
6003
|
|
6004
|
+
resetNoteFields() {
|
6005
|
+
// Ensure that all note fields are parsed anew when a note in this
|
6006
|
+
// cluster are drawn.
|
6007
|
+
for(let i = 0; i < this.notes.length; i++) {
|
6008
|
+
this.notes[i].parsed = false;
|
6009
|
+
}
|
6010
|
+
}
|
6011
|
+
|
6012
|
+
nextAvailableNoteNumber(n) {
|
6013
|
+
// Returns the first integer greater than `n` that is not already in use
|
6014
|
+
// by a note of this cluster
|
6015
|
+
let nn = parseInt(n) + 1;
|
6016
|
+
const nrs = [];
|
6017
|
+
for(let i = 0; i < this.notes.length; i++) {
|
6018
|
+
const nr = this.notes[i].number;
|
6019
|
+
if(nr) nrs.push(parseInt(nr));
|
6020
|
+
}
|
6021
|
+
while(nrs.indexOf(nn) >= 0) nn++;
|
6022
|
+
return nn;
|
6023
|
+
}
|
6024
|
+
|
5754
6025
|
clearAllProcesses() {
|
5755
6026
|
// Clear `all_processes` property of this cluster AND of all its parent clusters
|
5756
6027
|
this.all_processes = null;
|
@@ -7153,6 +7424,7 @@ class Process extends Node {
|
|
7153
7424
|
}
|
7154
7425
|
|
7155
7426
|
get defaultAttribute() {
|
7427
|
+
// Default attribute of processes is their level
|
7156
7428
|
return 'L';
|
7157
7429
|
}
|
7158
7430
|
|
@@ -7168,6 +7440,7 @@ class Process extends Node {
|
|
7168
7440
|
}
|
7169
7441
|
|
7170
7442
|
attributeExpression(a) {
|
7443
|
+
// Processes have three expression attributes
|
7171
7444
|
if(a === 'LB') return this.lower_bound;
|
7172
7445
|
if(a === 'UB') {
|
7173
7446
|
return (this.equal_bounds ? this.lower_bound : this.upper_bound);
|
@@ -7563,6 +7836,7 @@ class Product extends Node {
|
|
7563
7836
|
}
|
7564
7837
|
|
7565
7838
|
get defaultAttribute() {
|
7839
|
+
// Products have their level as default attribute
|
7566
7840
|
return 'L';
|
7567
7841
|
}
|
7568
7842
|
|
@@ -7576,6 +7850,7 @@ class Product extends Node {
|
|
7576
7850
|
}
|
7577
7851
|
|
7578
7852
|
attributeExpression(a) {
|
7853
|
+
// Products have four expression attributes
|
7579
7854
|
if(a === 'LB') return this.lower_bound;
|
7580
7855
|
if(a === 'UB') {
|
7581
7856
|
return (this.equal_bounds ? this.lower_bound : this.upper_bound);
|
@@ -7586,6 +7861,7 @@ class Product extends Node {
|
|
7586
7861
|
}
|
7587
7862
|
|
7588
7863
|
changeScaleUnit(name) {
|
7864
|
+
// Changes the scale unit for this product to `name`
|
7589
7865
|
let su = MODEL.addScaleUnit(name);
|
7590
7866
|
if(su !== this.scale_unit) {
|
7591
7867
|
this.scale_unit = su;
|
@@ -7873,6 +8149,7 @@ class Link {
|
|
7873
8149
|
}
|
7874
8150
|
|
7875
8151
|
get defaultAttribute() {
|
8152
|
+
// For links, the default attribute is their actual flow
|
7876
8153
|
return 'F';
|
7877
8154
|
}
|
7878
8155
|
|
@@ -7884,6 +8161,7 @@ class Link {
|
|
7884
8161
|
}
|
7885
8162
|
|
7886
8163
|
attributeExpression(a) {
|
8164
|
+
// Links have two expression attributes
|
7887
8165
|
if(a === 'R') return this.relative_rate;
|
7888
8166
|
if(a === 'D') return this.flow_delay;
|
7889
8167
|
return null;
|
@@ -7984,20 +8262,45 @@ class DatasetModifier {
|
|
7984
8262
|
}
|
7985
8263
|
|
7986
8264
|
get hasWildcards() {
|
7987
|
-
|
8265
|
+
// Returns TRUE if this modifier contains wildcards
|
8266
|
+
return this.dataset.isWildcardSelector(this.selector);
|
8267
|
+
}
|
8268
|
+
|
8269
|
+
get numberContext() {
|
8270
|
+
// Returns the string to be used to evaluate #.
|
8271
|
+
// NOTE: If the selector contains wildcards, return "?" to indicate
|
8272
|
+
// that the value of # cannot be inferred at compile time.
|
8273
|
+
if(this.hasWildcards) return '?';
|
8274
|
+
// Otherwise, # is the string of digits at the end of the selector.
|
8275
|
+
// NOTE: equation names are like entity names, so treat them as such,
|
8276
|
+
// i.e., also check for prefixes that end on digits.
|
8277
|
+
const sn = UI.prefixesAndName(this.name);
|
8278
|
+
let nc = endsWithDigits(sn.pop());
|
8279
|
+
while(!nc && sn.length > 0) {
|
8280
|
+
nc = endsWithDigits(sn.pop());
|
8281
|
+
}
|
8282
|
+
// NOTE: if the selector has no tail number, return the number context
|
8283
|
+
// of the dataset of this modifier.
|
8284
|
+
return nc || this.dataset.numberContext;
|
7988
8285
|
}
|
7989
8286
|
|
7990
8287
|
match(s) {
|
7991
|
-
if
|
7992
|
-
|
7993
|
-
|
7994
|
-
|
7995
|
-
|
8288
|
+
// Returns TRUE if string `s` matches with the wildcard pattern of
|
8289
|
+
// the selector.
|
8290
|
+
if(!this.hasWildcards) return s === this.selector;
|
8291
|
+
let re;
|
8292
|
+
if(this.dataset === MODEL.equations_dataset) {
|
8293
|
+
// Equations wildcards only match with digits
|
8294
|
+
re = wildcardMatchRegex(this.selector, true);
|
7996
8295
|
} else {
|
7997
|
-
|
8296
|
+
// Selector wildcards match with any character, so replace ? by .
|
8297
|
+
// (any character) in pattern, and * by .*
|
8298
|
+
const raw = this.selector.replace(/\?/g, '.').replace(/\*/g, '.*');
|
8299
|
+
re = new RegExp(`^${raw}$`);
|
7998
8300
|
}
|
8301
|
+
return re.test(s);
|
7999
8302
|
}
|
8000
|
-
|
8303
|
+
|
8001
8304
|
} // END of class DatasetModifier
|
8002
8305
|
|
8003
8306
|
|
@@ -8062,7 +8365,9 @@ class Dataset {
|
|
8062
8365
|
}
|
8063
8366
|
|
8064
8367
|
get numberContext() {
|
8065
|
-
// Returns the string to be used to evaluate #
|
8368
|
+
// Returns the string to be used to evaluate #
|
8369
|
+
// Like for nodes, this is the string of digits at the end of the
|
8370
|
+
// dataset name (if any) or an empty string (to denote undefined)
|
8066
8371
|
const sn = UI.prefixesAndName(this.name);
|
8067
8372
|
let nc = endsWithDigits(sn.pop());
|
8068
8373
|
while(!nc && sn.length > 0) {
|
@@ -8070,7 +8375,7 @@ class Dataset {
|
|
8070
8375
|
}
|
8071
8376
|
return nc;
|
8072
8377
|
}
|
8073
|
-
|
8378
|
+
|
8074
8379
|
get selectorList() {
|
8075
8380
|
// Returns sorted list of selectors (those with wildcards last)
|
8076
8381
|
const sl = [];
|
@@ -8090,14 +8395,45 @@ class Dataset {
|
|
8090
8395
|
return sl;
|
8091
8396
|
}
|
8092
8397
|
|
8093
|
-
|
8094
|
-
// Returns TRUE if
|
8095
|
-
for
|
8096
|
-
|
8398
|
+
isWildcardSelector(s) {
|
8399
|
+
// Returns TRUE if `s` contains * or ?
|
8400
|
+
// NOTE: for equations, the wildcard must be ??
|
8401
|
+
if(this.dataset === MODEL.equations_dataset) return s.indexOf('??') >= 0;
|
8402
|
+
return s.indexOf('*') >= 0 || s.indexOf('?') >= 0;
|
8403
|
+
}
|
8404
|
+
|
8405
|
+
matchingModifiers(l) {
|
8406
|
+
// Returns the list of modifiers of this dataset (in order: from most
|
8407
|
+
// to least specific) that match with 1 or more elements of `l`
|
8408
|
+
const
|
8409
|
+
sl = this.selectorList,
|
8410
|
+
shared = [];
|
8411
|
+
for(let i = 0; i < l.length; i++) {
|
8412
|
+
for(let j = 0; j < sl.length; j++) {
|
8413
|
+
const m = this.modifiers[UI.nameToID(sl[j])];
|
8414
|
+
if(m.match(l[i])) addDistinct(m, shared);
|
8415
|
+
}
|
8416
|
+
}
|
8417
|
+
return shared;
|
8418
|
+
}
|
8419
|
+
|
8420
|
+
modifiersAreStatic(l) {
|
8421
|
+
// Returns TRUE if expressions for all modifiers in `l` are static
|
8422
|
+
// NOTE: `l` may be a list of modifiers or strings
|
8423
|
+
for(let i = 0; i < l.length; i++) {
|
8424
|
+
let sel = l[i];
|
8425
|
+
if(sel instanceof DatasetModifier) sel = sel.selector;
|
8426
|
+
if(this.modifiers.hasOwnProperty(sel) &&
|
8427
|
+
!this.modifiers[sel].expression.isStatic) return false;
|
8097
8428
|
}
|
8098
8429
|
return true;
|
8099
8430
|
}
|
8100
8431
|
|
8432
|
+
get allModifiersAreStatic() {
|
8433
|
+
// Returns TRUE if all modifier expressions are static
|
8434
|
+
return this.modifiersAreStatic(Object.keys(this.modifiers));
|
8435
|
+
}
|
8436
|
+
|
8101
8437
|
get inferPrefixableModifiers() {
|
8102
8438
|
// Returns list of dataset modifiers with expressions that do not
|
8103
8439
|
// reference any variable and hence could probably better be represented
|
@@ -8141,21 +8477,6 @@ class Dataset {
|
|
8141
8477
|
}
|
8142
8478
|
}
|
8143
8479
|
|
8144
|
-
matchingModifiers(l) {
|
8145
|
-
// Returns the list of selectors of this dataset (in order: from most to
|
8146
|
-
// least specific) that match with 1 or more elements of `l`
|
8147
|
-
const
|
8148
|
-
sl = this.selectorList,
|
8149
|
-
shared = [];
|
8150
|
-
for(let i = 0; i < l.length; i++) {
|
8151
|
-
for(let j = 0; j < sl.length; j++) {
|
8152
|
-
const m = this.modifiers[UI.nameToID(sl[j])];
|
8153
|
-
if(m.match(l[i])) addDistinct(m, shared);
|
8154
|
-
}
|
8155
|
-
}
|
8156
|
-
return shared;
|
8157
|
-
}
|
8158
|
-
|
8159
8480
|
get dataString() {
|
8160
8481
|
// Data is stored simply as semicolon-separated floating point numbers,
|
8161
8482
|
// with N-digit precision to keep model files compact (default: N = 8)
|
@@ -8258,21 +8579,21 @@ class Dataset {
|
|
8258
8579
|
}
|
8259
8580
|
|
8260
8581
|
attributeValue(a) {
|
8261
|
-
// Returns the computed result for attribute `a
|
8582
|
+
// Returns the computed result for attribute `a`.
|
8262
8583
|
// NOTE: Datasets have ONE attribute (their vector) denoted by the empty
|
8263
|
-
// string; all other "attributes" should be modifier selectors
|
8584
|
+
// string; all other "attributes" should be modifier selectors, and
|
8585
|
+
// their value should be obtained using attributeExpression (see below).
|
8264
8586
|
if(a === '') return this.vector;
|
8265
8587
|
return null;
|
8266
8588
|
}
|
8267
8589
|
|
8268
8590
|
attributeExpression(a) {
|
8269
|
-
// Returns expression for selector `a
|
8270
|
-
//
|
8591
|
+
// Returns expression for selector `a` (also considering wildcard
|
8592
|
+
// modifiers), or NULL if no such selector exists.
|
8593
|
+
// NOTE: selectors no longer are case-sensitive.
|
8271
8594
|
if(a) {
|
8272
|
-
|
8273
|
-
|
8274
|
-
if(m === a) return this.modifiers[m].expression;
|
8275
|
-
}
|
8595
|
+
const mm = this.matchingModifiers([a]);
|
8596
|
+
if(mm.length > 0) return mm[0].expression;
|
8276
8597
|
}
|
8277
8598
|
return null;
|
8278
8599
|
}
|
@@ -8303,15 +8624,22 @@ class Dataset {
|
|
8303
8624
|
if(this === MODEL.equations_dataset) {
|
8304
8625
|
// Equation identifiers cannot contain characters that have special
|
8305
8626
|
// meaning in a variable identifier
|
8306
|
-
s = s.replace(/[
|
8627
|
+
s = s.replace(/[\*\|\[\]\{\}\@\#]/g, '');
|
8307
8628
|
if(s !== selector) {
|
8308
|
-
UI.warn('Equation name cannot contain [, ], {, }, |, @,
|
8629
|
+
UI.warn('Equation name cannot contain [, ], {, }, |, @, # or *');
|
8630
|
+
return null;
|
8631
|
+
}
|
8632
|
+
// Wildcard selectors must be exactly 2 consecutive question marks,
|
8633
|
+
// so reduce longer sequences (no warning)
|
8634
|
+
s = s.replace(/\?\?+/g, '??');
|
8635
|
+
if(s.split('??').length > 2) {
|
8636
|
+
UI.warn('Equation name can contain only 1 wildcard');
|
8309
8637
|
return null;
|
8310
8638
|
}
|
8311
8639
|
// Reduce inner spaces to one, and trim outer spaces
|
8312
8640
|
s = s.replace(/\s+/g, ' ').trim();
|
8313
|
-
|
8314
|
-
|
8641
|
+
// Then prefix it when the IO context argument is defined
|
8642
|
+
if(ioc) s = ioc.actualName(s);
|
8315
8643
|
// If equation already exists, return its modifier
|
8316
8644
|
const id = UI.nameToID(s);
|
8317
8645
|
if(this.modifiers.hasOwnProperty(id)) return this.modifiers[id];
|
@@ -9749,6 +10077,9 @@ class ExperimentRunResult {
|
|
9749
10077
|
}
|
9750
10078
|
} else if(v instanceof Dataset) {
|
9751
10079
|
// This dataset will be an "outcome" dataset => store statistics only
|
10080
|
+
// @@TO DO: deal with wildcard equations: these will have *multiple*
|
10081
|
+
// vectors associated with numbered entities (via #) and therefore
|
10082
|
+
// *all* these results should be stored (with # replaced by its value)
|
9752
10083
|
this.x_variable = false;
|
9753
10084
|
this.object_id = v.identifier;
|
9754
10085
|
if(v === MODEL.equations_dataset && a) {
|