linny-r 1.3.3 → 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/console.js +17 -0
- package/package.json +1 -1
- package/server.js +17 -2
- 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 +174 -73
- package/static/scripts/linny-r-model.js +491 -154
- package/static/scripts/linny-r-utils.js +134 -12
- package/static/scripts/linny-r-vm.js +815 -396
@@ -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
|
@@ -1609,16 +1692,20 @@ class LinnyRModel {
|
|
1609
1692
|
extras = [],
|
1610
1693
|
from_tos = [],
|
1611
1694
|
xml = [],
|
1612
|
-
ft_xml = [],
|
1613
1695
|
extra_xml = [],
|
1696
|
+
ft_xml = [],
|
1697
|
+
selc_xml = [],
|
1614
1698
|
selected_xml = [];
|
1615
1699
|
for(let i = 0; i < this.selection.length; i++) {
|
1616
1700
|
const obj = this.selection[i];
|
1617
1701
|
entities[obj.type].push(obj);
|
1618
|
-
|
1702
|
+
if(obj instanceof Cluster) selc_xml.push(
|
1703
|
+
'<selc name="', xmlEncoded(obj.name),
|
1704
|
+
'" actor-name="', xmlEncoded(obj.actor.name), '"></selc>');
|
1705
|
+
selected_xml.push(`<sel>${xmlEncoded(obj.displayName)}</sel>`);
|
1619
1706
|
}
|
1620
|
-
// Expand clusters by adding all
|
1621
|
-
// lists
|
1707
|
+
// Expand (sub)clusters by adding all their model entities to their
|
1708
|
+
// respective lists
|
1622
1709
|
for(let i = 0; i < entities.Cluster.length; i++) {
|
1623
1710
|
const c = entities.Cluster[i];
|
1624
1711
|
c.clearAllProcesses();
|
@@ -1645,7 +1732,7 @@ class LinnyRModel {
|
|
1645
1732
|
}
|
1646
1733
|
// Only add the XML for notes in the selection
|
1647
1734
|
for(let i = 0; i < entities.Note.length; i++) {
|
1648
|
-
xml.push(
|
1735
|
+
xml.push(entities.Note[i].asXML);
|
1649
1736
|
}
|
1650
1737
|
for(let i = 0; i < entities.Product.length; i++) {
|
1651
1738
|
const p = entities.Product[i];
|
@@ -1701,7 +1788,11 @@ class LinnyRModel {
|
|
1701
1788
|
for(let i = 0; i < from_tos.length; i++) {
|
1702
1789
|
const p = from_tos[i];
|
1703
1790
|
ft_xml.push('<from-to type="', p.type, '" name="', xmlEncoded(p.name));
|
1704
|
-
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
|
+
}
|
1705
1796
|
ft_xml.push('"></from-to>');
|
1706
1797
|
}
|
1707
1798
|
for(let i = 0; i < extras.length; i++) {
|
@@ -1714,7 +1805,8 @@ class LinnyRModel {
|
|
1714
1805
|
'"><entities>', xml.join(''),
|
1715
1806
|
'</entities><from-tos>', ft_xml.join(''),
|
1716
1807
|
'</from-tos><extras>', extra_xml.join(''),
|
1717
|
-
'</extras><
|
1808
|
+
'</extras><selected-clusters>', selc_xml.join(''),
|
1809
|
+
'</selected-clusters><selection>', selected_xml.join(''),
|
1718
1810
|
'</selection></copy>'].join('');
|
1719
1811
|
}
|
1720
1812
|
|
@@ -1819,7 +1911,7 @@ class LinnyRModel {
|
|
1819
1911
|
for(let i = 0; i < notes.length; i++) {
|
1820
1912
|
const c = this.addNote();
|
1821
1913
|
if(c) {
|
1822
|
-
c.copyPropertiesFrom(notes[i]);
|
1914
|
+
c.copyPropertiesFrom(notes[i], renumber);
|
1823
1915
|
c.x += 100;
|
1824
1916
|
c.y += 100;
|
1825
1917
|
cloned_selection.push(c);
|
@@ -2183,7 +2275,7 @@ class LinnyRModel {
|
|
2183
2275
|
|
2184
2276
|
get datasetVariables() {
|
2185
2277
|
// Returns list with all ChartVariable objects in this model that
|
2186
|
-
// reference a regular dataset, i.e., not an equation
|
2278
|
+
// reference a regular dataset, i.e., not an equation.
|
2187
2279
|
const vl = [];
|
2188
2280
|
for(let i = 0; i < MODEL.charts.length; i++) {
|
2189
2281
|
const c = MODEL.charts[i];
|
@@ -2349,19 +2441,21 @@ class LinnyRModel {
|
|
2349
2441
|
|
2350
2442
|
parseXML(data) {
|
2351
2443
|
// Parse data string into XML tree
|
2352
|
-
try {
|
2444
|
+
// try {
|
2353
2445
|
// NOTE: Convert %23 back to # (escaped by function saveModel)
|
2354
2446
|
const xml = parseXML(data.replace(/%23/g, '#'));
|
2355
2447
|
// NOTE: loading, not including => make sure that IO context is NULL
|
2356
2448
|
IO_CONTEXT = null;
|
2357
2449
|
this.initFromXML(xml);
|
2358
2450
|
return true;
|
2451
|
+
/*
|
2359
2452
|
} catch(err) {
|
2360
2453
|
// Cursor is set to WAITING when loading starts
|
2361
2454
|
UI.normalCursor();
|
2362
2455
|
UI.alert('Error while parsing model: ' + err);
|
2363
2456
|
return false;
|
2364
2457
|
}
|
2458
|
+
*/
|
2365
2459
|
}
|
2366
2460
|
|
2367
2461
|
initFromXML(node) {
|
@@ -2956,7 +3050,14 @@ class LinnyRModel {
|
|
2956
3050
|
}
|
2957
3051
|
|
2958
3052
|
resetExpressions() {
|
2959
|
-
// 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
|
+
|
2960
3061
|
let obj, l, p;
|
2961
3062
|
for(obj in this.actors) if(this.actors.hasOwnProperty(obj)) {
|
2962
3063
|
p = this.actors[obj];
|
@@ -2974,9 +3075,7 @@ class LinnyRModel {
|
|
2974
3075
|
this.cleanVector(p.cash_in, 0, 0);
|
2975
3076
|
this.cleanVector(p.cash_out, 0, 0);
|
2976
3077
|
// NOTE: note fields also must be reset
|
2977
|
-
|
2978
|
-
p.notes[i].parsed = false;
|
2979
|
-
}
|
3078
|
+
p.resetNoteFields();
|
2980
3079
|
}
|
2981
3080
|
for(obj in this.processes) if(this.processes.hasOwnProperty(obj)) {
|
2982
3081
|
p = this.processes[obj];
|
@@ -3236,6 +3335,7 @@ class LinnyRModel {
|
|
3236
3335
|
for(let i = 0; i < constraints.length; i++) {
|
3237
3336
|
const
|
3238
3337
|
c = constraints[i],
|
3338
|
+
// NOTE: constraints in list have levels greater than near-zero
|
3239
3339
|
fl = c.from_node.actualLevel(t),
|
3240
3340
|
tl = c.to_node.actualLevel(t);
|
3241
3341
|
let tcp;
|
@@ -3306,10 +3406,10 @@ class LinnyRModel {
|
|
3306
3406
|
if(af > VM.NEAR_ZERO) {
|
3307
3407
|
// Prevent division by zero
|
3308
3408
|
// NOTE: level can be zero even if actual flow > 0!
|
3309
|
-
const al = p.
|
3409
|
+
const al = p.nonZeroLevel(dt);
|
3310
3410
|
// NOTE: scale to level only when level > 1, or fixed
|
3311
3411
|
// costs for start-up or first commit will be amplified
|
3312
|
-
if(al > VM.
|
3412
|
+
if(al > VM.NEAR_ZERO) cp -= pr * af / Math.max(al, 1);
|
3313
3413
|
}
|
3314
3414
|
}
|
3315
3415
|
}
|
@@ -3428,8 +3528,8 @@ class LinnyRModel {
|
|
3428
3528
|
p.cost_price[t] = cp;
|
3429
3529
|
// For stocks, the CP includes stock price on t-1
|
3430
3530
|
if(p.is_buffer) {
|
3431
|
-
const prevl = p.
|
3432
|
-
if(prevl >
|
3531
|
+
const prevl = p.nonZeroLevel(t-1);
|
3532
|
+
if(prevl > VM.NEAR_ZERO) {
|
3433
3533
|
cp = (cnp + prevl * p.stockPrice(t-1)) / (qnp + prevl);
|
3434
3534
|
}
|
3435
3535
|
p.stock_price[t] = cp;
|
@@ -3486,7 +3586,7 @@ class LinnyRModel {
|
|
3486
3586
|
// Then (also) look for links having AF = 0 ...
|
3487
3587
|
for(let i = links.length-1; i >= 0; i--) {
|
3488
3588
|
const af = links[i].actualFlow(t);
|
3489
|
-
if(Math.abs(af) < VM.
|
3589
|
+
if(Math.abs(af) < VM.NEAR_ZERO) {
|
3490
3590
|
// ... and set their UCP to 0
|
3491
3591
|
links[i].unit_cost_price = 0;
|
3492
3592
|
links.splice(i, 1);
|
@@ -3522,7 +3622,7 @@ class LinnyRModel {
|
|
3522
3622
|
let hcp = VM.MINUS_INFINITY;
|
3523
3623
|
for(let i = 0; i < p.inputs.length; i++) {
|
3524
3624
|
const l = p.inputs[i];
|
3525
|
-
if(l.from_node instanceof Process && l.actualFlow(t) >
|
3625
|
+
if(l.from_node instanceof Process && l.actualFlow(t) > VM.NEAR_ZERO) {
|
3526
3626
|
const ld = l.actualDelay(t);
|
3527
3627
|
// NOTE: only consider the allocated share of cost
|
3528
3628
|
let cp = l.from_node.costPrice(t - ld) * l.share_of_cost;
|
@@ -4515,7 +4615,8 @@ class ObjectWithXYWH {
|
|
4515
4615
|
}
|
4516
4616
|
|
4517
4617
|
alignToGrid() {
|
4518
|
-
// 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
|
4519
4620
|
const
|
4520
4621
|
ox = this.x,
|
4521
4622
|
oy = this.y,
|
@@ -4536,22 +4637,29 @@ class ObjectWithXYWH {
|
|
4536
4637
|
} // END of CLASS ObjectWithXYWH
|
4537
4638
|
|
4538
4639
|
|
4539
|
-
// CLASS NoteField: numeric value of "field" [[
|
4640
|
+
// CLASS NoteField: numeric value of "field" [[variable]] in note text
|
4540
4641
|
class NoteField {
|
4541
|
-
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
|
4542
4644
|
// `f` holds the unmodified tag string [[dataset]] to be replaced by
|
4543
4645
|
// the value of vector or expression `o` for the current time step;
|
4544
|
-
// if specified, `u` is the unit of the value to be displayed,
|
4545
|
-
// `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;
|
4546
4650
|
this.field = f;
|
4547
4651
|
this.object = o;
|
4548
4652
|
this.unit = u;
|
4549
4653
|
this.multiplier = m;
|
4654
|
+
this.wildcard_number = (w ? parseInt(w) : false);
|
4550
4655
|
}
|
4551
4656
|
|
4552
4657
|
get value() {
|
4553
4658
|
// Returns the numeric value of this note field as a numeric string
|
4554
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';
|
4555
4663
|
let v = VM.UNDEFINED;
|
4556
4664
|
const t = MODEL.t;
|
4557
4665
|
if(Array.isArray(this.object)) {
|
@@ -4563,7 +4671,7 @@ class NoteField {
|
|
4563
4671
|
v = MODEL.flowBalance(this.object, t);
|
4564
4672
|
} else if(this.object instanceof Expression) {
|
4565
4673
|
// Object is an expression
|
4566
|
-
v = this.object.result(t);
|
4674
|
+
v = this.object.result(t, this.wildcard_number);
|
4567
4675
|
} else if(typeof this.object === 'number') {
|
4568
4676
|
v = this.object;
|
4569
4677
|
} else {
|
@@ -4588,7 +4696,7 @@ class Note extends ObjectWithXYWH {
|
|
4588
4696
|
super(cluster);
|
4589
4697
|
const dt = new Date();
|
4590
4698
|
// NOTE: use timestamp in msec to generate a unique identifier
|
4591
|
-
this.timestamp = dt.getTime();
|
4699
|
+
this.timestamp = dt.getTime();
|
4592
4700
|
this.contents = '';
|
4593
4701
|
this.lines = [];
|
4594
4702
|
this.fields = [];
|
@@ -4604,17 +4712,68 @@ class Note extends ObjectWithXYWH {
|
|
4604
4712
|
return 'Note';
|
4605
4713
|
}
|
4606
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
|
+
|
4607
4722
|
get displayName() {
|
4608
|
-
|
4609
|
-
this.
|
4723
|
+
const
|
4724
|
+
n = this.number,
|
4725
|
+
type = (n ? `Numbered note #${n}` : 'Note');
|
4726
|
+
return `${this.clusterPrefix}${type} at (${this.x}, ${this.y})`;
|
4610
4727
|
}
|
4611
4728
|
|
4612
|
-
get
|
4613
|
-
// Returns the
|
4614
|
-
// 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];
|
4615
4735
|
return '';
|
4616
4736
|
}
|
4617
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
|
+
|
4618
4777
|
get asXML() {
|
4619
4778
|
return ['<note><timestamp>', this.timestamp,
|
4620
4779
|
'</timestamp><contents>', xmlEncoded(this.contents),
|
@@ -4649,27 +4808,27 @@ class Note extends ObjectWithXYWH {
|
|
4649
4808
|
}
|
4650
4809
|
|
4651
4810
|
setCluster(c) {
|
4652
|
-
// Place this note into the specified cluster `c
|
4811
|
+
// Place this note into the specified cluster `c`.
|
4653
4812
|
if(this.cluster) {
|
4654
|
-
// Remove this note from its current cluster's note list
|
4813
|
+
// Remove this note from its current cluster's note list.
|
4655
4814
|
const i = this.cluster.notes.indexOf(this);
|
4656
4815
|
if(i >= 0) this.cluster.notes.splice(i, 1);
|
4657
4816
|
// Set its new cluster pointer...
|
4658
4817
|
this.cluster = c;
|
4659
|
-
// ... and add it to the new cluster's note list
|
4818
|
+
// ... and add it to the new cluster's note list.
|
4660
4819
|
if(c.notes.indexOf(this) < 0) c.notes.push(this);
|
4661
4820
|
}
|
4662
4821
|
}
|
4663
4822
|
|
4664
4823
|
get tagList() {
|
4665
|
-
// Returns a list of matches for [[...]], or NULL if none
|
4824
|
+
// Returns a list of matches for [[...]], or NULL if none.
|
4666
4825
|
return this.contents.match(/\[\[[^\]]+\]\]/g);
|
4667
4826
|
}
|
4668
4827
|
|
4669
4828
|
parseFields() {
|
4670
|
-
// 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.
|
4671
4830
|
// NOTE: this does not affect the text itself; tags will be replaced
|
4672
|
-
// by numerical values only when drawing the note
|
4831
|
+
// by numerical values only when drawing the note.
|
4673
4832
|
this.fields.length = 0;
|
4674
4833
|
const tags = this.tagList;
|
4675
4834
|
if(tags) {
|
@@ -4679,19 +4838,25 @@ class Note extends ObjectWithXYWH {
|
|
4679
4838
|
inner = tag.slice(2, tag.length - 2).trim(),
|
4680
4839
|
bar = inner.lastIndexOf('|'),
|
4681
4840
|
arrow = inner.lastIndexOf('->');
|
4682
|
-
//
|
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.
|
4683
4848
|
let ena,
|
4684
4849
|
from_unit = '1',
|
4685
4850
|
to_unit = '',
|
4686
4851
|
multiplier = 1;
|
4687
4852
|
if(arrow > bar) {
|
4688
|
-
// Now for sure it is entity->unit or entity|attr->unit
|
4853
|
+
// Now for sure it is entity->unit or entity|attr->unit.
|
4689
4854
|
ena = inner.split('->');
|
4690
4855
|
// As example, assume that unit = 'kWh' (so the value of the
|
4691
|
-
// field should be displayed in kilowatthour)
|
4692
|
-
// NOTE: use .trim() instead of UI.cleanName(...) here
|
4693
|
-
//
|
4694
|
-
// 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.
|
4695
4860
|
to_unit = ena[1].trim();
|
4696
4861
|
ena = ena[0].split('|');
|
4697
4862
|
if(!MODEL.scale_units.hasOwnProperty(to_unit)) {
|
@@ -4701,27 +4866,56 @@ class Note extends ObjectWithXYWH {
|
|
4701
4866
|
} else {
|
4702
4867
|
ena = inner.split('|');
|
4703
4868
|
}
|
4704
|
-
// Look up entity for name and attribute
|
4705
|
-
|
4706
|
-
|
4707
|
-
|
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'.
|
4708
4901
|
if(obj.dataset !== MODEL.equations_dataset) {
|
4709
4902
|
from_unit = obj.dataset.scale_unit;
|
4710
4903
|
multiplier = MODEL.unitConversionMultiplier(from_unit, to_unit);
|
4711
4904
|
}
|
4712
|
-
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));
|
4713
4907
|
} else if(obj) {
|
4714
|
-
// If attribute omitted, use default attribute of entity type
|
4908
|
+
// If attribute omitted, use default attribute of entity type.
|
4715
4909
|
const attr = (ena.length > 1 ? ena[1].trim() : obj.defaultAttribute);
|
4716
4910
|
let val = null;
|
4717
|
-
// NOTE: for datasets, use the active modifier
|
4911
|
+
// NOTE: for datasets, use the active modifier if no attribute.
|
4718
4912
|
if(!attr && obj instanceof Dataset) {
|
4719
4913
|
val = obj.activeModifierExpression;
|
4720
4914
|
} else {
|
4721
|
-
// Variable may specify a vector-type attribute
|
4915
|
+
// Variable may specify a vector-type attribute.
|
4722
4916
|
val = obj.attributeValue(attr);
|
4723
4917
|
}
|
4724
|
-
// If not, it may be a cluster unit balance
|
4918
|
+
// If not, it may be a cluster unit balance.
|
4725
4919
|
if(!val && attr.startsWith('=') && obj instanceof Cluster) {
|
4726
4920
|
val = {c: obj, u: attr.substring(1).trim()};
|
4727
4921
|
from_unit = val.u;
|
@@ -4747,9 +4941,20 @@ class Note extends ObjectWithXYWH {
|
|
4747
4941
|
} else if(attr === 'CI' || attr === 'CO' || attr === 'CF') {
|
4748
4942
|
from_unit = MODEL.currency_unit;
|
4749
4943
|
}
|
4750
|
-
// If
|
4944
|
+
// If still no value, `attr` may be an expression-type attribute.
|
4751
4945
|
if(!val) {
|
4752
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
|
+
}
|
4753
4958
|
if(obj instanceof Product) {
|
4754
4959
|
if(attr === 'IL' || attr === 'LB' || attr === 'UB') {
|
4755
4960
|
from_unit = obj.scale_unit;
|
@@ -4758,16 +4963,15 @@ class Note extends ObjectWithXYWH {
|
|
4758
4963
|
}
|
4759
4964
|
}
|
4760
4965
|
}
|
4761
|
-
// If no TO unit, add the FROM unit
|
4966
|
+
// If no TO unit, add the FROM unit.
|
4762
4967
|
if(to_unit === '') to_unit = from_unit;
|
4763
4968
|
if(val) {
|
4764
4969
|
multiplier = MODEL.unitConversionMultiplier(from_unit, to_unit);
|
4765
|
-
this.fields.push(new NoteField(tag, val, to_unit,
|
4970
|
+
this.fields.push(new NoteField(this, tag, val, to_unit,
|
4971
|
+
multiplier, wildcard));
|
4766
4972
|
} else {
|
4767
4973
|
UI.warn(`Unknown ${obj.type.toLowerCase()} attribute "${attr}"`);
|
4768
4974
|
}
|
4769
|
-
} else {
|
4770
|
-
UI.warn(`Unknown model entity "${ena[0].trim()}"`);
|
4771
4975
|
}
|
4772
4976
|
}
|
4773
4977
|
}
|
@@ -4775,20 +4979,24 @@ class Note extends ObjectWithXYWH {
|
|
4775
4979
|
}
|
4776
4980
|
|
4777
4981
|
get fieldEntities() {
|
4778
|
-
// Return a list with names of entities used in fields
|
4982
|
+
// Return a list with names of entities used in fields.
|
4779
4983
|
const
|
4780
4984
|
fel = [],
|
4781
4985
|
tags = this.tagList;
|
4782
4986
|
for(let i = 0; i < tags.length; i++) {
|
4783
4987
|
const
|
4784
4988
|
tag = tags[i],
|
4785
|
-
|
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()),
|
4786
4992
|
vb = inner.lastIndexOf('|'),
|
4787
4993
|
ua = inner.lastIndexOf('->');
|
4788
4994
|
if(vb >= 0) {
|
4995
|
+
// Vertical bar? Then the entity name is the left part.
|
4789
4996
|
addDistinct(inner.slice(0, vb), fel);
|
4790
4997
|
} else if(ua >= 0 &&
|
4791
4998
|
MODEL.scale_units.hasOwnProperty(inner.slice(ua + 2))) {
|
4999
|
+
// Unit arrow? Then trim the "->unit" part.
|
4792
5000
|
addDistinct(inner.slice(0, ua), fel);
|
4793
5001
|
} else {
|
4794
5002
|
addDistinct(inner, fel);
|
@@ -4812,29 +5020,30 @@ class Note extends ObjectWithXYWH {
|
|
4812
5020
|
}
|
4813
5021
|
|
4814
5022
|
rewriteFields(en1, en2) {
|
4815
|
-
// Rename fields that reference entity name `en1` to reference `en2`
|
4816
|
-
//
|
5023
|
+
// Rename fields that reference entity name `en1` to reference `en2`
|
5024
|
+
// instead.
|
5025
|
+
// NOTE: This does not affect the expression code.
|
4817
5026
|
if(en1 === en2) return;
|
4818
5027
|
for(let i = 0; i < this.fields.length; i++) {
|
4819
5028
|
const
|
4820
5029
|
f = this.fields[i],
|
4821
|
-
// Trim the double brackets on both sides
|
4822
|
-
tag = f.field.slice(2, f.field.length - 2);
|
4823
|
-
// 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).
|
4824
5033
|
let e = tag,
|
4825
5034
|
a = '',
|
4826
5035
|
vb = tag.lastIndexOf('|'),
|
4827
5036
|
ua = tag.lastIndexOf('->');
|
4828
5037
|
if(vb >= 0) {
|
4829
5038
|
e = tag.slice(0, vb);
|
4830
|
-
// NOTE:
|
5039
|
+
// NOTE: Attribute string includes the vertical bar '|'.
|
4831
5040
|
a = tag.slice(vb);
|
4832
5041
|
} else if(ua >= 0 && MODEL.scale_units.hasOwnProperty(tag.slice(ua + 2))) {
|
4833
5042
|
e = tag.slice(0, ua);
|
4834
|
-
// NOTE:
|
5043
|
+
// NOTE: Attribute string includes the unit conversion arrow '->'.
|
4835
5044
|
a = tag.slice(ua);
|
4836
5045
|
}
|
4837
|
-
// Check for match
|
5046
|
+
// Check for match.
|
4838
5047
|
const r = UI.replaceEntity(e, en1, en2);
|
4839
5048
|
if(r) {
|
4840
5049
|
e = `[[${r}${a}]]`;
|
@@ -4845,6 +5054,8 @@ class Note extends ObjectWithXYWH {
|
|
4845
5054
|
}
|
4846
5055
|
|
4847
5056
|
get evaluateFields() {
|
5057
|
+
// Returns the text content of this note with all tags replaced
|
5058
|
+
// by their note field values.
|
4848
5059
|
if(!this.parsed) this.parseFields();
|
4849
5060
|
let txt = this.contents;
|
4850
5061
|
for(let i = 0; i < this.fields.length; i++) {
|
@@ -4855,51 +5066,66 @@ class Note extends ObjectWithXYWH {
|
|
4855
5066
|
}
|
4856
5067
|
|
4857
5068
|
resize() {
|
4858
|
-
// Resizes the note; returns TRUE iff size has changed
|
5069
|
+
// Resizes the note; returns TRUE iff size has changed.
|
4859
5070
|
let txt = this.evaluateFields;
|
4860
5071
|
const
|
4861
5072
|
w = this.width,
|
4862
5073
|
h = this.height,
|
4863
|
-
// Minimumm note width of 10 characters
|
5074
|
+
// Minimumm note width of 10 characters.
|
4864
5075
|
n = Math.max(txt.length, 10),
|
4865
5076
|
fh = UI.textSize('hj').height;
|
4866
|
-
// Approximate the width to obtain a rectangle
|
5077
|
+
// Approximate the width to obtain a rectangle.
|
4867
5078
|
// NOTE: 3:1 may seem exagerated, but characters are higher than wide,
|
4868
|
-
// 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.
|
4869
5080
|
let tw = Math.ceil(3*Math.sqrt(n)) * fh / 2;
|
4870
5081
|
this.lines = UI.stringToLineArray(txt, tw).join('\n');
|
4871
5082
|
let bb = UI.textSize(this.lines, 8);
|
4872
|
-
//
|
5083
|
+
// Aim to make the shape wider than tall.
|
4873
5084
|
let nw = bb.width,
|
4874
5085
|
nh = bb.height;
|
4875
5086
|
while(bb.width < bb.height * 1.7) {
|
4876
5087
|
tw *= 1.2;
|
4877
5088
|
this.lines = UI.stringToLineArray(txt, tw).join('\n');
|
4878
5089
|
bb = UI.textSize(this.lines, 8);
|
4879
|
-
// Prevent infinite loop
|
5090
|
+
// Prevent infinite loop.
|
4880
5091
|
if(nw <= bb.width || nh > bb.height) break;
|
4881
5092
|
}
|
4882
5093
|
this.height = 1.05 * (bb.height + 6);
|
4883
5094
|
this.width = bb.width + 6;
|
5095
|
+
// Boolean return value indicates whether size has changed.
|
4884
5096
|
return this.width != w || this.height != h;
|
4885
5097
|
}
|
4886
5098
|
|
4887
5099
|
containsPoint(mpx, mpy) {
|
5100
|
+
// Returns TRUE iff given coordinates lie within the note rectangle.
|
4888
5101
|
return (Math.abs(mpx - this.x) <= this.width / 2 &&
|
4889
5102
|
Math.abs(mpy - this.y) <= this.height / 2);
|
4890
5103
|
}
|
4891
5104
|
|
4892
|
-
copyPropertiesFrom(n) {
|
4893
|
-
//
|
5105
|
+
copyPropertiesFrom(n, renumber=false) {
|
5106
|
+
// Sets properties to be identical to those of note `n`.
|
4894
5107
|
this.x = n.x;
|
4895
5108
|
this.y = n.y;
|
4896
|
-
|
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.
|
4897
5123
|
this.color.text = n.color.text;
|
4898
5124
|
this.parsed = false;
|
4899
5125
|
}
|
4900
5126
|
|
4901
5127
|
differences(n) {
|
4902
|
-
// Return "dictionary" of differences, or NULL if none
|
5128
|
+
// Return "dictionary" of differences, or NULL if none.
|
4903
5129
|
const d = differences(this, n, UI.MC.NOTE_PROPS);
|
4904
5130
|
if(Object.keys(d).length > 0) return d;
|
4905
5131
|
return null;
|
@@ -4941,6 +5167,17 @@ class NodeBox extends ObjectWithXYWH {
|
|
4941
5167
|
// NOTE: Display nothing if entity is "black-boxed"
|
4942
5168
|
if(n.startsWith(UI.BLACK_BOX)) return '';
|
4943
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
|
+
}
|
4944
5181
|
if(DEBUGGING && MODEL.solved) {
|
4945
5182
|
n += ' [';
|
4946
5183
|
if(this instanceof Process || this instanceof Product) {
|
@@ -4965,10 +5202,11 @@ class NodeBox extends ObjectWithXYWH {
|
|
4965
5202
|
}
|
4966
5203
|
|
4967
5204
|
get numberContext() {
|
4968
|
-
// Returns the string to be used to evaluate #, so for clusters,
|
4969
|
-
// and products this is the string of trailing digits
|
4970
|
-
// of the node name, or if that does not end
|
4971
|
-
// 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
|
4972
5210
|
const sn = UI.prefixesAndName(this.name);
|
4973
5211
|
let nc = endsWithDigits(sn.pop());
|
4974
5212
|
while(!nc && sn.length > 0) {
|
@@ -4977,33 +5215,50 @@ class NodeBox extends ObjectWithXYWH {
|
|
4977
5215
|
return nc;
|
4978
5216
|
}
|
4979
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
|
+
|
4980
5234
|
rename(name, actor_name) {
|
4981
|
-
// Changes the name and/or actor name of this process, product
|
4982
|
-
//
|
4983
|
-
//
|
4984
|
-
//
|
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.
|
4985
5240
|
name = UI.cleanName(name);
|
4986
5241
|
if(!UI.validName(name)) {
|
4987
5242
|
UI.warningInvalidName(name);
|
4988
5243
|
return false;
|
4989
5244
|
}
|
4990
|
-
// Compose the full name
|
5245
|
+
// Compose the full name.
|
4991
5246
|
if(actor_name === '') actor_name = UI.NO_ACTOR;
|
4992
5247
|
let fn = name;
|
4993
5248
|
if(actor_name != UI.NO_ACTOR) fn += ` (${actor_name})`;
|
4994
5249
|
// Get the ID (derived from the full name) and check if MODEL already
|
4995
|
-
// contains another entity with this ID
|
5250
|
+
// contains another entity with this ID.
|
4996
5251
|
const
|
4997
5252
|
old_name = this.displayName,
|
4998
5253
|
old_id = this.identifier,
|
4999
5254
|
new_id = UI.nameToID(fn),
|
5000
5255
|
n = MODEL.nodeBoxByID(new_id);
|
5001
|
-
// If so, do NOT rename, but return this object instead
|
5002
|
-
// NOTE:
|
5003
|
-
// 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.
|
5004
5259
|
if(n && n !== this) return n;
|
5005
|
-
// Otherwise, if IDs differ, add this object under its new key, and
|
5006
|
-
// its old entry
|
5260
|
+
// Otherwise, if IDs differ, add this object under its new key, and
|
5261
|
+
// remove its old entry.
|
5007
5262
|
if(old_id != new_id) {
|
5008
5263
|
if(this instanceof Process) {
|
5009
5264
|
MODEL.processes[new_id] = this;
|
@@ -5015,21 +5270,21 @@ class NodeBox extends ObjectWithXYWH {
|
|
5015
5270
|
MODEL.clusters[new_id] = this;
|
5016
5271
|
delete MODEL.clusters[old_id];
|
5017
5272
|
} else {
|
5018
|
-
// NOTE:
|
5273
|
+
// NOTE: This should never happen => report an error.
|
5019
5274
|
UI.alert('Can only rename processes, products and clusters');
|
5020
5275
|
return false;
|
5021
5276
|
}
|
5022
5277
|
}
|
5023
|
-
// Change this object's name and actor
|
5278
|
+
// Change this object's name and actor.
|
5024
5279
|
this.actor = MODEL.addActor(actor_name);
|
5025
5280
|
this.name = name;
|
5026
|
-
// 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.
|
5027
5282
|
MODEL.cleanUpActors();
|
5028
5283
|
MODEL.replaceEntityInExpressions(old_name, this.displayName);
|
5029
5284
|
MODEL.inferIgnoredEntities();
|
5030
|
-
// NOTE:
|
5285
|
+
// NOTE: Renaming may affect the node's display size.
|
5031
5286
|
if(this.resize()) this.drawWithLinks();
|
5032
|
-
// NOTE:
|
5287
|
+
// NOTE: Only TRUE indicates a successful (cosmetic) name change.
|
5033
5288
|
return true;
|
5034
5289
|
}
|
5035
5290
|
|
@@ -5478,7 +5733,8 @@ class Cluster extends NodeBox {
|
|
5478
5733
|
}
|
5479
5734
|
|
5480
5735
|
attributeValue(a) {
|
5481
|
-
// Return the computed result for attribute a
|
5736
|
+
// Return the computed result for attribute `a`
|
5737
|
+
// For clusters, this is always a vector
|
5482
5738
|
if(a === 'CF') return this.cash_flow;
|
5483
5739
|
if(a === 'CI') return this.cash_in;
|
5484
5740
|
if(a === 'CO') return this.cash_out;
|
@@ -5745,6 +6001,27 @@ class Cluster extends NodeBox {
|
|
5745
6001
|
return notes;
|
5746
6002
|
}
|
5747
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
|
+
|
5748
6025
|
clearAllProcesses() {
|
5749
6026
|
// Clear `all_processes` property of this cluster AND of all its parent clusters
|
5750
6027
|
this.all_processes = null;
|
@@ -7147,6 +7424,7 @@ class Process extends Node {
|
|
7147
7424
|
}
|
7148
7425
|
|
7149
7426
|
get defaultAttribute() {
|
7427
|
+
// Default attribute of processes is their level
|
7150
7428
|
return 'L';
|
7151
7429
|
}
|
7152
7430
|
|
@@ -7162,6 +7440,7 @@ class Process extends Node {
|
|
7162
7440
|
}
|
7163
7441
|
|
7164
7442
|
attributeExpression(a) {
|
7443
|
+
// Processes have three expression attributes
|
7165
7444
|
if(a === 'LB') return this.lower_bound;
|
7166
7445
|
if(a === 'UB') {
|
7167
7446
|
return (this.equal_bounds ? this.lower_bound : this.upper_bound);
|
@@ -7557,6 +7836,7 @@ class Product extends Node {
|
|
7557
7836
|
}
|
7558
7837
|
|
7559
7838
|
get defaultAttribute() {
|
7839
|
+
// Products have their level as default attribute
|
7560
7840
|
return 'L';
|
7561
7841
|
}
|
7562
7842
|
|
@@ -7570,6 +7850,7 @@ class Product extends Node {
|
|
7570
7850
|
}
|
7571
7851
|
|
7572
7852
|
attributeExpression(a) {
|
7853
|
+
// Products have four expression attributes
|
7573
7854
|
if(a === 'LB') return this.lower_bound;
|
7574
7855
|
if(a === 'UB') {
|
7575
7856
|
return (this.equal_bounds ? this.lower_bound : this.upper_bound);
|
@@ -7580,6 +7861,7 @@ class Product extends Node {
|
|
7580
7861
|
}
|
7581
7862
|
|
7582
7863
|
changeScaleUnit(name) {
|
7864
|
+
// Changes the scale unit for this product to `name`
|
7583
7865
|
let su = MODEL.addScaleUnit(name);
|
7584
7866
|
if(su !== this.scale_unit) {
|
7585
7867
|
this.scale_unit = su;
|
@@ -7867,6 +8149,7 @@ class Link {
|
|
7867
8149
|
}
|
7868
8150
|
|
7869
8151
|
get defaultAttribute() {
|
8152
|
+
// For links, the default attribute is their actual flow
|
7870
8153
|
return 'F';
|
7871
8154
|
}
|
7872
8155
|
|
@@ -7878,6 +8161,7 @@ class Link {
|
|
7878
8161
|
}
|
7879
8162
|
|
7880
8163
|
attributeExpression(a) {
|
8164
|
+
// Links have two expression attributes
|
7881
8165
|
if(a === 'R') return this.relative_rate;
|
7882
8166
|
if(a === 'D') return this.flow_delay;
|
7883
8167
|
return null;
|
@@ -7978,20 +8262,45 @@ class DatasetModifier {
|
|
7978
8262
|
}
|
7979
8263
|
|
7980
8264
|
get hasWildcards() {
|
7981
|
-
|
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;
|
7982
8285
|
}
|
7983
8286
|
|
7984
8287
|
match(s) {
|
7985
|
-
if
|
7986
|
-
|
7987
|
-
|
7988
|
-
|
7989
|
-
|
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);
|
7990
8295
|
} else {
|
7991
|
-
|
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}$`);
|
7992
8300
|
}
|
8301
|
+
return re.test(s);
|
7993
8302
|
}
|
7994
|
-
|
8303
|
+
|
7995
8304
|
} // END of class DatasetModifier
|
7996
8305
|
|
7997
8306
|
|
@@ -8056,7 +8365,9 @@ class Dataset {
|
|
8056
8365
|
}
|
8057
8366
|
|
8058
8367
|
get numberContext() {
|
8059
|
-
// 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)
|
8060
8371
|
const sn = UI.prefixesAndName(this.name);
|
8061
8372
|
let nc = endsWithDigits(sn.pop());
|
8062
8373
|
while(!nc && sn.length > 0) {
|
@@ -8064,7 +8375,7 @@ class Dataset {
|
|
8064
8375
|
}
|
8065
8376
|
return nc;
|
8066
8377
|
}
|
8067
|
-
|
8378
|
+
|
8068
8379
|
get selectorList() {
|
8069
8380
|
// Returns sorted list of selectors (those with wildcards last)
|
8070
8381
|
const sl = [];
|
@@ -8084,14 +8395,45 @@ class Dataset {
|
|
8084
8395
|
return sl;
|
8085
8396
|
}
|
8086
8397
|
|
8087
|
-
|
8088
|
-
// Returns TRUE if
|
8089
|
-
for
|
8090
|
-
|
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;
|
8091
8428
|
}
|
8092
8429
|
return true;
|
8093
8430
|
}
|
8094
8431
|
|
8432
|
+
get allModifiersAreStatic() {
|
8433
|
+
// Returns TRUE if all modifier expressions are static
|
8434
|
+
return this.modifiersAreStatic(Object.keys(this.modifiers));
|
8435
|
+
}
|
8436
|
+
|
8095
8437
|
get inferPrefixableModifiers() {
|
8096
8438
|
// Returns list of dataset modifiers with expressions that do not
|
8097
8439
|
// reference any variable and hence could probably better be represented
|
@@ -8135,21 +8477,6 @@ class Dataset {
|
|
8135
8477
|
}
|
8136
8478
|
}
|
8137
8479
|
|
8138
|
-
matchingModifiers(l) {
|
8139
|
-
// Returns the list of selectors of this dataset (in order: from most to
|
8140
|
-
// least specific) that match with 1 or more elements of `l`
|
8141
|
-
const
|
8142
|
-
sl = this.selectorList,
|
8143
|
-
shared = [];
|
8144
|
-
for(let i = 0; i < l.length; i++) {
|
8145
|
-
for(let j = 0; j < sl.length; j++) {
|
8146
|
-
const m = this.modifiers[UI.nameToID(sl[j])];
|
8147
|
-
if(m.match(l[i])) addDistinct(m, shared);
|
8148
|
-
}
|
8149
|
-
}
|
8150
|
-
return shared;
|
8151
|
-
}
|
8152
|
-
|
8153
8480
|
get dataString() {
|
8154
8481
|
// Data is stored simply as semicolon-separated floating point numbers,
|
8155
8482
|
// with N-digit precision to keep model files compact (default: N = 8)
|
@@ -8252,21 +8579,21 @@ class Dataset {
|
|
8252
8579
|
}
|
8253
8580
|
|
8254
8581
|
attributeValue(a) {
|
8255
|
-
// Returns the computed result for attribute `a
|
8582
|
+
// Returns the computed result for attribute `a`.
|
8256
8583
|
// NOTE: Datasets have ONE attribute (their vector) denoted by the empty
|
8257
|
-
// 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).
|
8258
8586
|
if(a === '') return this.vector;
|
8259
8587
|
return null;
|
8260
8588
|
}
|
8261
8589
|
|
8262
8590
|
attributeExpression(a) {
|
8263
|
-
// Returns expression for selector `a
|
8264
|
-
//
|
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.
|
8265
8594
|
if(a) {
|
8266
|
-
|
8267
|
-
|
8268
|
-
if(m === a) return this.modifiers[m].expression;
|
8269
|
-
}
|
8595
|
+
const mm = this.matchingModifiers([a]);
|
8596
|
+
if(mm.length > 0) return mm[0].expression;
|
8270
8597
|
}
|
8271
8598
|
return null;
|
8272
8599
|
}
|
@@ -8297,15 +8624,22 @@ class Dataset {
|
|
8297
8624
|
if(this === MODEL.equations_dataset) {
|
8298
8625
|
// Equation identifiers cannot contain characters that have special
|
8299
8626
|
// meaning in a variable identifier
|
8300
|
-
s = s.replace(/[
|
8627
|
+
s = s.replace(/[\*\|\[\]\{\}\@\#]/g, '');
|
8301
8628
|
if(s !== selector) {
|
8302
|
-
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');
|
8303
8637
|
return null;
|
8304
8638
|
}
|
8305
8639
|
// Reduce inner spaces to one, and trim outer spaces
|
8306
8640
|
s = s.replace(/\s+/g, ' ').trim();
|
8307
|
-
|
8308
|
-
|
8641
|
+
// Then prefix it when the IO context argument is defined
|
8642
|
+
if(ioc) s = ioc.actualName(s);
|
8309
8643
|
// If equation already exists, return its modifier
|
8310
8644
|
const id = UI.nameToID(s);
|
8311
8645
|
if(this.modifiers.hasOwnProperty(id)) return this.modifiers[id];
|
@@ -9743,6 +10077,9 @@ class ExperimentRunResult {
|
|
9743
10077
|
}
|
9744
10078
|
} else if(v instanceof Dataset) {
|
9745
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)
|
9746
10083
|
this.x_variable = false;
|
9747
10084
|
this.object_id = v.identifier;
|
9748
10085
|
if(v === MODEL.equations_dataset && a) {
|