linny-r 1.6.3 → 1.6.4

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.6.3",
3
+ "version": "1.6.4",
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
@@ -1300,6 +1300,23 @@ NOTE: Unit symbols are case-sensitive, so BTU ≠ Btu">
1300
1300
  </div>
1301
1301
  </div>
1302
1302
 
1303
+ <!-- BOUNDLINE POINT modal -->
1304
+ <div id="boundline-point-modal" class="modal">
1305
+ <div id="boundline-point-dlg" class="inp-dlg">
1306
+ <div class="dlg-title">
1307
+ Precise point coordinates
1308
+ <img class="cancel-btn" src="images/cancel.png">
1309
+ <img class="ok-btn" src="images/ok.png">
1310
+ </div>
1311
+ <div id="boundline-point-x-lbl">X =</div>
1312
+ <input id="boundline-point-x" type="text" autocomplete="off">
1313
+ <div id="boundline-point-x-percent">%</div>
1314
+ <div id="boundline-point-y-lbl">Y =</div>
1315
+ <input id="boundline-point-y" type="text" autocomplete="off">
1316
+ <div id="boundline-point-y-percent">%</div>
1317
+ </div>
1318
+ </div>
1319
+
1303
1320
  <!-- PROCESS properties dialog -->
1304
1321
  <div id="process-modal" class="modal">
1305
1322
  <div id="process-dlg" class="inp-dlg">
@@ -1922,8 +1922,8 @@ div.menu-item:hover {
1922
1922
  #constraint-point {
1923
1923
  position: absolute;
1924
1924
  top: 393px;
1925
- left: 160px;
1926
- width: 75px;
1925
+ left: 100px;
1926
+ width: 175px;
1927
1927
  text-align: center;
1928
1928
  }
1929
1929
 
@@ -2009,6 +2009,50 @@ div.menu-item:hover {
2009
2009
  right: 0px;
2010
2010
  }
2011
2011
 
2012
+ /* the BOUNDLINE POINT DIALOG allows entering precise coordinates */
2013
+ #boundline-point-dlg {
2014
+ width: 200px;
2015
+ height: 67px;
2016
+ }
2017
+
2018
+ #boundline-point-x-lbl {
2019
+ position: absolute;
2020
+ top: 25px;
2021
+ left: 2px;
2022
+ }
2023
+
2024
+ #boundline-point-x {
2025
+ position: absolute;
2026
+ top: 23px;
2027
+ left: 24px;
2028
+ width: 160px;
2029
+ }
2030
+
2031
+ #boundline-point-x-percent {
2032
+ position: absolute;
2033
+ top: 25px;
2034
+ left: 188px;
2035
+ }
2036
+
2037
+ #boundline-point-y-lbl {
2038
+ position: absolute;
2039
+ top: 47px;
2040
+ left: 2px;
2041
+ }
2042
+
2043
+ #boundline-point-y {
2044
+ position: absolute;
2045
+ top: 45px;
2046
+ left: 24px;
2047
+ width: 160px;
2048
+ }
2049
+
2050
+ #boundline-point-y-percent {
2051
+ position: absolute;
2052
+ top: 47px;
2053
+ left: 188px;
2054
+ }
2055
+
2012
2056
  /* NOTE: The following dialogs are not modal, but "stay-on-top",
2013
2057
  i.e., windows that remain open (until dismissed) and above
2014
2058
  the main window, and they are draggable and resizable.
@@ -56,7 +56,7 @@ class ConstraintEditor {
56
56
  this.container.addEventListener('mousemove',
57
57
  (event) => CONSTRAINT_EDITOR.mouseMove(event));
58
58
  this.container.addEventListener('mousedown',
59
- () => CONSTRAINT_EDITOR.mouseDown());
59
+ () => CONSTRAINT_EDITOR.mouseDown(event));
60
60
  this.container.addEventListener('mouseup',
61
61
  () => CONSTRAINT_EDITOR.mouseUp());
62
62
  // NOTE: interpret leaving the area as a mouse-up so that dragging ceases
@@ -84,6 +84,11 @@ class ConstraintEditor {
84
84
  this.delete_bl_btn = document.getElementById('del-bl-btn');
85
85
  this.delete_bl_btn.addEventListener('click',
86
86
  () => CONSTRAINT_EDITOR.deleteBoundLine());
87
+ this.point_modal = new ModalDialog('boundline-point');
88
+ this.point_modal.ok.addEventListener(
89
+ 'click', () => CONSTRAINT_EDITOR.setPointPosition());
90
+ this.point_modal.cancel.addEventListener(
91
+ 'click', () => CONSTRAINT_EDITOR.point_modal.hide());
87
92
  // The chart is stored as an SVG string
88
93
  this.svg = '';
89
94
  // Scale, origin X and Y assume a 300x300 px square chart area
@@ -93,7 +98,7 @@ class ConstraintEditor {
93
98
  // 0 => silver, LE => orange/red, GE => cyan/blue, EQ => purple
94
99
  this.line_color = ['#a0a0a0', '#c04000', '#0040c0', '#9000a0'];
95
100
  // Use brighter shades if selected (darker for gray)
96
- this.selected_color = ['#808080', '#ff8040', '#00ffff', '#a800ff'];
101
+ this.selected_color = ['#808080', '#ff8040', '#00b0d0', '#a800ff'];
97
102
  // The selected bound line object (NULL => no line selected)
98
103
  this.selected = null;
99
104
  // Cursor position in chart coordinates (100 x 100 grid)
@@ -105,6 +110,7 @@ class ConstraintEditor {
105
110
  this.on_point = -1;
106
111
  this.dragged_point = -1;
107
112
  this.selected_point = -1;
113
+ this.last_time_clicked = 0;
108
114
  this.cursor = 'default';
109
115
  // Properties for tracking which constraint is being edited.
110
116
  this.edited_constraint = null;
@@ -118,6 +124,21 @@ class ConstraintEditor {
118
124
  // NOTE: All edits will be ignored unless the modeler clicks OK.
119
125
  }
120
126
 
127
+ get doubleClicked() {
128
+ const
129
+ now = Date.now(),
130
+ dt = now - this.last_time_clicked;
131
+ this.last_time_clicked = now;
132
+ if(this.on_point === this.selected_point) {
133
+ // Consider click to be "double" if it occurred less than 300 ms ago
134
+ if(dt < 300) {
135
+ this.last_time_clicked = 0;
136
+ return true;
137
+ }
138
+ }
139
+ return false;
140
+ }
141
+
121
142
  mouseMove(e) {
122
143
  // The onMouseMove response of the constraint editor's graph area
123
144
  // Calculate cursor point without restricting it to 100x100 grid
@@ -132,16 +153,18 @@ class ConstraintEditor {
132
153
  this.pos_y = Math.min(100, Math.max(0, y));
133
154
  this.updateStatus();
134
155
  if(this.dragged_point >= 0) {
135
- this.movePoint();
156
+ this.movePoint(this.pos_x, this.pos_y);
136
157
  } else {
137
158
  this.checkLines();
138
159
  }
139
160
  }
140
161
 
141
- mouseDown() {
142
- // The onMouseDown response of the constraint editor's graph area
162
+ mouseDown(e) {
163
+ // The onMouseDown response of the constraint editor's graph area.
143
164
  if(this.adding_point) {
144
165
  this.doAddPointToLine();
166
+ } else if((e.altKey || this.doubleClicked) && this.on_point >= 0) {
167
+ this.positionPoint();
145
168
  } else if(this.on_line) {
146
169
  this.selectBoundLine(this.on_line);
147
170
  this.dragged_point = this.on_point;
@@ -155,7 +178,7 @@ class ConstraintEditor {
155
178
  }
156
179
 
157
180
  mouseUp() {
158
- // The onMouseUp response of the constraint editor's graph area
181
+ // The onMouseUp response of the constraint editor's graph area.
159
182
  this.dragged_point = -1;
160
183
  this.container.style.cursor = this.cursor;
161
184
  this.updateStatus();
@@ -179,36 +202,85 @@ class ConstraintEditor {
179
202
  this.container.style.cursor = this.cursor;
180
203
  }
181
204
 
182
- arrowKey(k) {
205
+ arrowKey(e) {
206
+ // Move point by 1 grid unit (1/3 pixel), or more precisely when
207
+ // Shift, Ctrl and/or Alt are pressed. Shift resolution is 1/10,
208
+ // Ctrl resolution = 1/100, combined => 1/1000. Just Alt resolution
209
+ // is 1/10000, and with Shift + Ctrl becomes 1e-7.
183
210
  if(this.selected && this.selected_point >= 0) {
211
+ const custom = e.shiftKey || e.ctrlKey || e.altKey;
212
+ let divisor = 3;
213
+ if(e.shiftKey) {
214
+ divisor = 10;
215
+ if(e.ctrlKey) divisor = 1000;
216
+ } else if(e.ctrlKey) {
217
+ divisor = 100;
218
+ }
219
+ if(e.altKey) {
220
+ if(divisor === 3) {
221
+ divisor = 10000;
222
+ } else {
223
+ divisor *= 10000;
224
+ }
225
+ }
184
226
  const
227
+ k = e.keyCode,
185
228
  i = this.selected_point,
186
229
  pts = this.selected.points,
187
230
  li = pts.length - 1,
188
231
  p = pts[this.selected_point],
189
232
  minx = (i === 0 ? 0 : (i === li ? 100 : pts[i - 1][0])),
190
233
  maxx = (i === 0 ? 0 : (i === li ? 100 : pts[i + 1][0]));
234
+ let cx = false,
235
+ cy = false;
191
236
  if(k === 37) {
192
- p[0] = Math.max(minx, p[0] - 1/3);
193
- } else if (k === 38 && p[1] <= 299/3) {
194
- p[1] += 1/3;
237
+ p[0] = Math.max(minx, p[0] - 1 / divisor);
238
+ cx = true;
239
+ } else if (k === 38 && p[1] <= 100 - 1 / divisor) {
240
+ p[1] += 1 / divisor;
241
+ cy = true;
195
242
  } else if (k === 39) {
196
- p[0] = Math.min(maxx, p[0] + 1/3);
197
- } else if (k === 40 && p[1] >= 1/3) {
198
- p[1] -= 1/3;
243
+ p[0] = Math.min(maxx, p[0] + 1 / divisor);
244
+ cx = true;
245
+ } else if (k === 40 && p[1] >= 1 / divisor) {
246
+ p[1] -= 1 / divisor;
247
+ cy = true;
248
+ }
249
+ // NOTE: Compensate for small numerical errors
250
+ const cp = this.customPoint(p[0], p[1]);
251
+ if(cx) {
252
+ if(cp & 1 && custom) {
253
+ p[0] = Math.round(divisor * p[0]) / divisor;
254
+ } else {
255
+ p[0] = Math.round(3 * p[0]) / 3;
256
+ }
257
+ }
258
+ if(cy) {
259
+ if(cp & 2 && custom) {
260
+ p[1] = Math.round(divisor * p[1]) / divisor;
261
+ } else {
262
+ p[1] = Math.round(3 * p[1]) / 3;
263
+ }
199
264
  }
200
- // NOTE: compensate for small numerical errors
201
- p[0] = Math.round(3 * p[0]) / 3;
202
- p[1] = Math.round(3 * p[1]) / 3;
203
265
  this.draw();
204
266
  this.updateEquation();
205
267
  }
206
268
  }
207
269
 
270
+ customPoint(x, y) {
271
+ // Return 0 if `x` and `y` both are "regular" points on the pixel
272
+ // grid. For these regular points, X and Y are multiples of 1/3,
273
+ // so 3*X and 3*Y should both be integer (apart from numerical
274
+ // imprecision). If only X is custom, return 1, if only Y is custom,
275
+ // return 2, and if both are custom, return 3.
276
+ return (Math.abs(3*x - Math.round(3*x)) > VM.NEAR_ZERO ? 1 : 0) +
277
+ (Math.abs(3*y - Math.round(3*y)) > VM.NEAR_ZERO ? 2 : 0);
278
+ }
279
+
208
280
  point(x, y) {
209
- // Returns a string denoting the point (x, y) in SVG notation, assuming
281
+ // Return a string denoting the point (x, y) in SVG notation, assuming
210
282
  // that x and y are mathematical coordinates (y-axis pointing UP) and
211
- // scaled to the constraint editor chart area, cf. global constants
283
+ // scaled to the constraint editor chart area, cf. global constants.
212
284
  // defined for the constraint editor.
213
285
  return (this.oX + x * this.scale) + ',' + (this.oY - y * this.scale);
214
286
  }
@@ -396,10 +468,52 @@ class ConstraintEditor {
396
468
  }
397
469
  this.equation_div.innerHTML = segeq;
398
470
  }
471
+
472
+ positionPoint() {
473
+ // Prompt modeler for precise point coordinates.
474
+ if(this.selected_point < 0) return;
475
+ const
476
+ md = this.point_modal,
477
+ pc = this.point_div.innerHTML.split(', ');
478
+ md.element('x').value = pc[0].substring(1);
479
+ md.element('y').value = pc[1].substring(0, pc[1].length - 1);
480
+ md.show('x');
481
+ }
482
+
483
+ validPointCoordinate(c) {
484
+ // Return floating point for coordinate `c` (= 'x' or 'y') or FALSE
485
+ // if input is invalid.
486
+ const
487
+ md = this.point_modal,
488
+ e = md.element(c),
489
+ v = safeStrToFloat(e.value, false);
490
+ if(v === false || v < 0 || v > 100) {
491
+ UI.warn('Invalid boundline point coordinate');
492
+ e.select();
493
+ e.focus();
494
+ return false;
495
+ }
496
+ return v;
497
+ }
498
+
499
+ setPointPosition() {
500
+ // Check coordinates, and if valid update those of the selected point.
501
+ // Otherwise, warn user and select offending input field.
502
+ if(this.selected_point < 0) return;
503
+ const
504
+ x = this.validPointCoordinate('x'),
505
+ y = this.validPointCoordinate('y');
506
+ if(x !== false && y !== false) {
507
+ this.dragged_point = this.selected_point;
508
+ this.movePoint(x, y);
509
+ this.dragged_point = -1;
510
+ this.point_modal.hide();
511
+ }
512
+ }
399
513
 
400
- movePoint() {
401
- // Moves the dragged point of the selected bound line
402
- // Use l as shorthand for the selected line
514
+ movePoint(x, y) {
515
+ // Move the dragged point of the selected bound line.
516
+ // Use `l` as shorthand for the selected line.
403
517
  const
404
518
  l = this.selected,
405
519
  pi = this.dragged_point,
@@ -411,8 +525,8 @@ class ConstraintEditor {
411
525
  py = p[1],
412
526
  minx = (pi === 0 ? 0 : (pi === lpi ? 100 : l.points[pi - 1][0])),
413
527
  maxx = (pi === 0 ? 0 : (pi === lpi ? 100 : l.points[pi + 1][0])),
414
- newx = Math.min(maxx, Math.max(minx, this.pos_x)),
415
- newy = Math.min(100, Math.max(0, this.pos_y));
528
+ newx = Math.min(maxx, Math.max(minx, x)),
529
+ newy = Math.min(100, Math.max(0, y));
416
530
  // No action needed unless point has been moved
417
531
  if(newx !== px || newy !== py) {
418
532
  p[0] = newx;
@@ -424,17 +538,22 @@ class ConstraintEditor {
424
538
 
425
539
  updateStatus() {
426
540
  // Displays cursor position as X and Y (in chart coordinates), and updates
427
- // controls
541
+ // controls.
428
542
  this.pos_x_div.innerHTML = 'X = ' + this.pos_x.toPrecision(3);
429
543
  this.pos_y_div.innerHTML = 'Y = ' + this.pos_y.toPrecision(3);
544
+ this.point_div.innerHTML = '';
430
545
  const blbtns = 'add-point del-bl';
431
546
  if(this.selected) {
432
547
  if(this.selected_point >= 0) {
433
548
  const p = this.selected.points[this.selected_point];
434
- this.point_div.innerHTML =
435
- `(${p[0].toPrecision(3)}, ${p[1].toPrecision(3)})`;
436
- } else {
437
- this.point_div.innerHTML = '';
549
+ let px = p[0].toPrecision(3),
550
+ py = p[1].toPrecision(3),
551
+ cp = this.customPoint(p[0], p[1]);
552
+ if(cp & 1) px = p[0].toPrecision(10).replace(/[0]+$/, '')
553
+ .replace(/\.$/, '');
554
+ if(cp & 2) py = p[1].toPrecision(10).replace(/[0]+$/, '')
555
+ .replace(/\.$/, '');
556
+ this.point_div.innerHTML = `(${px}, ${py})`;
438
557
  }
439
558
  // Check whether selected point is an end point
440
559
  const ep = this.selected_point === 0 ||
@@ -591,13 +710,19 @@ class ConstraintEditor {
591
710
  width = 1.5;
592
711
  color = this.line_color[l.type];
593
712
  }
594
- const cfs = `fill="${color}" stroke="${color}" stroke-width="${width}"`;
713
+ const
714
+ cfs = `fill="${color}" stroke="${color}" stroke-width="${width}"`,
715
+ icfs = 'fill="white" stroke="white" stroke-width="1"';
595
716
  for(let i = 0; i < l.points.length; i++) {
596
717
  const
597
718
  px = l.points[i][0],
598
719
  py = l.points[i][1];
599
720
  pp.push(this.point(px, py));
600
721
  dots += `<circle ${this.circleCenter(px, py)} r="3" ${cfs}></circle>`;
722
+ // Draw "custom points" with a white inner circle.
723
+ if(this.customPoint(px, py)) {
724
+ dots += `<circle ${this.circleCenter(px, py)} r="1.5" ${icfs}></circle>`;
725
+ }
601
726
  }
602
727
  const cp = 'M' + pp.join('L');
603
728
  // For EQ bound lines, the line path is the contour; this will be
@@ -2154,7 +2154,7 @@ class GUIController extends Controller {
2154
2154
  if(topmod && topmod.id === 'constraint-modal') {
2155
2155
  if([37, 38, 39, 40].indexOf(e.keyCode) >= 0) {
2156
2156
  e.preventDefault();
2157
- CONSTRAINT_EDITOR.arrowKey(e.keyCode);
2157
+ CONSTRAINT_EDITOR.arrowKey(e);
2158
2158
  return;
2159
2159
  }
2160
2160
  }
@@ -6954,7 +6954,7 @@ function VMI_log(x) {
6954
6954
  if(d !== false) {
6955
6955
  if(DEBUGGING) console.log('LOG (' + d.join(', ') + ')');
6956
6956
  try {
6957
- d = Math.exp(Math.log(d[1]) / Math.log(d[0]));
6957
+ d = Math.log(d[1]) / Math.log(d[0]);
6958
6958
  } catch(err) {
6959
6959
  d = VM.BAD_CALC;
6960
6960
  }
@@ -8093,9 +8093,9 @@ const
8093
8093
  '%', '^', 'log', '|'],
8094
8094
  DYADIC_CODES = [
8095
8095
  VMI_concat, VMI_if_then, VMI_if_else, VMI_or, VMI_and,
8096
- VMI_eq, VMI_ne, VMI_ne,
8097
- VMI_gt, VMI_lt, VMI_ge, VMI_le, VMI_add, VMI_sub, VMI_mul, VMI_div,
8098
- VMI_mod, VMI_power, VMI_log, VMI_replace_undefined],
8096
+ VMI_eq, VMI_ne, VMI_ne, VMI_gt, VMI_lt, VMI_ge, VMI_le,
8097
+ VMI_add, VMI_sub, VMI_mul, VMI_div, VMI_mod,
8098
+ VMI_power, VMI_log, VMI_replace_undefined],
8099
8099
 
8100
8100
  // Compiler checks for random codes as they make an expression dynamic
8101
8101
  RANDOM_CODES = [VMI_binomial, VMI_exponential, VMI_normal, VMI_poisson,