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