linny-r 2.0.8 → 2.0.9

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.
Files changed (31) hide show
  1. package/README.md +3 -40
  2. package/package.json +1 -1
  3. package/server.js +19 -157
  4. package/static/index.html +58 -20
  5. package/static/linny-r.css +20 -16
  6. package/static/scripts/iro.min.js +7 -7
  7. package/static/scripts/linny-r-ctrl.js +50 -72
  8. package/static/scripts/linny-r-gui-actor-manager.js +23 -33
  9. package/static/scripts/linny-r-gui-chart-manager.js +43 -41
  10. package/static/scripts/linny-r-gui-constraint-editor.js +6 -10
  11. package/static/scripts/linny-r-gui-controller.js +254 -230
  12. package/static/scripts/linny-r-gui-dataset-manager.js +17 -36
  13. package/static/scripts/linny-r-gui-documentation-manager.js +11 -17
  14. package/static/scripts/linny-r-gui-equation-manager.js +22 -22
  15. package/static/scripts/linny-r-gui-experiment-manager.js +102 -129
  16. package/static/scripts/linny-r-gui-file-manager.js +42 -48
  17. package/static/scripts/linny-r-gui-finder.js +105 -51
  18. package/static/scripts/linny-r-gui-model-autosaver.js +2 -4
  19. package/static/scripts/linny-r-gui-monitor.js +35 -41
  20. package/static/scripts/linny-r-gui-paper.js +42 -70
  21. package/static/scripts/linny-r-gui-power-grid-manager.js +31 -34
  22. package/static/scripts/linny-r-gui-receiver.js +1 -2
  23. package/static/scripts/linny-r-gui-repository-browser.js +44 -46
  24. package/static/scripts/linny-r-gui-scale-unit-manager.js +32 -32
  25. package/static/scripts/linny-r-gui-sensitivity-analysis.js +61 -67
  26. package/static/scripts/linny-r-gui-undo-redo.js +94 -95
  27. package/static/scripts/linny-r-milp.js +20 -24
  28. package/static/scripts/linny-r-model.js +1832 -2248
  29. package/static/scripts/linny-r-utils.js +27 -27
  30. package/static/scripts/linny-r-vm.js +801 -905
  31. package/static/show-png.html +0 -113
@@ -298,8 +298,7 @@ class GroupPropertiesDialog extends ModalDialog {
298
298
  const
299
299
  propname = this.fields[name],
300
300
  prop = obj[propname];
301
- for(let i = 0; i < this.group.length; i++) {
302
- const ge = this.group[i];
301
+ for(const ge of this.group) {
303
302
  // NOTE: For links, special care must be taken.
304
303
  if(!(ge instanceof Link) ||
305
304
  this.validLinkProperty(ge, propname, prop)) {
@@ -332,12 +331,9 @@ class GUIController extends Controller {
332
331
  ['chrome', 'Chrome'],
333
332
  ['firefox', 'Firefox'],
334
333
  ['safari', 'Safari']];
335
- for(let i = 0; i < browsers.length; i++) {
336
- const b = browsers[i];
337
- if(ua.indexOf(b[0]) >= 0) {
338
- this.browser_name = b[1];
339
- break;
340
- }
334
+ for(const b of browsers) if(ua.indexOf(b[0]) >= 0) {
335
+ this.browser_name = b[1];
336
+ break;
341
337
  }
342
338
  // Display version number as clickable link just below the Linny-R logo.
343
339
  this.version_number = LINNY_R_VERSION;
@@ -363,12 +359,22 @@ class GUIController extends Controller {
363
359
  this.mouse_y = 0;
364
360
  this.mouse_down_x = 0;
365
361
  this.mouse_down_y = 0;
362
+ // When clicking on a node, difference between cursor coordinates
363
+ // and node coordinates is recorded.
366
364
  this.move_dx = 0;
367
365
  this.move_dy = 0;
368
- this.start_sel_x = -1;
369
- this.start_sel_y = -1;
366
+ // When moving the cursor, the cumulative movement since the last
367
+ // mouse DOWN or UP event is recorded.
368
+ this.net_move_x = 0;
369
+ this.net_move_y = 0;
370
+ // When mouse button is pressed while some add button is active,
371
+ // the coordinates of the cursor are recorded.
370
372
  this.add_x = 0;
371
373
  this.add_y = 0;
374
+ // When mouse button is pressed while no node is under the cursor,
375
+ // cursor coordinates are recorded as origin of the drag rectangle.
376
+ this.start_sel_x = -1;
377
+ this.start_sel_y = -1;
372
378
  this.on_node = null;
373
379
  this.on_arrow = null;
374
380
  this.on_link = null;
@@ -393,7 +399,6 @@ class GUIController extends Controller {
393
399
  'D': 'dataset',
394
400
  'E': 'equation',
395
401
  'F': 'finder',
396
- 'G': 'savediagram', // G for "Graph" (as Scalable Vector Graphics image)
397
402
  'H': 'receiver', // activate receiver (H for "Host")
398
403
  'I': 'documentation',
399
404
  'J': 'sensitivity', // J for "Jitter"
@@ -421,7 +426,7 @@ class GUIController extends Controller {
421
426
  this.edit_btns = ['replace', 'clone', 'paste', 'delete', 'undo', 'redo'];
422
427
  this.model_btns = ['settings', 'save', 'repository', 'actors',
423
428
  'dataset', 'equation', 'chart', 'sensitivity', 'experiment',
424
- 'diagram', 'savediagram', 'finder', 'monitor', 'tex', 'solve'];
429
+ 'savediagram', 'finder', 'monitor', 'tex', 'solve'];
425
430
  this.other_btns = ['new', 'load', 'receiver', 'documentation',
426
431
  'parent', 'lift', 'solve', 'stop', 'reset', 'zoomin', 'zoomout',
427
432
  'stepback', 'stepforward', 'autosave', 'recall'];
@@ -429,8 +434,7 @@ class GUIController extends Controller {
429
434
  this.edit_btns, this.model_btns, this.other_btns);
430
435
 
431
436
  // Add all button DOM elements as controller properties.
432
- for(let i = 0; i < this.all_btns.length; i++) {
433
- const b = this.all_btns[i];
437
+ for(const b of this.all_btns) {
434
438
  this.buttons[b] = document.getElementById(b + '-btn');
435
439
  }
436
440
  this.active_button = null;
@@ -450,10 +454,9 @@ class GUIController extends Controller {
450
454
  const main_modals = ['logon', 'model', 'load', 'password', 'settings',
451
455
  'actors', 'add-process', 'add-product', 'move', 'note', 'clone',
452
456
  'replace', 'expression', 'server', 'solver'];
453
- for(let i = 0; i < main_modals.length; i++) {
454
- this.modals[main_modals[i]] = new ModalDialog(main_modals[i]);
455
- }
457
+ for(const m of main_modals) this.modals[m] = new ModalDialog(m);
456
458
 
459
+ // Property dialogs for entities may permit group editing.
457
460
  this.modals.cluster = new GroupPropertiesDialog('cluster', {
458
461
  'collapsed': 'collapsed',
459
462
  'ignore': 'ignore',
@@ -534,12 +537,18 @@ class GUIController extends Controller {
534
537
  this.cc.addEventListener('drop', (event) => UI.drop(event));
535
538
 
536
539
  // Disable dragging on all images.
537
- const
538
- imgs = document.getElementsByTagName('img'),
539
- nodrag = (event) => { event.preventDefault(); return false; };
540
- for(let i = 0; i < imgs.length; i++) {
541
- imgs[i].addEventListener('dragstart', nodrag);
540
+ const nodrag = (event) => { event.preventDefault(); return false; };
541
+ for(const img of document.getElementsByTagName('img')) {
542
+ img.addEventListener('dragstart', nodrag);
542
543
  }
544
+
545
+ // Moving cursor over Linny-R logo etc. should display information
546
+ // in Information & Documentation manager.
547
+ const lrf = () => DOCUMENTATION_MANAGER.clearEntity(true);
548
+ document.getElementById('static-icon').addEventListener('mousemove', lrf);
549
+ document.getElementById('linny-r-name').addEventListener('mousemove', lrf);
550
+ document.getElementById('linny-r-version-number')
551
+ .addEventListener('mousemove', lrf);
543
552
 
544
553
  // Make all buttons respond to a mouse click.
545
554
  this.buttons['new'].addEventListener('click',
@@ -552,10 +561,8 @@ class GUIController extends Controller {
552
561
  () => FILE_MANAGER.saveModel(event.shiftKey));
553
562
  this.buttons.actors.addEventListener('click',
554
563
  () => ACTOR_MANAGER.showDialog());
555
- this.buttons.diagram.addEventListener('click',
556
- () => FILE_MANAGER.renderDiagramAsPNG(event.shiftKey));
557
564
  this.buttons.savediagram.addEventListener('click',
558
- () => FILE_MANAGER.saveDiagramAsSVG(event.shiftKey));
565
+ () => FILE_MANAGER.saveDiagramAsSVG(event));
559
566
  this.buttons.receiver.addEventListener('click',
560
567
  () => RECEIVER.toggle());
561
568
  // NOTE: All draggable & resizable dialogs "toggle" show/hide.
@@ -651,11 +658,9 @@ class GUIController extends Controller {
651
658
  () => AUTO_SAVE.getAutoSavedModels());
652
659
 
653
660
  // Make "stay active" buttons respond to Shift-click.
654
- const
655
- tbs = document.getElementsByClassName('toggle'),
656
- tf = (event) => UI.toggleButton(event);
657
- for(let i = 0; i < tbs.length; i++) {
658
- tbs[i].addEventListener('click', tf);
661
+ const tf = (event) => UI.toggleButton(event);
662
+ for(const tb of document.getElementsByClassName('toggle')) {
663
+ tb.addEventListener('click', tf);
659
664
  }
660
665
 
661
666
  // Add listeners to OK and CANCEL buttons on main modal dialogs.
@@ -857,18 +862,16 @@ class GUIController extends Controller {
857
862
 
858
863
  // Make checkboxes respond to click.
859
864
  // NOTE: Checkbox-specific events must be bound AFTER this general setting.
860
- const
861
- cbs = document.getElementsByClassName('box'),
862
- cbf = (event) => UI.toggleBox(event);
863
- for(let i = 0; i < cbs.length; i++) {
864
- cbs[i].addEventListener('click', cbf);
865
+ const cbf = (event) => UI.toggleBox(event);
866
+ for(const cb of document.getElementsByClassName('box')) {
867
+ cb.addEventListener('click', cbf);
865
868
  }
866
- // Make infoline respond to `mouseenter`
869
+ // Make infoline respond to `mouseenter`.
867
870
  this.info_line = document.getElementById('info-line');
868
871
  this.info_line.addEventListener('mouseenter',
869
872
  (event) => DOCUMENTATION_MANAGER.showInfoMessages(event.shiftKey));
870
873
  // Ensure that all modal windows respond to ESCape
871
- // (and more in general to other special keys)
874
+ // (and more in general to other special keys).
872
875
  document.addEventListener('keydown', (event) => UI.checkModals(event));
873
876
  }
874
877
 
@@ -993,8 +996,7 @@ class GUIController extends Controller {
993
996
 
994
997
  drawLinkArrows(cluster, link) {
995
998
  // Draw all arrows in `cluster` that represent `link`.
996
- for(let i = 0; i < cluster.arrows.length; i++) {
997
- const a = cluster.arrows[i];
999
+ for(const a of cluster.arrows) {
998
1000
  if(a.links.indexOf(link) >= 0) this.paper.drawArrow(a);
999
1001
  }
1000
1002
  }
@@ -1011,8 +1013,7 @@ class GUIController extends Controller {
1011
1013
  if(VM.server === 'local host') {
1012
1014
  host.title = 'Linny-R directory is ' + VM.working_directory;
1013
1015
  }
1014
- for(let i = 0; i < VM.solver_list.length; i++) {
1015
- const s = VM.solver_list[i];
1016
+ for(const s of VM.solver_list) {
1016
1017
  html.push(['<option value="', s,
1017
1018
  (s === VM.solver_id ? '"selected="selected' : ''),
1018
1019
  '">', VM.solver_names[s], '</option>'].join(''));
@@ -1562,10 +1563,10 @@ class GUIController extends Controller {
1562
1563
 
1563
1564
  reorderDialogs() {
1564
1565
  // Set z-index of draggable dialogs according to their order
1565
- // (most recently shown or clicked on top)
1566
+ // (most recently shown or clicked on top).
1566
1567
  let z = 10;
1567
- for(let i = 0; i < this.dr_dialog_order.length; i++) {
1568
- this.dr_dialog_order[i].style.zIndex = z;
1568
+ for(const dd of this.dr_dialog_order) {
1569
+ dd.style.zIndex = z;
1569
1570
  z += 5;
1570
1571
  }
1571
1572
  }
@@ -1575,18 +1576,16 @@ class GUIController extends Controller {
1575
1576
  //
1576
1577
 
1577
1578
  enableButtons(btns) {
1578
- btns = btns.trim().split(/\s+/);
1579
- for(let i = 0; i < btns.length; i++) {
1580
- const b = document.getElementById(btns[i] + '-btn');
1579
+ for(const btn of btns.trim().split(/\s+/)) {
1580
+ const b = document.getElementById(btn + '-btn');
1581
1581
  b.classList.remove('disab', 'activ');
1582
1582
  b.classList.add('enab');
1583
1583
  }
1584
1584
  }
1585
1585
 
1586
1586
  disableButtons(btns) {
1587
- btns = btns.trim().split(/\s+/);
1588
- for(let i = 0; i < btns.length; i++) {
1589
- const b = document.getElementById(btns[i] + '-btn');
1587
+ for(const btn of btns.trim().split(/\s+/)) {
1588
+ const b = document.getElementById(btn + '-btn');
1590
1589
  b.classList.remove('enab', 'activ', 'stay-activ');
1591
1590
  b.classList.add('disab');
1592
1591
  }
@@ -1598,7 +1597,7 @@ class GUIController extends Controller {
1598
1597
  node_btns = 'process product link constraint cluster note ',
1599
1598
  edit_btns = 'replace clone paste delete undo redo ',
1600
1599
  model_btns = 'settings save actors dataset equation chart ' +
1601
- 'diagram savediagram finder monitor solve';
1600
+ 'savediagram finder monitor solve';
1602
1601
  if(MODEL === null) {
1603
1602
  this.disableButtons(node_btns + edit_btns + model_btns);
1604
1603
  return;
@@ -1680,10 +1679,9 @@ class GUIController extends Controller {
1680
1679
  }
1681
1680
 
1682
1681
  get stayActiveButton() {
1683
- // Return the button that is "stay active", or NULL if none
1684
- const btns = ['process', 'product', 'link', 'constraint', 'cluster', 'note'];
1685
- for(let i = 0; i < btns.length; i++) {
1686
- const b = document.getElementById(btns[i] + '-btn');
1682
+ // Return the button that is "stay active", or NULL if none .
1683
+ for(const btn of ['process', 'product', 'link', 'constraint', 'cluster', 'note']) {
1684
+ const b = document.getElementById(btn + '-btn');
1687
1685
  if(b.classList.contains('stay-activ')) return b;
1688
1686
  }
1689
1687
  return null;
@@ -1707,12 +1705,20 @@ class GUIController extends Controller {
1707
1705
  //
1708
1706
 
1709
1707
  updateCursorPosition(e) {
1710
- // Updates the cursor coordinates and displays them on the status bar
1708
+ // Update the cursor coordinates, and display them on the status bar.
1711
1709
  const cp = this.paper.cursorPosition(e.pageX, e.pageY);
1710
+ // Keep track of the cumulative relative movement since the last
1711
+ // mousedown event.
1712
+ this.net_move_x += cp[0] - this.mouse_x;
1713
+ this.net_move_y += cp[1] - this.mouse_y;
1714
+ // Only now update the mouse coordinates.
1712
1715
  this.mouse_x = cp[0];
1713
1716
  this.mouse_y = cp[1];
1717
+ // Show the coordinates on the status bar.
1714
1718
  document.getElementById('pos-x').innerHTML = 'X = ' + this.mouse_x;
1715
- document.getElementById('pos-y').innerHTML = 'Y = ' + this.mouse_y;
1719
+ document.getElementById('pos-y').innerHTML = 'Y = ' + this.mouse_y;
1720
+ // Reset all "object under cursor detection variables" so that they
1721
+ // will be re-established correctly by mouseMove.
1716
1722
  this.on_note = null;
1717
1723
  this.on_node = null;
1718
1724
  this.on_cluster = null;
@@ -1723,76 +1729,82 @@ class GUIController extends Controller {
1723
1729
  }
1724
1730
 
1725
1731
  mouseMove(e) {
1726
- // Responds to mouse cursor moving over Linny-R diagram area
1732
+ // Respond to mouse cursor moving over Linny-R diagram area.
1733
+ // First translate browser cursor coordinates to diagram coordinates.
1727
1734
  this.updateCursorPosition(e);
1728
1735
 
1729
- // NOTE: check, as MODEL might still be undefined
1736
+ // NOTE: Prevent errors in case MODEL is still undefined.
1730
1737
  if(!MODEL) return;
1731
1738
 
1732
1739
  //console.log(e);
1733
1740
  const fc = MODEL.focal_cluster;
1741
+ // NOTE: Proceed from last added to first added node.
1734
1742
  for(let i = fc.processes.length-1; i >= 0; i--) {
1735
- const obj = fc.processes[i];
1736
- if(obj.containsPoint(this.mouse_x, this.mouse_y)) {
1737
- this.on_node = obj;
1743
+ const p = fc.processes[i];
1744
+ if(p.containsPoint(this.mouse_x, this.mouse_y)) {
1745
+ this.on_node = p;
1738
1746
  break;
1739
1747
  }
1740
1748
  }
1741
1749
  if(!this.on_node) {
1742
1750
  for(let i = fc.product_positions.length-1; i >= 0; i--) {
1743
- const obj = fc.product_positions[i].product.setPositionInFocalCluster();
1744
- if(obj.product.containsPoint(this.mouse_x, this.mouse_y)) {
1745
- this.on_node = obj.product;
1751
+ // NOTE: Set product coordinates to its position in focal cluster.
1752
+ const p = fc.product_positions[i].product.setPositionInFocalCluster();
1753
+ if(p.product.containsPoint(this.mouse_x, this.mouse_y)) {
1754
+ this.on_node = p.product;
1746
1755
  break;
1747
1756
  }
1748
1757
  }
1749
1758
  }
1750
- for(let i = 0; i < fc.arrows.length; i++) {
1751
- const arr = fc.arrows[i];
1759
+ for(const arr of fc.arrows) {
1752
1760
  if(arr) {
1753
1761
  this.on_arrow = arr;
1754
- // NOTE: arrow may represent multiple links, so find out which one
1755
- const obj = arr.containsPoint(this.mouse_x, this.mouse_y);
1756
- if(obj) {
1757
- this.on_link = obj;
1762
+ // NOTE: Arrow may represent multiple links, and `containsPoint`
1763
+ // returns the link if this can be established unambiguously, or
1764
+ // NULL otherwise.
1765
+ const l = arr.containsPoint(this.mouse_x, this.mouse_y);
1766
+ if(l) {
1767
+ this.on_link = l;
1758
1768
  break;
1759
1769
  }
1760
1770
  }
1761
1771
  }
1762
1772
  this.on_constraint = this.constraintStillUnderCursor();
1763
1773
  if(fc.related_constraints != null) {
1764
- for(let i = 0; i < fc.related_constraints.length; i++) {
1765
- const obj = fc.related_constraints[i];
1766
- if(obj.containsPoint(this.mouse_x, this.mouse_y)) {
1767
- this.on_constraint = obj;
1774
+ for(const c of fc.related_constraints) {
1775
+ if(c.containsPoint(this.mouse_x, this.mouse_y)) {
1776
+ this.on_constraint = c;
1768
1777
  break;
1769
1778
  }
1770
1779
  }
1771
1780
  }
1772
1781
  for(let i = fc.sub_clusters.length-1; i >= 0; i--) {
1773
- const obj = fc.sub_clusters[i];
1774
- // NOTE: Ignore cluster that is being dragged, so that a cluster
1775
- // it is being dragged over will be detected instead.
1776
- if(obj != this.dragged_node &&
1777
- obj.containsPoint(this.mouse_x, this.mouse_y)) {
1778
- this.on_cluster = obj;
1779
- this.on_cluster_edge = obj.onEdge(this.mouse_x, this.mouse_y);
1780
- break;
1782
+ const c = fc.sub_clusters[i];
1783
+ if(c.containsPoint(this.mouse_x, this.mouse_y)) {
1784
+ // NOTE: Cluster that is being dragged is superseded by other clusters
1785
+ // so that a cluster it is being dragged over will be detected instead.
1786
+ if(!this.on_cluster || c !== this.dragged_node) {
1787
+ this.on_cluster = c;
1788
+ // NOTE: Cluster edge responds differently to doubble-click.
1789
+ this.on_cluster_edge = c.onEdge(this.mouse_x, this.mouse_y);
1790
+ }
1781
1791
  }
1782
1792
  }
1783
1793
  // Unset and redraw target cluster if cursor no longer over it.
1784
- if(!this.on_cluster && this.target_cluster) {
1794
+ if(this.on_cluster !== this.target_cluster) {
1785
1795
  const c = this.target_cluster;
1786
1796
  this.target_cluster = null;
1787
- UI.paper.drawCluster(c);
1788
- // NOTE: Element is persistent, so semi-transparency must also be
1789
- // undone.
1790
- c.shape.element.setAttribute('opacity', 1);
1797
+ if(c) {
1798
+ UI.paper.drawCluster(c);
1799
+ // NOTE: Element is persistent, so semi-transparency must also be
1800
+ // undone.
1801
+ c.shape.element.setAttribute('opacity', 1);
1802
+ }
1791
1803
  }
1792
1804
  for(let i = fc.notes.length-1; i >= 0; i--) {
1793
- const obj = fc.notes[i];
1794
- if(obj.containsPoint(this.mouse_x, this.mouse_y)) {
1795
- this.on_note = obj;
1805
+ const n = fc.notes[i];
1806
+ if(n.containsPoint(this.mouse_x, this.mouse_y)) {
1807
+ this.on_note = n;
1796
1808
  break;
1797
1809
  }
1798
1810
  }
@@ -1857,10 +1869,12 @@ class GUIController extends Controller {
1857
1869
  this.setMessage('');
1858
1870
  }
1859
1871
  }
1860
- // When dragging selection that contains a process, change cursor to
1872
+ // When dragging a selection over a cluster, change cursor to "cell" to
1861
1873
  // indicate that selected process(es) will be moved into the cluster.
1862
1874
  if(this.dragged_node) {
1863
- if(this.on_cluster) {
1875
+ // NOTE: Cursor will always be over the dragged node, so do not indicate
1876
+ // "drop here?" unless dragged over a different cluster.
1877
+ if(this.on_cluster && this.on_cluster !== this.dragged_node) {
1864
1878
  cr = 'cell';
1865
1879
  this.target_cluster = this.on_cluster;
1866
1880
  // Redraw the target cluster so it will appear on top (and highlighted).
@@ -1874,10 +1888,16 @@ class GUIController extends Controller {
1874
1888
  }
1875
1889
 
1876
1890
  mouseDown(e) {
1877
- // Responds to mousedown event in model diagram area.
1878
- // In case mouseup event occurred outside drawing area,ignore this
1879
- // mousedown event, so that only the mouseup will be processed.
1891
+ // Respond to mousedown event in model diagram area.
1892
+ // NOTE: While dragging the selection rectangle, the mouseup event will
1893
+ // not be observed when it occurred outside the drawing area. In such
1894
+ // cases, the mousedown event must be ignored so that only the mouseup
1895
+ // will be processed.
1880
1896
  if(this.start_sel_x >= 0 && this.start_sel_y >= 0) return;
1897
+ // Reset the cumulative movement since mousedown.
1898
+ this.net_move_x = 0;
1899
+ this.net_move_y = 0;
1900
+ // Get the paper coordinates indicated by the cursor.
1881
1901
  const cp = this.paper.cursorPosition(e.pageX, e.pageY);
1882
1902
  this.mouse_down_x = cp[0];
1883
1903
  this.mouse_down_y = cp[1];
@@ -1891,7 +1911,7 @@ class GUIController extends Controller {
1891
1911
  }
1892
1912
  // NOTE: Only left button is detected (browser catches right menu button).
1893
1913
  if(e.ctrlKey) {
1894
- // Remove clicked item from selection
1914
+ // Remove clicked item from selection.
1895
1915
  if(MODEL.selection) {
1896
1916
  // NOTE: First check constraints -- see mouseMove() for motivation.
1897
1917
  if(this.on_constraint) {
@@ -1943,37 +1963,15 @@ class GUIController extends Controller {
1943
1963
  UI.drawDiagram(MODEL);
1944
1964
  }
1945
1965
 
1946
- // If one of the top six sidebar buttons is active, prompt for new node
1947
- // (not link or constraint).
1966
+ // If one of the top six sidebar buttons is active, prompt for new node.
1967
+ // Note that this does not apply for links or constraints.
1948
1968
  if(this.active_button && this.active_button !== this.buttons.link &&
1949
1969
  this.active_button !== this.buttons.constraint) {
1950
1970
  this.add_x = this.mouse_x;
1951
1971
  this.add_y = this.mouse_y;
1952
- const obj = this.active_button.id.split('-')[0];
1972
+ const ot = this.active_button.id.split('-')[0];
1953
1973
  if(!this.stayActive) this.resetActiveButton();
1954
- if(obj === 'process') {
1955
- setTimeout(() => {
1956
- const md = UI.modals['add-process'];
1957
- md.element('name').value = '';
1958
- md.element('actor').value = '';
1959
- md.show('name');
1960
- });
1961
- } else if(obj === 'product') {
1962
- setTimeout(() => {
1963
- const md = UI.modals['add-product'];
1964
- md.element('name').value = '';
1965
- md.element('unit').value = MODEL.default_unit;
1966
- UI.setBox('add-product-data', false);
1967
- md.show('name');
1968
- });
1969
- } else if(obj === 'cluster') {
1970
- setTimeout(() => {
1971
- const md = UI.modals.cluster;
1972
- md.element('name').value = '';
1973
- md.element('actor').value = '';
1974
- md.show('name');
1975
- });
1976
- } else if(obj === 'note') {
1974
+ if(ot === 'note') {
1977
1975
  setTimeout(() => {
1978
1976
  const md = UI.modals.note;
1979
1977
  md.element('action').innerHTML = 'Add';
@@ -1981,6 +1979,33 @@ class GUIController extends Controller {
1981
1979
  md.element('text').value = '';
1982
1980
  md.show('text');
1983
1981
  });
1982
+ } else {
1983
+ // Align position to the grid.
1984
+ this.add_x = MODEL.aligned(this.add_x);
1985
+ this.add_y = MODEL.aligned(this.add_y);
1986
+ if(ot === 'process') {
1987
+ setTimeout(() => {
1988
+ const md = UI.modals['add-process'];
1989
+ md.element('name').value = '';
1990
+ md.element('actor').value = '';
1991
+ md.show('name');
1992
+ });
1993
+ } else if(ot === 'product') {
1994
+ setTimeout(() => {
1995
+ const md = UI.modals['add-product'];
1996
+ md.element('name').value = '';
1997
+ md.element('unit').value = MODEL.default_unit;
1998
+ UI.setBox('add-product-data', false);
1999
+ md.show('name');
2000
+ });
2001
+ } else if(ot === 'cluster') {
2002
+ setTimeout(() => {
2003
+ const md = UI.modals.cluster;
2004
+ md.element('name').value = '';
2005
+ md.element('actor').value = '';
2006
+ md.show('name');
2007
+ });
2008
+ }
1984
2009
  }
1985
2010
  return;
1986
2011
  }
@@ -2020,7 +2045,7 @@ class GUIController extends Controller {
2020
2045
  } else if(this.on_node) {
2021
2046
  if(this.active_button === this.buttons.link) {
2022
2047
  this.linking_node = this.on_node;
2023
- // NOTE: return without updating buttons
2048
+ // NOTE: Return without updating buttons.
2024
2049
  return;
2025
2050
  } else if(this.active_button === this.buttons.constraint) {
2026
2051
  // Allow constraints only on nodes having upper bounds defined.
@@ -2031,6 +2056,7 @@ class GUIController extends Controller {
2031
2056
  }
2032
2057
  } else {
2033
2058
  this.dragged_node = this.on_node;
2059
+ // NOTE: Keep track of relative movement of the dragged node.
2034
2060
  this.move_dx = this.mouse_x - this.on_node.x;
2035
2061
  this.move_dy = this.mouse_y - this.on_node.y;
2036
2062
  if(MODEL.selection.indexOf(this.on_node) < 0) MODEL.select(this.on_node);
@@ -2055,6 +2081,10 @@ class GUIController extends Controller {
2055
2081
  mouseUp(e) {
2056
2082
  // Responds to mouseup event.
2057
2083
  const cp = this.paper.cursorPosition(e.pageX, e.pageY);
2084
+ // Keep track of the cumulative relative movement since the last
2085
+ // mousedown event.
2086
+ this.net_move_x += cp[0] - this.mouse_x;
2087
+ this.net_move_y += cp[1] - this.mouse_y;
2058
2088
  this.mouse_up_x = cp[0];
2059
2089
  this.mouse_up_y = cp[1];
2060
2090
  // First check whether user is selecting a rectangle.
@@ -2071,44 +2101,32 @@ class GUIController extends Controller {
2071
2101
  // If rectangle has size greater than 2x2 pixels, select all elements
2072
2102
  // having their center inside the selection rectangle.
2073
2103
  if(brx - tlx > 2 && bry - tly > 2) {
2074
- const ol = [], fc = MODEL.focal_cluster;
2075
- for(let i = 0; i < fc.processes.length; i++) {
2076
- const obj = fc.processes[i];
2077
- if(obj.x >= tlx && obj.x <= brx && obj.y >= tly && obj.y < bry) {
2078
- ol.push(obj);
2079
- }
2104
+ const
2105
+ ol = [],
2106
+ fc = MODEL.focal_cluster;
2107
+ for(const p of fc.processes) {
2108
+ if(p.x >= tlx && p.x <= brx && p.y >= tly && p.y < bry) ol.push(p);
2080
2109
  }
2081
- for(let i = 0; i < fc.product_positions.length; i++) {
2082
- const obj = fc.product_positions[i];
2083
- if(obj.x >= tlx && obj.x <= brx && obj.y >= tly && obj.y < bry) {
2084
- ol.push(obj.product);
2110
+ for(const pp of fc.product_positions) {
2111
+ if(pp.x >= tlx && pp.x <= brx && pp.y >= tly && pp.y < bry) {
2112
+ ol.push(pp.product);
2085
2113
  }
2086
2114
  }
2087
- for(let i = 0; i < fc.sub_clusters.length; i++) {
2088
- const obj = fc.sub_clusters[i];
2089
- if(obj.x >= tlx && obj.x <= brx && obj.y >= tly && obj.y < bry) {
2090
- ol.push(obj);
2091
- }
2115
+ for(const c of fc.sub_clusters) {
2116
+ if(c.x >= tlx && c.x <= brx && c.y >= tly && c.y < bry) ol.push(c);
2092
2117
  }
2093
- for(let i = 0; i < fc.notes.length; i++) {
2094
- const obj = fc.notes[i];
2095
- if(obj.x >= tlx && obj.x <= brx && obj.y >= tly && obj.y < bry) {
2096
- ol.push(obj);
2097
- }
2118
+ for(const n of fc.notes) {
2119
+ if(n.x >= tlx && n.x <= brx && n.y >= tly && n.y < bry) ol.push(n);
2098
2120
  }
2099
- for(let i in MODEL.links) if(MODEL.links.hasOwnProperty(i)) {
2100
- const obj = MODEL.links[i];
2121
+ for(let k in MODEL.links) if(MODEL.links.hasOwnProperty(k)) {
2122
+ const l = MODEL.links[k];
2101
2123
  // Only add a link if both its nodes are selected as well.
2102
- if(fc.linkInList(obj, ol)) {
2103
- ol.push(obj);
2104
- }
2124
+ if(fc.linkInList(l, ol)) ol.push(l);
2105
2125
  }
2106
- for(let i in MODEL.constraints) if(MODEL.constraints.hasOwnProperty(i)) {
2107
- const obj = MODEL.constraints[i];
2126
+ for(let k in MODEL.constraints) if(MODEL.constraints.hasOwnProperty(k)) {
2127
+ const c = MODEL.constraints[k];
2108
2128
  // Only add a constraint if both its nodes are selected as well.
2109
- if(fc.linkInList(obj, ol)) {
2110
- ol.push(obj);
2111
- }
2129
+ if(fc.linkInList(c, ol)) ol.push(c);
2112
2130
  }
2113
2131
  // Having compiled the object list, actually select them.
2114
2132
  MODEL.selectList(ol);
@@ -2123,9 +2141,9 @@ class GUIController extends Controller {
2123
2141
  } else if(this.linking_node) {
2124
2142
  // If so, check whether the cursor is over a node of the appropriate type.
2125
2143
  if(this.on_node && MODEL.canLink(this.linking_node, this.on_node)) {
2126
- const obj = MODEL.addLink(this.linking_node, this.on_node);
2127
- UNDO_STACK.push('add', obj);
2128
- MODEL.select(obj);
2144
+ const l = MODEL.addLink(this.linking_node, this.on_node);
2145
+ UNDO_STACK.push('add', l);
2146
+ MODEL.select(l);
2129
2147
  this.paper.drawModel(MODEL);
2130
2148
  }
2131
2149
  this.linking_node = null;
@@ -2149,31 +2167,25 @@ class GUIController extends Controller {
2149
2167
  // Then check whether the user is moving a node (possibly part of a
2150
2168
  // larger selection).
2151
2169
  } else if(this.dragged_node) {
2152
- // Always perform the move operation (this will do nothing if the
2153
- // cursor did not move).
2154
- MODEL.moveSelection(
2155
- this.mouse_up_x - this.mouse_x, this.mouse_up_y - this.mouse_y);
2156
- // Set cursor to pointer, as it should be on some node while dragging.
2157
- this.paper.container.style.cursor = 'pointer';
2158
- // @@TO DO: if on top of a cluster, move it there.
2159
- // NOTE: Cursor will always be over the selected cluster (while dragging).
2160
- if(this.on_cluster && !this.on_cluster.selected) {
2161
- UNDO_STACK.push('drop', this.on_cluster);
2162
- MODEL.dropSelectionIntoCluster(this.on_cluster);
2163
- this.on_node = null;
2164
- this.on_note = null;
2165
- this.target_cluster = null;
2166
- // Redraw cluster to erase its orange "target corona".
2167
- UI.paper.drawCluster(this.on_cluster);
2168
- }
2169
-
2170
- // Check wether the cursor has been moved.
2170
+ // NOTE: When double-clicking with a sensitive mouse, the cursor
2171
+ // may move a few pixels, and then this should NOT be considered
2172
+ // as an intentional move. Hence, check wether the cursor has been
2173
+ // moved *significantly* since the mouseDown event.
2171
2174
  const
2172
- absdx = Math.abs(this.mouse_down_x - this.mouse_x),
2173
- absdy = Math.abs(this.mouse_down_y - this.mouse_y);
2174
- // If no *significant* move made, remove the move undo.
2175
- if(absdx + absdy === 0) UNDO_STACK.pop('move');
2176
- if(this.doubleClicked && absdx + absdy < 3) {
2175
+ mdx = this.mouse_down_x - this.mouse_x,
2176
+ mdy = this.mouse_down_y - this.mouse_y,
2177
+ absdx = Math.abs(this.net_move_x),
2178
+ absdy = Math.abs(this.net_move_y),
2179
+ sigmv = (MODEL.align_to_grid ? MODEL.grid_pixels / 4 : 2.5);
2180
+ if(this.doubleClicked) {
2181
+ // Ignore insignificant move.
2182
+ if(absdx < sigmv && absdy < sigmv) {
2183
+ // Undo the move and remove the action from the UNDO-stack.
2184
+ // NOTE: Do not use the regular `undo` routine as this would
2185
+ // make the action redoable.
2186
+ MODEL.moveSelection(mdx, mdy);
2187
+ UNDO_STACK.pop('move');
2188
+ }
2177
2189
  // Double-clicking opens properties dialog, except for clusters;
2178
2190
  // then "drill down", i.e., make the double-clicked cluster focal.
2179
2191
  if(this.dragged_node instanceof Cluster) {
@@ -2197,6 +2209,30 @@ class GUIController extends Controller {
2197
2209
  } else {
2198
2210
  this.showNotePropertiesDialog(this.dragged_node);
2199
2211
  }
2212
+ } else {
2213
+ // Move the selection, even if the movement is very small, because the
2214
+ // final movement since last mouse event may make the *cumulative*
2215
+ // movement since the last mouseDown significant.
2216
+ MODEL.moveSelection(
2217
+ this.mouse_up_x - this.mouse_x, this.mouse_up_y - this.mouse_y);
2218
+ if(this.net_move_x < 0.5 && this.net_move_y < 0.5) {
2219
+ // No effective move of the selection => remove the UNDO.
2220
+ UNDO_STACK.pop('move');
2221
+ }
2222
+ // Set cursor to pointer, as it should be on some node while dragging.
2223
+ this.paper.container.style.cursor = 'pointer';
2224
+ // NOTE: Cursor will always be over the selected cluster (while dragging).
2225
+ if(this.on_cluster && !this.on_cluster.selected) {
2226
+ UNDO_STACK.push('drop', this.on_cluster);
2227
+ MODEL.dropSelectionIntoCluster(this.on_cluster);
2228
+ this.on_node = null;
2229
+ this.on_note = null;
2230
+ this.target_cluster = null;
2231
+ // Redraw cluster to erase its orange "target corona".
2232
+ UI.paper.drawCluster(this.on_cluster);
2233
+ }
2234
+ // Only now align to grid.
2235
+ MODEL.alignToGrid();
2200
2236
  }
2201
2237
  this.dragged_node = null;
2202
2238
 
@@ -2210,6 +2246,8 @@ class GUIController extends Controller {
2210
2246
  this.showConstraintPropertiesDialog(this.on_constraint);
2211
2247
  }
2212
2248
  }
2249
+ // Finally, reset "selecting with rectangle" (just to be sure), and
2250
+ // update the UI button states.
2213
2251
  this.start_sel_x = -1;
2214
2252
  this.start_sel_y = -1;
2215
2253
  this.updateButtons();
@@ -2253,9 +2291,8 @@ class GUIController extends Controller {
2253
2291
  topmod = null,
2254
2292
  code = e.code,
2255
2293
  alt = e.altKey;
2256
- for(let i = 0; i < modals.length; i++) {
2294
+ for(const m of modals) {
2257
2295
  const
2258
- m = modals[i],
2259
2296
  cs = window.getComputedStyle(m),
2260
2297
  z = parseInt(cs.zIndex);
2261
2298
  if(cs.display !== 'none' && z > maxz) {
@@ -2569,7 +2606,7 @@ class GUIController extends Controller {
2569
2606
  validNames(nn, an='') {
2570
2607
  // Check whether names meet conventions; if not, warn user
2571
2608
  if(!UI.validName(nn) || nn.indexOf(UI.BLACK_BOX) >= 0) {
2572
- UI.warn(`Invalid name "${nn}"`);
2609
+ this.warningInvalidName(nn);
2573
2610
  return false;
2574
2611
  }
2575
2612
  if(an === '' || an === UI.NO_ACTOR) return true;
@@ -2627,12 +2664,12 @@ class GUIController extends Controller {
2627
2664
  }
2628
2665
 
2629
2666
  updateScaleUnitList() {
2630
- // Update the HTML datalist element to reflect all scale units
2667
+ // Update the HTML datalist element to reflect all scale units.
2631
2668
  const
2632
2669
  ul = [],
2633
2670
  keys = Object.keys(MODEL.scale_units).sort(ciCompare);
2634
- for(let i = 0; i < keys.length; i++) {
2635
- ul.push(`<option value="${MODEL.scale_units[keys[i]].name}">`);
2671
+ for(const k of keys) {
2672
+ ul.push(`<option value="${MODEL.scale_units[k].name}">`);
2636
2673
  }
2637
2674
  document.getElementById('units-data').innerHTML = ul.join('');
2638
2675
  }
@@ -3001,6 +3038,7 @@ class GUIController extends Controller {
3001
3038
  const vn = this.validName(nn);
3002
3039
  if(!vn) {
3003
3040
  UNDO_STACK.pop();
3041
+ this.warningInvalidName(nn);
3004
3042
  return false;
3005
3043
  }
3006
3044
  // NOTE: Pre-check if product exists.
@@ -3229,8 +3267,8 @@ class GUIController extends Controller {
3229
3267
  if(elig.length) {
3230
3268
  sl.push('<div class="paste-select"><select id="paste-ft-', i,
3231
3269
  '" style="font-size: 12px">');
3232
- for(let j = 0; j < elig.length; j++) {
3233
- const dn = elig[j].displayName;
3270
+ for(const e of elig) {
3271
+ const dn = e.displayName;
3234
3272
  sl.push('<option value="', dn, '">', dn, '</option>');
3235
3273
  }
3236
3274
  sl.push('</select></div>');
@@ -3404,8 +3442,7 @@ class GUIController extends Controller {
3404
3442
  function nameConflicts(node) {
3405
3443
  // Maps names of entities defined by the child nodes of `node`
3406
3444
  // while detecting name conflicts.
3407
- for(let i = 0; i < node.childNodes.length; i++) {
3408
- const c = node.childNodes[i];
3445
+ for(const c of node.childNodes) {
3409
3446
  if(c.nodeName !== 'link' && c.nodeName !== 'constraint') {
3410
3447
  const
3411
3448
  fn = fullName(c),
@@ -3505,9 +3542,8 @@ class GUIController extends Controller {
3505
3542
  // Prompt for names of selected cluster nodes.
3506
3543
  if(selc_node.childNodes.length && !mapping.prefix) {
3507
3544
  mapping.top_clusters = {};
3508
- for(let i = 0; i < selc_node.childNodes.length; i++) {
3545
+ for(const c of selc_node.childNodes) {
3509
3546
  const
3510
- c = selc_node.childNodes[i],
3511
3547
  fn = fullName(c),
3512
3548
  mn = mappedName(fn);
3513
3549
  mapping.top_clusters[fn] = mn;
@@ -3522,9 +3558,8 @@ class GUIController extends Controller {
3522
3558
  const
3523
3559
  ft_map = {},
3524
3560
  ft_type = {};
3525
- for(let i = 0; i < from_tos_node.childNodes.length; i++) {
3561
+ for(const c of from_tos_node.childNodes) {
3526
3562
  const
3527
- c = from_tos_node.childNodes[i],
3528
3563
  fn = fullName(c),
3529
3564
  mn = mappedName(fn);
3530
3565
  if(MODEL.objectByName(mn)) {
@@ -3554,20 +3589,14 @@ console.log('HERE name conflicts', name_conflicts, mapping);
3554
3589
  }
3555
3590
 
3556
3591
  // No conflicts => add all
3557
- for(let i = 0; i < extras_node.childNodes.length; i++) {
3558
- addEntityFromNode(extras_node.childNodes[i]);
3559
- }
3560
- for(let i = 0; i < from_tos_node.childNodes.length; i++) {
3561
- addEntityFromNode(from_tos_node.childNodes[i]);
3562
- }
3563
- for(let i = 0; i < entities_node.childNodes.length; i++) {
3564
- addEntityFromNode(entities_node.childNodes[i]);
3565
- }
3592
+ for(const c of extras_node.childNodes) addEntityFromNode(c);
3593
+ for(const c of from_tos_node.childNodes) addEntityFromNode(c);
3594
+ for(const c of entities_node.childNodes) addEntityFromNode(c);
3566
3595
  // Update diagram, showing newly added nodes as selection.
3567
3596
  MODEL.clearSelection();
3568
- for(let i = 0; i < selection_node.childNodes.length; i++) {
3597
+ for(const c of selection_node.childNodes) {
3569
3598
  const
3570
- n = xmlDecoded(nodeContent(selection_node.childNodes[i])),
3599
+ n = xmlDecoded(nodeContent(c)),
3571
3600
  obj = MODEL.objectByName(mappedName(n));
3572
3601
  if(obj) {
3573
3602
  // NOTE: Selected products must be positioned.
@@ -3681,6 +3710,12 @@ console.log('HERE name conflicts', name_conflicts, mapping);
3681
3710
  cb = UI.boxChecked('settings-power');
3682
3711
  redraw = redraw || cb !== model.with_power_flow;
3683
3712
  model.with_power_flow = cb;
3713
+ // NOTE: Clear the "ignore" options if no power flow constraints.
3714
+ if(!model.with_power_flow) {
3715
+ model.ignore_grid_capacity = false;
3716
+ model.ignore_KVL = false;
3717
+ model.ignore_power_losses = false;
3718
+ }
3684
3719
  cb = UI.boxChecked('settings-cost-prices');
3685
3720
  redraw = redraw || cb !== model.infer_cost_prices;
3686
3721
  model.infer_cost_prices = cb;
@@ -3749,8 +3784,7 @@ console.log('HERE name conflicts', name_conflicts, mapping);
3749
3784
  const
3750
3785
  md = this.modals.solver,
3751
3786
  html = ['<option value="">(default)</option>'];
3752
- for(let i = 0; i < VM.solver_list.length; i++) {
3753
- const s = VM.solver_list[i];
3787
+ for(const s of VM.solver_list) {
3754
3788
  html.push(['<option value="', s,
3755
3789
  (s === MODEL.preferred_solver ? '"selected="selected' : ''),
3756
3790
  '">', VM.solver_names[s], '</option>'].join(''));
@@ -3885,17 +3919,13 @@ console.log('HERE name conflicts', name_conflicts, mapping);
3885
3919
  plate.innerHTML = pg.voltage;
3886
3920
  overlay.style.display = 'block';
3887
3921
  // Disable tab stop for the properties that are now not shown.
3888
- for(let i = 0; i < notab.length; i++) {
3889
- md.element(notab[i]).tabIndex = -1;
3890
- }
3922
+ for(const nt of notab) md.element(nt).tabIndex = -1;
3891
3923
  } else {
3892
3924
  plate.innerHTML = '(&#x21AF;)';
3893
3925
  plate.className = 'no-grid-plate';
3894
3926
  overlay.style.display = 'none';
3895
3927
  // Enable tab stop for the properties that are now not shown.
3896
- for(let i = 0; i < notab.length; i++) {
3897
- md.element(notab[i]).tabIndex = 0;
3898
- }
3928
+ for(const nt of notab) md.element(nt).tabIndex = 0;
3899
3929
  }
3900
3930
  this.hideGridPlateMenu('process');
3901
3931
  // Show plate "button" only when power grids option is set for model.
@@ -4131,9 +4161,7 @@ console.log('HERE name conflicts', name_conflicts, mapping);
4131
4161
 
4132
4162
  showClusterPropertiesDialog(c, group=[]) {
4133
4163
  let bb = false;
4134
- for(let i = 0; !bb && i < group.length; i++) {
4135
- bb = group[i].is_black_boxed;
4136
- }
4164
+ for(const g of group) bb = bb || g.is_black_boxed;
4137
4165
  if(bb || c.is_black_boxed) {
4138
4166
  this.notify('Black-boxed clusters cannot be edited');
4139
4167
  return;
@@ -4375,31 +4403,27 @@ console.log('HERE name conflicts', name_conflicts, mapping);
4375
4403
  this.drawObject(p);
4376
4404
  // Make list of nodes related to P by links
4377
4405
  const rel_nodes = [];
4378
- for(let i = 0; i < p.inputs.length; i++) {
4379
- rel_nodes.push(p.inputs[i].from_node);
4380
- }
4381
- for(let i = 0; i < p.outputs.length; i++) {
4382
- rel_nodes.push(p.outputs[i].to_node);
4383
- }
4406
+ for(const l of p.inputs) rel_nodes.push(l.from_node);
4407
+ for(const l of p.outputs) rel_nodes.push(l.to_node);
4384
4408
  const options = [];
4385
- for(let i in MODEL.products) if(MODEL.products.hasOwnProperty(i) &&
4409
+ for(let k in MODEL.products) if(MODEL.products.hasOwnProperty(k) &&
4386
4410
  // NOTE: do not show "black-boxed" products
4387
- !i.startsWith(UI.BLACK_BOX)) {
4388
- const po = MODEL.products[i];
4411
+ !k.startsWith(UI.BLACK_BOX)) {
4412
+ const po = MODEL.products[k];
4389
4413
  // Skip the product that is to be replaced, an also products having a
4390
4414
  // different type (regular product or data product)
4391
4415
  if(po !== p && po.is_data === p.is_data) {
4392
4416
  // NOTE: also skip products PO that are linked to a node Q that is
4393
4417
  // already linked to P (as replacing would then create a two-way link)
4394
4418
  let no_rel = true;
4395
- for(let j = 0; j < po.inputs.length; j++) {
4396
- if(rel_nodes.indexOf(po.inputs[j].from_node) >= 0) {
4419
+ for(const l of po.inputs) {
4420
+ if(rel_nodes.indexOf(l.from_node) >= 0) {
4397
4421
  no_rel = false;
4398
4422
  break;
4399
4423
  }
4400
4424
  }
4401
- for(let j = 0; j < po.outputs.length; j++) {
4402
- if(rel_nodes.indexOf(po.outputs[j].to_node) >= 0) {
4425
+ for(const l of po.outputs) {
4426
+ if(rel_nodes.indexOf(l.to_node) >= 0) {
4403
4427
  no_rel = false;
4404
4428
  break;
4405
4429
  }