linny-r 1.8.0 → 1.8.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "linny-r",
3
- "version": "1.8.0",
3
+ "version": "1.8.2",
4
4
  "description": "Executable graphical language with WYSIWYG editor for MILP models",
5
5
  "main": "server.js",
6
6
  "scripts": {
package/static/index.html CHANGED
@@ -375,7 +375,7 @@ and move the cursor over the status bar">
375
375
  <img id="redo-btn" class="btn disab" src="images/redo.png"
376
376
  title="Redo (Ctrl-Y)">
377
377
  <img id="solve-btn" class="btn enab" src="images/solve.png"
378
- title="Run simulation (Ctrl-R)">
378
+ title="Run simulation (Ctrl-R) &ndash; Alt-click to diagnose infeasible/unbounded problem (Alt-R)">
379
379
  <img id="stop-btn" class="btn enab off" src="images/stop.png"
380
380
  title="Stop solving (Ctrl-Q)">
381
381
  <img id="reset-btn" class="btn enab off" src="images/reset.png"
@@ -576,11 +576,9 @@ and move the cursor over the status bar">
576
576
  <table style="width:100%; white-space: nowrap">
577
577
  <tr>
578
578
  <td style="padding:0px">
579
- <div id="settings-decimal-comma" class="box clear"></div>
580
- </td>
581
- <td style="padding-bottom:4px">
582
- Use decimal <u>comma</u> when copying data to clipboard
579
+ <div id="settings-block-arrows" class="box clear"></div>
583
580
  </td>
581
+ <td style="padding-bottom:4px">Show hidden flows as block arrows</td>
584
582
  </tr>
585
583
  <tr>
586
584
  <td style="padding:0px">
@@ -632,25 +630,34 @@ and move the cursor over the status bar">
632
630
  title="Set solver preferences">
633
631
  </td>
634
632
  </tr>
633
+ <tr title="When checked, finite process bounds and slack variables are always added">
634
+ <td style="padding:0px">
635
+ <div id="settings-diagnose" class="box clear"></div>
636
+ </td>
637
+ <td style="padding-bottom:4px">Diagnose infeasible/unbounded problems</td>
638
+ </td>
639
+ </tr>
635
640
  <tr>
636
641
  <td style="padding:0px">
637
642
  <div id="settings-cost-prices" class="box clear"></div>
638
643
  </td>
639
644
  <td style="padding-bottom:4px">Infer and display cost prices</td>
640
645
  </tr>
641
- <tr title="Reports will be saved in user/reports/, and removed after 24 h">
646
+ <tr>
642
647
  <td style="padding:0px">
643
- <div id="settings-report-results" class="box clear"></div>
648
+ <div id="settings-decimal-comma" class="box clear"></div>
644
649
  </td>
645
650
  <td style="padding-bottom:4px">
646
- Report results after each run
651
+ Use decimal <u>comma</u> when copying data to clipboard
647
652
  </td>
648
653
  </tr>
649
- <tr>
654
+ <tr title="Reports will be saved in user/reports/, and removed after 24 h">
650
655
  <td style="padding:0px">
651
- <div id="settings-block-arrows" class="box clear"></div>
656
+ <div id="settings-report-results" class="box clear"></div>
657
+ </td>
658
+ <td style="padding-bottom:4px">
659
+ Report results after each run
652
660
  </td>
653
- <td style="padding-bottom:4px">Show hidden flows as block arrows</td>
654
661
  </tr>
655
662
  <tr>
656
663
  <td style="padding:0px">
@@ -743,13 +750,6 @@ NOTE: Unit symbols are case-sensitive, so BTU &ne; Btu">
743
750
  placeholder="1e-4" type="text" autocomplete="off">
744
751
  </td>
745
752
  </tr>
746
- <tr title="When checked, finite process bounds and slack variables are always added">
747
- <td style="padding:0px">
748
- <div id="solver-diagnose" class="box clear"></div>
749
- </td>
750
- <td style="padding-bottom:4px">Diagnose infeasible/unbounded problems</td>
751
- </td>
752
- </tr>
753
753
  </table>
754
754
  </div>
755
755
  </div>
@@ -572,6 +572,7 @@ class Controller {
572
572
  VM.reset();
573
573
  // Redraw model in the browser (GUI only).
574
574
  MODEL.clearSelection();
575
+ this.clearStatusLine();
575
576
  this.drawDiagram(MODEL);
576
577
  }
577
578
 
@@ -605,6 +606,7 @@ class Controller {
605
606
  setProgressNeedle() {}
606
607
  updateTimeStep() {}
607
608
  updateIssuePanel() {}
609
+ clearStatusLine() {}
608
610
  updateDraggableDialogs() {}
609
611
  logHeapSize() {}
610
612
 
@@ -909,7 +909,7 @@ class GUIController extends Controller {
909
909
  // Reset the Virtual Machine.
910
910
  VM.reset();
911
911
  this.updateIssuePanel();
912
- this.setMessage('');
912
+ this.clearStatusLine();
913
913
  this.updateButtons();
914
914
  // Undoable operations no longer apply!
915
915
  UNDO_STACK.clear();
@@ -1733,8 +1733,8 @@ class GUIController extends Controller {
1733
1733
  }
1734
1734
  for(let i = fc.sub_clusters.length-1; i >= 0; i--) {
1735
1735
  const obj = fc.sub_clusters[i];
1736
- // NOTE: ignore cluster that is being dragged, so that a cluster it is
1737
- // being dragged over will be detected instead
1736
+ // NOTE: Ignore cluster that is being dragged, so that a cluster
1737
+ // it is being dragged over will be detected instead.
1738
1738
  if(obj != this.dragged_node &&
1739
1739
  obj.containsPoint(this.mouse_x, this.mouse_y)) {
1740
1740
  this.on_cluster = obj;
@@ -1742,12 +1742,13 @@ class GUIController extends Controller {
1742
1742
  break;
1743
1743
  }
1744
1744
  }
1745
- // unset and redraw target cluster if cursor no longer over it
1745
+ // Unset and redraw target cluster if cursor no longer over it.
1746
1746
  if(!this.on_cluster && this.target_cluster) {
1747
1747
  const c = this.target_cluster;
1748
1748
  this.target_cluster = null;
1749
1749
  UI.paper.drawCluster(c);
1750
- // NOTE: element is persistent, so semi-transparency must also be undone
1750
+ // NOTE: Element is persistent, so semi-transparency must also be
1751
+ // undone.
1751
1752
  c.shape.element.setAttribute('opacity', 1);
1752
1753
  }
1753
1754
  for(let i = fc.notes.length-1; i >= 0; i--) {
@@ -1758,15 +1759,15 @@ class GUIController extends Controller {
1758
1759
  }
1759
1760
  }
1760
1761
  if(this.active_button === this.buttons.link && this.linking_node) {
1761
- // Draw red dotted line from linking node to cursor
1762
+ // Draw red dotted line from linking node to cursor.
1762
1763
  this.paper.dragLineToCursor(this.linking_node, this.mouse_x, this.mouse_y);
1763
1764
  } else if(this.start_sel_x >= 0 && this.start_sel_y >= 0) {
1764
- // Draw selecting rectangle in red dotted lines
1765
+ // Draw selecting rectangle in red dotted lines.
1765
1766
  this.paper.dragRectToCursor(this.start_sel_x, this.start_sel_y,
1766
1767
  this.mouse_x, this.mouse_y);
1767
1768
  } else if(this.active_button === this.buttons.constraint &&
1768
1769
  this.constraining_node) {
1769
- // Draw red dotted line from constraining node to cursor
1770
+ // Draw red dotted line from constraining node to cursor.
1770
1771
  this.paper.dragLineToCursor(this.constraining_node,
1771
1772
  this.mouse_x, this.mouse_y);
1772
1773
  } else if(this.dragged_node) {
@@ -1774,14 +1775,14 @@ class GUIController extends Controller {
1774
1775
  this.mouse_y - this.move_dy - this.dragged_node.y);
1775
1776
  }
1776
1777
  let cr = 'pointer';
1777
- // NOTE: first check ON_CONSTRAINT because constraint thumbnails overlap
1778
- // with nodes
1778
+ // NOTE: First check ON_CONSTRAINT because constraint thumbnails overlap
1779
+ // with nodes.
1779
1780
  if(this.on_constraint) {
1780
1781
  DOCUMENTATION_MANAGER.update(this.on_constraint, e.shiftKey);
1781
- // NOTE: skip the "on node" check if the node is being dragged
1782
+ // NOTE: Skip the "on node" check if the node is being dragged.
1782
1783
  } else if(this.on_node && this.on_node !== this.dragged_node) {
1783
1784
  if((this.active_button === this.buttons.link) && this.linking_node) {
1784
- // Cannot link process to process
1785
+ // Cannot link process to process.
1785
1786
  cr = (MODEL.canLink(this.linking_node, this.on_node) ?
1786
1787
  'crosshair' : 'not-allowed');
1787
1788
  } else if(this.active_button === this.buttons.constraint) {
@@ -1789,21 +1790,21 @@ class GUIController extends Controller {
1789
1790
  cr = (this.constraining_node.canConstrain(this.on_node) ?
1790
1791
  'crosshair' : 'not-allowed');
1791
1792
  } else if(!this.on_node.hasBounds) {
1792
- // Products can only constrain when they have bounds
1793
+ // Products can only constrain when they have bounds.
1793
1794
  cr = 'not-allowed';
1794
1795
  }
1795
1796
  }
1796
- // NOTE: do not overwite status line when cursor is on a block arrow
1797
+ // NOTE: Do not overwite status line when cursor is on a block arrow.
1797
1798
  if(!this.on_block_arrow) {
1798
1799
  DOCUMENTATION_MANAGER.update(this.on_node, e.shiftKey);
1799
1800
  }
1800
1801
  } else if(this.on_note) {
1801
- // When shift-moving over a note, show the model's documentation
1802
+ // When shift-moving over a note, show the model's documentation.
1802
1803
  DOCUMENTATION_MANAGER.update(MODEL, e.shiftKey);
1803
1804
  } else {
1804
1805
  if((this.active_button === this.buttons.link && this.linking_node) ||
1805
1806
  (this.active_button === this.buttons.constraint && this.constraining_node)) {
1806
- // Cannot link to clusters or notes
1807
+ // Cannot link to clusters or notes.
1807
1808
  cr = (this.on_cluster || this.on_note ? 'not-allowed' : 'crosshair');
1808
1809
  } else if(!this.on_note && !this.on_constraint && !this.on_link &&
1809
1810
  !this.on_cluster_edge) {
@@ -1819,38 +1820,42 @@ class GUIController extends Controller {
1819
1820
  }
1820
1821
  }
1821
1822
  // When dragging selection that contains a process, change cursor to
1822
- // indicate that selected process(es) will be moved into the cluster
1823
- if(this.dragged_node && this.on_cluster) {
1824
- cr = 'cell';
1825
- this.target_cluster = this.on_cluster;
1826
- // Redraw the target cluster so it will appear on top (and highlighted)
1827
- UI.paper.drawCluster(this.target_cluster);
1823
+ // indicate that selected process(es) will be moved into the cluster.
1824
+ if(this.dragged_node) {
1825
+ if(this.on_cluster) {
1826
+ cr = 'cell';
1827
+ this.target_cluster = this.on_cluster;
1828
+ // Redraw the target cluster so it will appear on top (and highlighted).
1829
+ UI.paper.drawCluster(this.target_cluster);
1830
+ } else {
1831
+ cr = 'grab';
1832
+ }
1828
1833
  }
1829
1834
  }
1830
1835
  this.paper.container.style.cursor = cr;
1831
1836
  }
1832
1837
 
1833
1838
  mouseDown(e) {
1834
- // Responds to mousedown event in model diagram area
1839
+ // Responds to mousedown event in model diagram area.
1835
1840
  // In case mouseup event occurred outside drawing area,ignore this
1836
- // mousedown event, so that only the mouseup will be processed
1841
+ // mousedown event, so that only the mouseup will be processed.
1837
1842
  if(this.start_sel_x >= 0 && this.start_sel_y >= 0) return;
1838
1843
  const cp = this.paper.cursorPosition(e.pageX, e.pageY);
1839
1844
  this.mouse_down_x = cp[0];
1840
1845
  this.mouse_down_y = cp[1];
1841
1846
  // De-activate "stay active" buttons if dysfunctional, or if SHIFT,
1842
- // ALT or CTRL is pressed
1847
+ // ALT or CTRL is pressed.
1843
1848
  if((e.shiftKey || e.altKey || e.ctrlKey ||
1844
1849
  this.on_note || this.on_cluster || this.on_link || this.on_constraint ||
1845
1850
  (this.on_node && this.active_button !== this.buttons.link &&
1846
1851
  this.active_button !== this.buttons.constraint)) && this.stayActive) {
1847
1852
  resetActiveButton();
1848
1853
  }
1849
- // NOTE: only left button is detected (browser catches right menu button)
1854
+ // NOTE: Only left button is detected (browser catches right menu button).
1850
1855
  if(e.ctrlKey) {
1851
1856
  // Remove clicked item from selection
1852
1857
  if(MODEL.selection) {
1853
- // NOTE: first check constraints -- see mouseMove() for motivation
1858
+ // NOTE: First check constraints -- see mouseMove() for motivation.
1854
1859
  if(this.on_constraint) {
1855
1860
  if(MODEL.selection.indexOf(this.on_constraint) >= 0) {
1856
1861
  MODEL.deselect(this.on_constraint);
@@ -1889,7 +1894,7 @@ class GUIController extends Controller {
1889
1894
  } // END IF Ctrl
1890
1895
 
1891
1896
  // Clear selection unless SHIFT pressed or mouseDown while hovering
1892
- // over a SELECTED node or link
1897
+ // over a SELECTED node or link.
1893
1898
  if(!(e.shiftKey ||
1894
1899
  (this.on_node && MODEL.selection.indexOf(this.on_node) >= 0) ||
1895
1900
  (this.on_cluster && MODEL.selection.indexOf(this.on_cluster) >= 0) ||
@@ -1901,7 +1906,7 @@ class GUIController extends Controller {
1901
1906
  }
1902
1907
 
1903
1908
  // If one of the top six sidebar buttons is active, prompt for new node
1904
- // (not link or constraint)
1909
+ // (not link or constraint).
1905
1910
  if(this.active_button && this.active_button !== this.buttons.link &&
1906
1911
  this.active_button !== this.buttons.constraint) {
1907
1912
  this.add_x = this.mouse_x;
@@ -1943,16 +1948,16 @@ class GUIController extends Controller {
1943
1948
  }
1944
1949
 
1945
1950
  // ALT key pressed => open properties dialog if cursor hovers over
1946
- // some element
1951
+ // some element.
1947
1952
  if(e.altKey) {
1948
- // NOTE: first check constraints -- see mouseMove() for motivation
1953
+ // NOTE: First check constraints -- see mouseMove() for motivation.
1949
1954
  if(this.on_constraint) {
1950
1955
  this.showConstraintPropertiesDialog(this.on_constraint);
1951
1956
  } else if(this.on_node) {
1952
1957
  if(this.on_node instanceof Process) {
1953
1958
  this.showProcessPropertiesDialog(this.on_node);
1954
1959
  } else if(e.shiftKey) {
1955
- // Shift-Alt on product is like Shift-Double-click
1960
+ // Shift-Alt on product is like Shift-Double-click.
1956
1961
  this.showReplaceProductDialog(this.on_node);
1957
1962
  } else {
1958
1963
  this.showProductPropertiesDialog(this.on_node);
@@ -1964,7 +1969,7 @@ class GUIController extends Controller {
1964
1969
  } else if(this.on_link) {
1965
1970
  this.showLinkPropertiesDialog(this.on_link);
1966
1971
  }
1967
- // NOTE: first check constraints -- see mouseMove() for motivation
1972
+ // NOTE: First check constraints -- see mouseMove() for motivation.
1968
1973
  } else if(this.on_constraint) {
1969
1974
  MODEL.select(this.on_constraint);
1970
1975
  } else if(this.on_note) {
@@ -1973,17 +1978,17 @@ class GUIController extends Controller {
1973
1978
  this.move_dy = this.mouse_y - this.on_note.y;
1974
1979
  MODEL.select(this.on_note);
1975
1980
  UNDO_STACK.push('move', this.dragged_node, true);
1976
- // Cursor on node => add link or constraint, or start moving
1981
+ // Cursor on node => add link or constraint, or start moving.
1977
1982
  } else if(this.on_node) {
1978
1983
  if(this.active_button === this.buttons.link) {
1979
1984
  this.linking_node = this.on_node;
1980
1985
  // NOTE: return without updating buttons
1981
1986
  return;
1982
1987
  } else if(this.active_button === this.buttons.constraint) {
1983
- // Allow constraints only on nodes having upper bounds defined
1988
+ // Allow constraints only on nodes having upper bounds defined.
1984
1989
  if(this.on_node.upper_bound.defined) {
1985
1990
  this.constraining_node = this.on_node;
1986
- // NOTE: here, too, return without updating buttons
1991
+ // NOTE: Here, too, return without updating buttons.
1987
1992
  return;
1988
1993
  }
1989
1994
  } else {
@@ -1991,7 +1996,7 @@ class GUIController extends Controller {
1991
1996
  this.move_dx = this.mouse_x - this.on_node.x;
1992
1997
  this.move_dy = this.mouse_y - this.on_node.y;
1993
1998
  if(MODEL.selection.indexOf(this.on_node) < 0) MODEL.select(this.on_node);
1994
- // Pass dragged node for UNDO
1999
+ // Pass dragged node for UNDO.
1995
2000
  UNDO_STACK.push('move', this.dragged_node, true);
1996
2001
  }
1997
2002
  } else if(this.on_cluster) {
@@ -2010,23 +2015,23 @@ class GUIController extends Controller {
2010
2015
  }
2011
2016
 
2012
2017
  mouseUp(e) {
2013
- // Responds to mouseup event
2018
+ // Responds to mouseup event.
2014
2019
  const cp = this.paper.cursorPosition(e.pageX, e.pageY);
2015
2020
  this.mouse_up_x = cp[0];
2016
2021
  this.mouse_up_y = cp[1];
2017
- // First check whether user is selecting a rectangle
2022
+ // First check whether user is selecting a rectangle.
2018
2023
  if(this.start_sel_x >= 0 && this.start_sel_y >= 0) {
2019
2024
  // Clear previous selection unless user is adding to it (by still
2020
- // holding SHIFT button down)
2025
+ // holding SHIFT button down).
2021
2026
  if(!e.shiftKey) MODEL.clearSelection();
2022
- // Compute defining points of rectangle (top left and bottom right)
2027
+ // Compute defining points of rectangle (top left and bottom right).
2023
2028
  const
2024
2029
  tlx = Math.min(this.start_sel_x, this.mouse_up_x),
2025
2030
  tly = Math.min(this.start_sel_y, this.mouse_up_y),
2026
2031
  brx = Math.max(this.start_sel_x, this.mouse_up_x),
2027
2032
  bry = Math.max(this.start_sel_y, this.mouse_up_y);
2028
2033
  // If rectangle has size greater than 2x2 pixels, select all elements
2029
- // having their center inside the selection rectangle
2034
+ // having their center inside the selection rectangle.
2030
2035
  if(brx - tlx > 2 && bry - tly > 2) {
2031
2036
  const ol = [], fc = MODEL.focal_cluster;
2032
2037
  for(let i = 0; i < fc.processes.length; i++) {
@@ -2055,19 +2060,19 @@ class GUIController extends Controller {
2055
2060
  }
2056
2061
  for(let i in MODEL.links) if(MODEL.links.hasOwnProperty(i)) {
2057
2062
  const obj = MODEL.links[i];
2058
- // Only add a link if both its nodes are selected as well
2063
+ // Only add a link if both its nodes are selected as well.
2059
2064
  if(fc.linkInList(obj, ol)) {
2060
2065
  ol.push(obj);
2061
2066
  }
2062
2067
  }
2063
2068
  for(let i in MODEL.constraints) if(MODEL.constraints.hasOwnProperty(i)) {
2064
2069
  const obj = MODEL.constraints[i];
2065
- // Only add a constraint if both its nodes are selected as well
2070
+ // Only add a constraint if both its nodes are selected as well.
2066
2071
  if(fc.linkInList(obj, ol)) {
2067
2072
  ol.push(obj);
2068
2073
  }
2069
2074
  }
2070
- // Having compiled the object list, actually select them
2075
+ // Having compiled the object list, actually select them.
2071
2076
  MODEL.selectList(ol);
2072
2077
  this.paper.drawSelection(MODEL);
2073
2078
  }
@@ -2075,10 +2080,10 @@ class GUIController extends Controller {
2075
2080
  this.start_sel_y = -1;
2076
2081
  this.paper.hideDragRect();
2077
2082
 
2078
- // Then check whether user is drawing a flow link
2079
- // (by dragging its endpoint)
2083
+ // Then check whether user is drawing a flow link (by dragging its
2084
+ // endpoint).
2080
2085
  } else if(this.linking_node) {
2081
- // If so, check whether the cursor is over a node of the appropriate type
2086
+ // If so, check whether the cursor is over a node of the appropriate type.
2082
2087
  if(this.on_node && MODEL.canLink(this.linking_node, this.on_node)) {
2083
2088
  const obj = MODEL.addLink(this.linking_node, this.on_node);
2084
2089
  UNDO_STACK.push('add', obj);
@@ -2089,8 +2094,8 @@ class GUIController extends Controller {
2089
2094
  if(!this.stayActive) this.resetActiveButton();
2090
2095
  this.paper.hideDragLine();
2091
2096
 
2092
- // Then check whether user is drawing a constraint link
2093
- // (again: by dragging its endpoint)
2097
+ // Then check whether user is drawing a constraint link (again: by
2098
+ // dragging its endpoint).
2094
2099
  } else if(this.constraining_node) {
2095
2100
  if(this.on_node && this.constraining_node.canConstrain(this.on_node)) {
2096
2101
  // display constraint editor
@@ -2104,35 +2109,37 @@ class GUIController extends Controller {
2104
2109
  UI.drawDiagram(MODEL);
2105
2110
 
2106
2111
  // Then check whether the user is moving a node (possibly part of a
2107
- // larger selection)
2112
+ // larger selection).
2108
2113
  } else if(this.dragged_node) {
2109
2114
  // Always perform the move operation (this will do nothing if the
2110
- // cursor did not move)
2115
+ // cursor did not move).
2111
2116
  MODEL.moveSelection(
2112
2117
  this.mouse_up_x - this.mouse_x, this.mouse_up_y - this.mouse_y);
2113
- // @@TO DO: if on top of a cluster, move it there
2114
- // NOTE: cursor will always be over the selected cluster (while dragging)
2118
+ // Set cursor to pointer, as it should be on some node while dragging.
2119
+ this.paper.container.style.cursor = 'pointer';
2120
+ // @@TO DO: if on top of a cluster, move it there.
2121
+ // NOTE: Cursor will always be over the selected cluster (while dragging).
2115
2122
  if(this.on_cluster && !this.on_cluster.selected) {
2116
2123
  UNDO_STACK.push('drop', this.on_cluster);
2117
2124
  MODEL.dropSelectionIntoCluster(this.on_cluster);
2118
2125
  this.on_node = null;
2119
2126
  this.on_note = null;
2120
2127
  this.target_cluster = null;
2121
- // Redraw cluster to erase its "target corona"
2128
+ // Redraw cluster to erase its orange "target corona".
2122
2129
  UI.paper.drawCluster(this.on_cluster);
2123
2130
  }
2124
2131
 
2125
- // Check wether the cursor has been moved
2132
+ // Check wether the cursor has been moved.
2126
2133
  const
2127
2134
  absdx = Math.abs(this.mouse_down_x - this.mouse_x),
2128
2135
  absdy = Math.abs(this.mouse_down_y - this.mouse_y);
2129
- // If no *significant* move made, remove the move undo
2136
+ // If no *significant* move made, remove the move undo.
2130
2137
  if(absdx + absdy === 0) UNDO_STACK.pop('move');
2131
2138
  if(this.doubleClicked && absdx + absdy < 3) {
2132
2139
  // Double-clicking opens properties dialog, except for clusters;
2133
- // then "drill down", i.e., make the double-clicked cluster focal
2140
+ // then "drill down", i.e., make the double-clicked cluster focal.
2134
2141
  if(this.dragged_node instanceof Cluster) {
2135
- // NOTE: bottom & right cluster edges remain sensitive!
2142
+ // NOTE: Bottom & right cluster edges remain sensitive!
2136
2143
  if(this.on_cluster_edge) {
2137
2144
  this.showClusterPropertiesDialog(this.dragged_node);
2138
2145
  } else {
@@ -2142,7 +2149,7 @@ class GUIController extends Controller {
2142
2149
  if(e.shiftKey) {
2143
2150
  // Shift-double-clicking on a *product* prompts for "remapping"
2144
2151
  // the product position to another product (and potentially
2145
- // deleting the original one if it has no more occurrences)
2152
+ // deleting the original one if it has no more occurrences).
2146
2153
  this.showReplaceProductDialog(this.dragged_node);
2147
2154
  } else {
2148
2155
  this.showProductPropertiesDialog(this.dragged_node);
@@ -2155,7 +2162,7 @@ class GUIController extends Controller {
2155
2162
  }
2156
2163
  this.dragged_node = null;
2157
2164
 
2158
- // Then check whether the user is clicking on a link
2165
+ // Then check whether the user is clicking on a link.
2159
2166
  } else if(this.on_link) {
2160
2167
  if(this.doubleClicked) {
2161
2168
  this.showLinkPropertiesDialog(this.on_link);
@@ -2171,8 +2178,8 @@ class GUIController extends Controller {
2171
2178
  }
2172
2179
 
2173
2180
  dragOver(e) {
2174
- // Accepts products that are dragged from the Finder and do not have
2175
- // a placeholder in the focal cluster
2181
+ // Accept products that are dragged from the Finder and do not have
2182
+ // a placeholder in the focal cluster.
2176
2183
  this.updateCursorPosition(e);
2177
2184
  const p = MODEL.products[e.dataTransfer.getData('text')];
2178
2185
  if(p && MODEL.focal_cluster.indexOfProduct(p) < 0) e.preventDefault();
@@ -2180,7 +2187,7 @@ class GUIController extends Controller {
2180
2187
 
2181
2188
  drop(e) {
2182
2189
  // Adds a product that is dragged from the Finder to the focal cluster
2183
- // at the cursor position if it does not have a placeholder yet
2190
+ // at the cursor position if it does not have a placeholder yet.
2184
2191
  const p = MODEL.products[e.dataTransfer.getData('text')];
2185
2192
  if(p && MODEL.focal_cluster.indexOfProduct(p) < 0) {
2186
2193
  e.preventDefault();
@@ -2189,7 +2196,7 @@ class GUIController extends Controller {
2189
2196
  this.selectNode(p);
2190
2197
  this.drawDiagram(MODEL);
2191
2198
  }
2192
- // NOTE: update afterwards, as the modeler may target a precise (X, Y)
2199
+ // NOTE: Update afterwards, as the modeler may target a precise (X, Y).
2193
2200
  this.updateCursorPosition(e);
2194
2201
  }
2195
2202
 
@@ -2198,14 +2205,16 @@ class GUIController extends Controller {
2198
2205
  //
2199
2206
 
2200
2207
  checkModals(e) {
2201
- // Respond to Escape, Enter and shortcut keys
2208
+ // Respond to Escape, Enter and shortcut keys.
2202
2209
  const
2203
2210
  ttype = e.target.type,
2204
2211
  ttag = e.target.tagName,
2205
2212
  modals = document.getElementsByClassName('modal');
2206
- // Modal dialogs: hide on ESC and move to next input on ENTER
2213
+ // Modal dialogs: hide on ESC and move to next input on ENTER.
2207
2214
  let maxz = 0,
2208
- topmod = null;
2215
+ topmod = null,
2216
+ code = e.code,
2217
+ alt = e.altKey;
2209
2218
  for(let i = 0; i < modals.length; i++) {
2210
2219
  const
2211
2220
  m = modals[i],
@@ -2216,11 +2225,11 @@ class GUIController extends Controller {
2216
2225
  maxz = z;
2217
2226
  }
2218
2227
  }
2219
- // NOTE: consider only the top modal (if any)
2220
- if(e.keyCode === 27) {
2228
+ // NOTE: Consider only the top modal (if any is showing).
2229
+ if(code === 'Escape') {
2221
2230
  e.stopImmediatePropagation();
2222
2231
  if(topmod) topmod.style.display = 'none';
2223
- } else if(e.keyCode === 13 && ttype !== 'textarea') {
2232
+ } else if(code === 'Enter' && ttype !== 'textarea') {
2224
2233
  e.preventDefault();
2225
2234
  if(topmod) {
2226
2235
  const inp = Array.from(topmod.getElementsByTagName('input'));
@@ -2229,73 +2238,78 @@ class GUIController extends Controller {
2229
2238
  if(i < inp.length) {
2230
2239
  inp[i].focus();
2231
2240
  } else if('constraint-modal xp-clusters-modal'.indexOf(topmod.id) >= 0) {
2232
- // NOTE: constraint modal and "ignore clusters" modal must NOT close
2233
- // when Enter is pressed; just de-focus the input field
2241
+ // NOTE: Constraint modal and "ignore clusters" modal must NOT close
2242
+ // when Enter is pressed, but only de-focus the input field.
2234
2243
  e.target.blur();
2235
2244
  } else {
2236
2245
  const btns = topmod.getElementsByClassName('ok-btn');
2237
2246
  if(btns.length > 0) btns[0].dispatchEvent(new Event('click'));
2238
2247
  }
2239
2248
  } else if(this.dr_dialog_order.length > 0) {
2240
- // Send ENTER key event to the top draggable dialog
2249
+ // Send ENTER key event to the top draggable dialog.
2241
2250
  const last = this.dr_dialog_order.length - 1;
2242
2251
  if(last >= 0) {
2243
2252
  const mgr = window[this.dr_dialog_order[last].dataset.manager];
2244
2253
  if(mgr && 'enterKey' in mgr) mgr.enterKey();
2245
2254
  }
2246
2255
  }
2247
- } else if(e.keyCode === 8 &&
2256
+ } else if(code === 'Backspace' &&
2248
2257
  ttype !== 'text' && ttype !== 'password' && ttype !== 'textarea') {
2249
- // Prevent backspace to be interpreted (by FireFox) as "go back in browser"
2258
+ // Prevent backspace to be interpreted (by FireFox) as "go back in browser".
2250
2259
  e.preventDefault();
2251
2260
  } else if(ttag === 'BODY') {
2252
- // Constraint Editor accepts arrow keys
2261
+ // Constraint Editor accepts arrow keys.
2253
2262
  if(topmod && topmod.id === 'constraint-modal') {
2254
- if([37, 38, 39, 40].indexOf(e.keyCode) >= 0) {
2263
+ if(code.startsWith('Arrow')) {
2255
2264
  e.preventDefault();
2256
2265
  CONSTRAINT_EDITOR.arrowKey(e);
2257
2266
  return;
2258
2267
  }
2259
2268
  }
2260
- // Up and down arrow keys
2261
- if([38, 40].indexOf(e.keyCode) >= 0) {
2269
+ // Up and down arrow keys.
2270
+ if(code === 'ArrowUp' || code === 'ArrowDown') {
2262
2271
  e.preventDefault();
2263
- // Send event to the top draggable dialog
2272
+ // Send event to the top draggable dialog.
2264
2273
  const last = this.dr_dialog_order.length - 1;
2265
2274
  if(last >= 0) {
2266
2275
  const mgr = window[this.dr_dialog_order[last].dataset.manager];
2267
- // NOTE: pass key direction as -1 for UP and +1 for DOWN
2276
+ // NOTE: Pass key direction as -1 for UP and +1 for DOWN.
2268
2277
  if(mgr && 'upDownKey' in mgr) mgr.upDownKey(e.keyCode - 39);
2269
2278
  }
2270
2279
  }
2271
- // end, home, Left and right arrow keys
2272
- if([35, 36, 37, 39].indexOf(e.keyCode) >= 0) e.preventDefault();
2273
- if(e.keyCode === 35) {
2280
+ // End, Home, and left and right arrow keys.
2281
+ if(code === 'End') {
2282
+ e.preventDefault();
2274
2283
  MODEL.t = MODEL.end_period - MODEL.start_period + 1;
2275
2284
  UI.updateTimeStep();
2276
2285
  UI.drawDiagram(MODEL);
2277
- } else if(e.keyCode === 36) {
2286
+ } else if(code === 'Home') {
2287
+ e.preventDefault();
2278
2288
  MODEL.t = 1;
2279
2289
  UI.updateTimeStep();
2280
2290
  UI.drawDiagram(MODEL);
2281
- } else if(e.keyCode === 37) {
2291
+ } else if(code === 'ArrowLeft') {
2292
+ e.preventDefault();
2282
2293
  this.stepBack(e);
2283
- } else if(e.keyCode === 39) {
2294
+ } else if(code === 'ArrowRight') {
2295
+ e.preventDefault();
2284
2296
  this.stepForward(e);
2285
- } else if(e.altKey && [67, 77].indexOf(e.keyCode) >= 0) {
2286
- // Special shortcut keys for "clone selection" and "model settings"
2297
+ } else if(alt && code === 'KeyR') {
2298
+ // Alt-R means: run to diagnose infeasible/unbounded problem.
2299
+ VM.solveModel(true);
2300
+ } else if(alt && ['KeyC', 'KeyM'].indexOf(code) >= 0) {
2301
+ // Special shortcut keys for "clone selection" and "model settings".
2287
2302
  const be = new Event('click');
2288
- be.altKey = true;
2289
- if(e.keyCode === 67) {
2303
+ if(code === 'KeyC') {
2290
2304
  this.buttons.clone.dispatchEvent(be);
2291
2305
  } else {
2292
2306
  this.buttons.settings.dispatchEvent(be);
2293
2307
  }
2294
- } else if(!e.shiftKey && !e.altKey &&
2295
- (!topmod || [65, 67, 86].indexOf(e.keyCode) < 0)) {
2296
- // Interpret special keys as shortcuts unless a modal dialog is open
2297
- if(e.keyCode === 46) {
2298
- // DEL button => delete selection
2308
+ } else if(!e.shiftKey && !alt &&
2309
+ (!topmod || ['KeyA', 'KeyC', 'KeyV'].indexOf(code) < 0)) {
2310
+ // Interpret special keys as shortcuts unless a modal dialog is open.
2311
+ if(code === 'Delete') {
2312
+ // DEL button => delete selection.
2299
2313
  e.preventDefault();
2300
2314
  if(!this.hidden('constraint-modal')) {
2301
2315
  CONSTRAINT_EDITOR.deleteBoundLine();
@@ -2304,18 +2318,18 @@ class GUIController extends Controller {
2304
2318
  } else {
2305
2319
  this.buttons['delete'].dispatchEvent(new Event('click'));
2306
2320
  }
2307
- } else if (e.keyCode === 190 && (e.ctrlKey || e.metaKey)) {
2308
- // Ctrl-. (dot) moves entire diagram to upper-left corner
2321
+ } else if (code === 'Period' && (e.ctrlKey || e.metaKey)) {
2322
+ // Ctrl-. (dot) moves entire diagram to upper-left corner.
2309
2323
  e.preventDefault();
2310
2324
  this.paper.fitToSize();
2311
2325
  MODEL.alignToGrid();
2312
- } else if (e.keyCode >= 65 && e.keyCode <= 90 && (e.ctrlKey || e.metaKey)) {
2313
- // ALWAYS prevent browser to do respond to Ctrl-letter commands
2314
- // NOTE: this cannot prevent a new tab from opening on Ctrl-T
2326
+ } else if (code >= 'KeyA' && code <= 'KeyZ' && (e.ctrlKey || e.metaKey)) {
2327
+ // ALWAYS prevent browser to do respond to Ctrl-letter commands.
2328
+ // NOTE: This cannot prevent a new tab from opening on Ctrl-T.
2315
2329
  e.preventDefault();
2316
- let shortcut = String.fromCharCode(e.keyCode);
2330
+ let shortcut = code.substring(3);
2317
2331
  if(shortcut === 'Z' && e.shiftKey) {
2318
- // Interpret Shift-Ctrl-Z as Ctrl-Y (redo last undone operation)
2332
+ // Interpret Shift-Ctrl-Z as Ctrl-Y (redo last undone operation).
2319
2333
  shortcut = 'Y';
2320
2334
  }
2321
2335
  if(this.shortcuts.hasOwnProperty(shortcut)) {
@@ -2666,9 +2680,15 @@ class GUIController extends Controller {
2666
2680
  //
2667
2681
  // Informing the modeler via the status line
2668
2682
  //
2669
-
2683
+
2684
+ clearStatusLine() {
2685
+ // Clear message on the status line.
2686
+ this.info_line.innerHTML = '';
2687
+ UI.info_line.classList.remove(...UI.info_line.classList);
2688
+ }
2689
+
2670
2690
  setMessage(msg, type=null) {
2671
- // Displays message on infoline unless no type (= plain text) and some
2691
+ // Display `msg` on infoline unless no type (= plain text) and some
2672
2692
  // info, warning or error message is already displayed.
2673
2693
  super.setMessage(msg, type);
2674
2694
  const types = ['notification', 'warning', 'error'];
@@ -2692,8 +2712,8 @@ class GUIController extends Controller {
2692
2712
  // Queue warnings if an error message is still being displayed.
2693
2713
  setTimeout(() => {
2694
2714
  UI.info_line.innerHTML = msg;
2695
- UI.info_line.classList.remove(...types);
2696
- UI.info_line.classList.add(type);
2715
+ UI.info_line.classList.remove(...UI.info_line.classList);
2716
+ if(type) UI.info_line.classList.add(type);
2697
2717
  UI.updateIssuePanel();
2698
2718
  }, rt);
2699
2719
  } else if(lmti < 0 || mti > lmti || rt <= 0) {
@@ -2715,8 +2735,8 @@ class GUIController extends Controller {
2715
2735
  UI.updateIssuePanel();
2716
2736
  }, this.message_display_time);
2717
2737
  }
2718
- UI.info_line.classList.remove(...types);
2719
- UI.info_line.classList.add(type);
2738
+ UI.info_line.classList.remove(...UI.info_line.classList);
2739
+ if(type) UI.info_line.classList.add(type);
2720
2740
  UI.info_line.innerHTML = msg;
2721
2741
  }
2722
2742
  }
@@ -2869,11 +2889,15 @@ class GUIController extends Controller {
2869
2889
  }
2870
2890
  }
2871
2891
  } else if(type === 'process' || type === 'product') {
2892
+ /* NOT CLEAR WHAT THIS CODE DOES, SO DISABLE IT
2872
2893
  if(this.dbl_clicked_node) {
2873
2894
  n = this.dbl_clicked_node;
2874
2895
  md = this.modals['add-' + type];
2875
2896
  this.dbl_clicked_node = null;
2876
2897
  } else {
2898
+ */
2899
+ if(true) { // added line
2900
+ this.dbl_clicked_node = null; // added line
2877
2901
  if(type === 'process') {
2878
2902
  md = this.modals['add-process'];
2879
2903
  nn = md.element('name').value;
@@ -2927,7 +2951,7 @@ class GUIController extends Controller {
2927
2951
  UNDO_STACK.pop();
2928
2952
  return false;
2929
2953
  }
2930
- // NOTE: pre-check if product exists
2954
+ // NOTE: Pre-check if product exists.
2931
2955
  const pp = MODEL.objectByName(nn);
2932
2956
  n = MODEL.addProduct(nn);
2933
2957
  if(n) {
@@ -2948,7 +2972,7 @@ class GUIController extends Controller {
2948
2972
  }
2949
2973
  if(n) {
2950
2974
  // If process, and X and Y are set, it exists; then if not in the
2951
- // focal cluster, ask whether to move it there
2975
+ // focal cluster, ask whether to move it there.
2952
2976
  if(n instanceof Process && (n.x !== 0 || n.y !== 0)) {
2953
2977
  if(n.cluster !== MODEL.focal_cluster) {
2954
2978
  this.confirmToMoveNode(n);
@@ -2966,9 +2990,9 @@ class GUIController extends Controller {
2966
2990
  MODEL.inferIgnoredEntities();
2967
2991
  if(n) {
2968
2992
  md.hide();
2969
- // Select the newly added entity
2970
- // NOTE: If the focal cluster was selected (via the top tool bar), it
2971
- // cannot be selected
2993
+ // Select the newly added entity.
2994
+ // NOTE: If the focal cluster was selected (via the top tool bar),
2995
+ // it cannot be selected.
2972
2996
  if(n !== MODEL.focal_cluster) this.selectNode(n);
2973
2997
  }
2974
2998
  }
@@ -3526,12 +3550,13 @@ console.log('HERE name conflicts', name_conflicts, mapping);
3526
3550
  md.element('block-length').value = model.block_length;
3527
3551
  md.element('look-ahead').value = model.look_ahead;
3528
3552
  md.element('time-limit').value = model.timeout_period;
3529
- this.setBox('settings-encrypt', model.encrypt);
3530
3553
  this.setBox('settings-decimal-comma', model.decimal_comma);
3531
3554
  this.setBox('settings-align-to-grid', model.align_to_grid);
3555
+ this.setBox('settings-block-arrows', model.show_block_arrows);
3556
+ this.setBox('settings-diagnose', MODEL.always_diagnose);
3532
3557
  this.setBox('settings-cost-prices', model.infer_cost_prices);
3533
3558
  this.setBox('settings-report-results', model.report_results);
3534
- this.setBox('settings-block-arrows', model.show_block_arrows);
3559
+ this.setBox('settings-encrypt', model.encrypt);
3535
3560
  md.show('name');
3536
3561
  }
3537
3562
 
@@ -3586,6 +3611,12 @@ console.log('HERE name conflicts', name_conflicts, mapping);
3586
3611
  model.report_results = UI.boxChecked('settings-report-results');
3587
3612
  model.encrypt = UI.boxChecked('settings-encrypt');
3588
3613
  model.decimal_comma = UI.boxChecked('settings-decimal-comma');
3614
+ MODEL.always_diagnose = this.boxChecked('settings-diagnose');
3615
+ // Notify modeler that diagnosis changes the value of +INF.
3616
+ if(MODEL.always_diagnose) {
3617
+ UI.notify('To diagnose unbounded problems, values beyond 1e+10 ' +
3618
+ 'are considered as infinite (\u221E)');
3619
+ }
3589
3620
  // Some changes may necessitate redrawing the diagram
3590
3621
  let cb = UI.boxChecked('settings-align-to-grid'),
3591
3622
  redraw = !model.align_to_grid && cb;
@@ -3653,7 +3684,6 @@ console.log('HERE name conflicts', name_conflicts, mapping);
3653
3684
  md.element('preference').innerHTML = html.join('');
3654
3685
  md.element('int-feasibility').value = MODEL.integer_tolerance;
3655
3686
  md.element('mip-gap').value = MODEL.MIP_gap;
3656
- this.setBox('solver-diagnose', MODEL.always_diagnose);
3657
3687
  md.show();
3658
3688
  }
3659
3689
 
@@ -3682,11 +3712,6 @@ console.log('HERE name conflicts', name_conflicts, mapping);
3682
3712
  }
3683
3713
  MODEL.integer_tolerance = Math.max(1e-9, Math.min(0.1, itol));
3684
3714
  MODEL.MIP_gap = Math.max(0, Math.min(0.5, mgap));
3685
- MODEL.always_diagnose = this.boxChecked('solver-diagnose');
3686
- if(MODEL.always_diagnose) {
3687
- UI.notify('To diagnose unbounded problems, values beyond 1e+10 ' +
3688
- 'are considered as infinite (\u221E)');
3689
- }
3690
3715
  // Close the dialog.
3691
3716
  md.hide();
3692
3717
  }
@@ -283,7 +283,7 @@ class DocumentationManager {
283
283
  if(list.indexOf(this.entity) >= 0) {
284
284
  this.stopEditing();
285
285
  this.entity = null;
286
- this.title.innerHTML = 'Documentation';
286
+ this.title.innerHTML = 'Information and documentation';
287
287
  this.viewer.innerHTML = this.about_linny_r;
288
288
  }
289
289
  }
@@ -74,16 +74,16 @@ Attributes, however, are case sensitive!">[Actor X|CF]</code> for cash flow.
74
74
  <code title="Number of last round in the sequence (1=a, 2=b, etc.)">lr</code>,
75
75
  <code title="Number of rounds in the sequence">nr</code>,
76
76
  <code title="Number of current experiment run (starts at 0)">x</code>,
77
- <code title="Number of runs in the experiment">nx</code>,
78
- <span title="Index variables of iterator dimensions)">
77
+ <code title="Number of runs in the current experiment">nx</code>,
78
+ <span title="Index variables of iterator dimensions">
79
79
  <code>i</code>, <code>j</code>, <code>k</code>,
80
80
  </span>
81
- <code title="Number of time steps in 1 year)">yr</code>,
82
- <code title="Number of time steps in 1 week)">wk</code>,
83
- <code title="Number of time steps in 1 day)">d</code>,
84
- <code title="Number of time steps in 1 hour)">h</code>,
85
- <code title="Number of time steps in 1 minute)">m</code>,
86
- <code title="Number of time steps in 1 second)">s</code>,
81
+ <code title="Number of time steps in 1 year">yr</code>,
82
+ <code title="Number of time steps in 1 week">wk</code>,
83
+ <code title="Number of time steps in 1 day">d</code>,
84
+ <code title="Number of time steps in 1 hour">h</code>,
85
+ <code title="Number of time steps in 1 minute">m</code>,
86
+ <code title="Number of time steps in 1 second">s</code>,
87
87
  <code title="A random number from the uniform distribution U(0, 1)">random</code>),
88
88
  constants (<code title="Mathematical constant &pi; = ${Math.PI}">pi</code>,
89
89
  <code title="Logical constant true = 1
@@ -178,8 +178,8 @@ NOTE: Grouping groups results in a single group, e.g., (1;2);(3;4;5) evaluates a
178
178
  }
179
179
 
180
180
  editExpression(event) {
181
- // Infer which entity property expression is to edited from the button
182
- // that was clicked, and then opens the dialog.
181
+ // Infer which entity property expression is to be edited from the
182
+ // button that was clicked, and then opens the dialog.
183
183
  const
184
184
  btn = event.target,
185
185
  ids = btn.id.split('-'), // 3-tuple [entity type, attribute, 'x']