linny-r 1.4.2 → 1.4.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.
Files changed (50) hide show
  1. package/README.md +162 -74
  2. package/package.json +1 -1
  3. package/server.js +145 -49
  4. package/static/images/check-off-not-same-changed.png +0 -0
  5. package/static/images/check-off-not-same-not-changed.png +0 -0
  6. package/static/images/check-off-same-changed.png +0 -0
  7. package/static/images/check-off-same-not-changed.png +0 -0
  8. package/static/images/check-on-not-same-changed.png +0 -0
  9. package/static/images/check-on-not-same-not-changed.png +0 -0
  10. package/static/images/check-on-same-changed.png +0 -0
  11. package/static/images/check-on-same-not-changed.png +0 -0
  12. package/static/images/eq-not-same-changed.png +0 -0
  13. package/static/images/eq-not-same-not-changed.png +0 -0
  14. package/static/images/eq-same-changed.png +0 -0
  15. package/static/images/eq-same-not-changed.png +0 -0
  16. package/static/images/ne-not-same-changed.png +0 -0
  17. package/static/images/ne-not-same-not-changed.png +0 -0
  18. package/static/images/ne-same-changed.png +0 -0
  19. package/static/images/ne-same-not-changed.png +0 -0
  20. package/static/images/octaeder.svg +993 -0
  21. package/static/images/sort-asc-lead.png +0 -0
  22. package/static/images/sort-asc.png +0 -0
  23. package/static/images/sort-desc-lead.png +0 -0
  24. package/static/images/sort-desc.png +0 -0
  25. package/static/images/sort-not.png +0 -0
  26. package/static/index.html +72 -647
  27. package/static/linny-r.css +199 -417
  28. package/static/scripts/linny-r-gui-actor-manager.js +340 -0
  29. package/static/scripts/linny-r-gui-chart-manager.js +944 -0
  30. package/static/scripts/linny-r-gui-constraint-editor.js +681 -0
  31. package/static/scripts/linny-r-gui-controller.js +4005 -0
  32. package/static/scripts/linny-r-gui-dataset-manager.js +1176 -0
  33. package/static/scripts/linny-r-gui-documentation-manager.js +739 -0
  34. package/static/scripts/linny-r-gui-equation-manager.js +307 -0
  35. package/static/scripts/linny-r-gui-experiment-manager.js +1944 -0
  36. package/static/scripts/linny-r-gui-expression-editor.js +449 -0
  37. package/static/scripts/linny-r-gui-file-manager.js +392 -0
  38. package/static/scripts/linny-r-gui-finder.js +727 -0
  39. package/static/scripts/linny-r-gui-model-autosaver.js +230 -0
  40. package/static/scripts/linny-r-gui-monitor.js +448 -0
  41. package/static/scripts/linny-r-gui-paper.js +2789 -0
  42. package/static/scripts/linny-r-gui-receiver.js +323 -0
  43. package/static/scripts/linny-r-gui-repository-browser.js +819 -0
  44. package/static/scripts/linny-r-gui-scale-unit-manager.js +244 -0
  45. package/static/scripts/linny-r-gui-sensitivity-analysis.js +778 -0
  46. package/static/scripts/linny-r-gui-undo-redo.js +560 -0
  47. package/static/scripts/linny-r-model.js +27 -11
  48. package/static/scripts/linny-r-utils.js +17 -2
  49. package/static/scripts/linny-r-vm.js +31 -12
  50. package/static/scripts/linny-r-gui.js +0 -16761
@@ -0,0 +1,681 @@
1
+ /*
2
+ Linny-R is an executable graphical specification language for (mixed integer)
3
+ linear programming (MILP) problems, especially unit commitment problems (UCP).
4
+ The Linny-R language and tool have been developed by Pieter Bots at Delft
5
+ University of Technology, starting in 2009. The project to develop a browser-
6
+ based version started in 2017. See https://linny-r.org for more information.
7
+
8
+ This JavaScript file (linny-r-gui-constraint-editor.js) provides the GUI
9
+ dialog for the Linny-R constraint editor.
10
+
11
+ */
12
+
13
+ /*
14
+ Copyright (c) 2017-2023 Delft University of Technology
15
+
16
+ Permission is hereby granted, free of charge, to any person obtaining a copy
17
+ of this software and associated documentation files (the "Software"), to deal
18
+ in the Software without restriction, including without limitation the rights to
19
+ use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
20
+ of the Software, and to permit persons to whom the Software is furnished to do
21
+ so, subject to the following conditions:
22
+
23
+ The above copyright notice and this permission notice shall be included in
24
+ all copies or substantial portions of the Software.
25
+
26
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
27
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
28
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
29
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
30
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
31
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
32
+ SOFTWARE.
33
+ */
34
+
35
+ // CLASS ConstraintEditor
36
+ class ConstraintEditor {
37
+ constructor() {
38
+ this.dialog = document.getElementById('constraint-dlg');
39
+ this.group_size = document.getElementById('constraint-group');
40
+ this.from_name = document.getElementById('constraint-from-name');
41
+ this.to_name = document.getElementById('constraint-to-name');
42
+ this.bl_type = document.getElementById('bl-type');
43
+ this.bl_selectors = document.getElementById('bl-selectors');
44
+ this.soc_direct = document.getElementById('constraint-soc-direct');
45
+ this.soc = document.getElementById('constraint-share-of-cost');
46
+ this.soc_div = document.getElementById('constraint-soc');
47
+ // Make GUI elements responsive
48
+ UI.modals.constraint.dialog.addEventListener('mousemove',
49
+ () => DOCUMENTATION_MANAGER.update(
50
+ CONSTRAINT_EDITOR.edited_constraint, true));
51
+ UI.modals.constraint.cancel.addEventListener('click',
52
+ () => UI.modals.constraint.hide());
53
+ UI.modals.constraint.ok.addEventListener('click',
54
+ () => CONSTRAINT_EDITOR.updateConstraint());
55
+ this.container = document.getElementById('constraint-container');
56
+ this.container.addEventListener('mousemove',
57
+ (event) => CONSTRAINT_EDITOR.mouseMove(event));
58
+ this.container.addEventListener('mousedown',
59
+ () => CONSTRAINT_EDITOR.mouseDown());
60
+ this.container.addEventListener('mouseup',
61
+ () => CONSTRAINT_EDITOR.mouseUp());
62
+ // NOTE: interpret leaving the area as a mouse-up so that dragging ceases
63
+ this.container.addEventListener('mouseleave',
64
+ () => CONSTRAINT_EDITOR.mouseUp());
65
+ this.pos_x_div = document.getElementById('constraint-pos-x');
66
+ this.pos_y_div = document.getElementById('constraint-pos-y');
67
+ this.point_div = document.getElementById('constraint-point');
68
+ this.equation_div = document.getElementById('constraint-equation');
69
+ this.add_point_btn = document.getElementById('add-point-btn');
70
+ this.add_point_btn.addEventListener('click',
71
+ () => CONSTRAINT_EDITOR.addPointToLine());
72
+ this.del_point_btn = document.getElementById('del-point-btn');
73
+ this.del_point_btn.addEventListener('click',
74
+ () => CONSTRAINT_EDITOR.deletePointFromLine());
75
+ this.add_bl_btn = document.getElementById('add-bl-btn');
76
+ this.add_bl_btn.addEventListener('click',
77
+ () => CONSTRAINT_EDITOR.addBoundLine());
78
+ this.bl_type.addEventListener('change',
79
+ () => CONSTRAINT_EDITOR.changeLineType());
80
+ this.bl_selectors.addEventListener('blur',
81
+ () => CONSTRAINT_EDITOR.changeLineSelectors());
82
+ this.soc.addEventListener('blur',
83
+ () => CONSTRAINT_EDITOR.changeShareOfCost());
84
+ this.delete_bl_btn = document.getElementById('del-bl-btn');
85
+ this.delete_bl_btn.addEventListener('click',
86
+ () => CONSTRAINT_EDITOR.deleteBoundLine());
87
+ // The chart is stored as an SVG string
88
+ this.svg = '';
89
+ // Scale, origin X and Y assume a 300x300 px square chart area
90
+ this.scale = 3;
91
+ this.oX = 25;
92
+ this.oY = 315;
93
+ // 0 => silver, LE => orange/red, GE => cyan/blue, EQ => purple
94
+ this.line_color = ['#a0a0a0', '#c04000', '#0040c0', '#9000a0'];
95
+ // Use brighter shades if selected (darker for gray)
96
+ this.selected_color = ['#808080', '#ff8040', '#00ffff', '#a800ff'];
97
+ // The selected bound line object (NULL => no line selected)
98
+ this.selected = null;
99
+ // Cursor position in chart coordinates (100 x 100 grid)
100
+ this.pos_x = 0;
101
+ this.pos_y = 0;
102
+ // `on_line`: the first bound line object detected under the cursor
103
+ this.on_line = null;
104
+ // `on_point`: index of point under the cursor
105
+ this.on_point = -1;
106
+ this.dragged_point = -1;
107
+ this.selected_point = -1;
108
+ this.cursor = 'default';
109
+ // Properties for tracking which constraint is being edited.
110
+ this.edited_constraint = null;
111
+ this.from_node = null;
112
+ this.to_node = null;
113
+ // The constraint object being edited (either a new instance, or a
114
+ // copy of edited_constraint).
115
+ this.constraint = null;
116
+ // List of constraints when multiple constraints are edited.
117
+ this.group = [];
118
+ // NOTE: All edits will be ignored unless the modeler clicks OK.
119
+ }
120
+
121
+ mouseMove(e) {
122
+ // The onMouseMove response of the constraint editor's graph area
123
+ // Calculate cursor point without restricting it to 100x100 grid
124
+ const
125
+ rect = this.container.getBoundingClientRect(),
126
+ top = rect.top + window.scrollY + document.body.scrollTop,
127
+ left = rect.left + window.scrollX + document.body.scrollLeft,
128
+ x = Math.floor(e.clientX - left - this.oX) / this.scale,
129
+ y = 100 - Math.floor(e.clientY - top - (this.oY - 100*this.scale)) / this.scale;
130
+ // Limit X and Y so that they will always display between 0 and 100
131
+ this.pos_x = Math.min(100, Math.max(0, x));
132
+ this.pos_y = Math.min(100, Math.max(0, y));
133
+ this.updateStatus();
134
+ if(this.dragged_point >= 0) {
135
+ this.movePoint();
136
+ } else {
137
+ this.checkLines();
138
+ }
139
+ }
140
+
141
+ mouseDown() {
142
+ // The onMouseDown response of the constraint editor's graph area
143
+ if(this.adding_point) {
144
+ this.doAddPointToLine();
145
+ } else if(this.on_line) {
146
+ this.selectBoundLine(this.on_line);
147
+ this.dragged_point = this.on_point;
148
+ this.selected_point = this.on_point;
149
+ } else {
150
+ this.selected = null;
151
+ this.dragged_point = -1;
152
+ this.selected_point = -1;
153
+ }
154
+ this.draw();
155
+ }
156
+
157
+ mouseUp() {
158
+ // The onMouseUp response of the constraint editor's graph area
159
+ this.dragged_point = -1;
160
+ this.container.style.cursor = this.cursor;
161
+ this.updateStatus();
162
+ }
163
+
164
+ updateCursor() {
165
+ // Updates cursor shape in accordance with current state
166
+ if(this.dragged_point >= 0 || this.on_point >= 0) {
167
+ this.cursor = 'move';
168
+ } else if(this.adding_point) {
169
+ if(this.pos_x === 0 || this.pos_x === 100) {
170
+ this.cursor = 'not-allowed';
171
+ } else {
172
+ this.cursor = 'crosshair';
173
+ }
174
+ } else if(this.on_line) {
175
+ this.cursor = 'pointer';
176
+ } else {
177
+ this.cursor = 'default';
178
+ }
179
+ this.container.style.cursor = this.cursor;
180
+ }
181
+
182
+ arrowKey(k) {
183
+ if(this.selected && this.selected_point >= 0) {
184
+ const
185
+ i = this.selected_point,
186
+ pts = this.selected.points,
187
+ li = pts.length - 1,
188
+ p = pts[this.selected_point],
189
+ minx = (i === 0 ? 0 : (i === li ? 100 : pts[i - 1][0])),
190
+ maxx = (i === 0 ? 0 : (i === li ? 100 : pts[i + 1][0]));
191
+ 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;
195
+ } 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;
199
+ }
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
+ this.draw();
204
+ this.updateEquation();
205
+ }
206
+ }
207
+
208
+ point(x, y) {
209
+ // Returns a string denoting the point (x, y) in SVG notation, assuming
210
+ // that x and y are mathematical coordinates (y-axis pointing UP) and
211
+ // scaled to the constraint editor chart area, cf. global constants
212
+ // defined for the constraint editor.
213
+ return (this.oX + x * this.scale) + ',' + (this.oY - y * this.scale);
214
+ }
215
+
216
+ circleCenter(x, y) {
217
+ // Similar to cePoint above, but prefixing the coordinates to conform
218
+ // to SVG notation for a circle center
219
+ return `cx="${this.oX + x * this.scale}" cy="${this.oY - y * this.scale}"`;
220
+ }
221
+
222
+ selectBoundLine(l) {
223
+ // Selects bound line `l` and move it to end of list so it will be drawn
224
+ // last and hence on top of all other bound lines (if any)
225
+ this.selected = l;
226
+ const li = this.constraint.bound_lines.indexOf(l);
227
+ if(li < this.constraint.bound_lines.length - 1) {
228
+ this.constraint.bound_lines.splice(li, 1);
229
+ this.constraint.bound_lines.push(l);
230
+ }
231
+ }
232
+
233
+ addBoundLine() {
234
+ // Adds a new lower bound line to the set
235
+ this.selected = this.constraint.addBoundLine();
236
+ this.selected_point = -1;
237
+ this.adding_point = false;
238
+ this.updateStatus();
239
+ this.draw();
240
+ }
241
+
242
+ deleteBoundLine() {
243
+ // Removes selected boundline from the set
244
+ if(this.selected) {
245
+ this.constraint.deleteBoundLine(this.selected);
246
+ this.selected = null;
247
+ this.adding_point = false;
248
+ this.updateStatus();
249
+ this.draw();
250
+ }
251
+ }
252
+
253
+ addPointToLine() {
254
+ // Prepares to add point on next "mouse down" event
255
+ if(this.selected) {
256
+ this.add_point_btn.classList.add('activ');
257
+ this.adding_point = true;
258
+ this.selected_point = -1;
259
+ this.draw();
260
+ }
261
+ }
262
+
263
+ doAddPointToLine() {
264
+ // Actually add point to selected line
265
+ if(!this.selected) return;
266
+ const
267
+ p = [this.pos_x, this.pos_y],
268
+ lp = this.selected.points;
269
+ let i = 0;
270
+ while(i < lp.length && lp[i][0] < p[0]) i++;
271
+ lp.splice(i, 0, p);
272
+ this.selected_point = i;
273
+ this.dragged_point = i;
274
+ this.draw();
275
+ // this.dragging_point = new point index!
276
+ this.add_point_btn.classList.remove('activ');
277
+ this.adding_point = false;
278
+ }
279
+
280
+ deletePointFromLine() {
281
+ // Deletes selected point from selected line (unless first or last point)
282
+ if(this.selected && this.selected_point > 0 &&
283
+ this.selected_point < this.selected.points.length - 1) {
284
+ this.selected.points.splice(this.selected_point, 1);
285
+ this.selected_point = -1;
286
+ this.draw();
287
+ }
288
+ }
289
+
290
+ changeLineType() {
291
+ // Changes type of selected boundline
292
+ if(this.selected) {
293
+ this.selected.type = parseInt(this.bl_type.value);
294
+ this.draw();
295
+ }
296
+ }
297
+
298
+ changeLineSelectors() {
299
+ // Changes experiment run selectors of selected boundline
300
+ if(this.selected) {
301
+ const sel = this.bl_selectors.value.replace(
302
+ /[\;\,]/g, ' ').trim().replace(
303
+ /[^a-zA-Z0-9\+\-\%\_\s]/g, '').split(/\s+/).join(' ');
304
+ this.selected.selectors = sel;
305
+ this.bl_selectors.value = sel;
306
+ this.draw();
307
+ }
308
+ }
309
+
310
+ changeShareOfCost() {
311
+ // Validates input of share-of-cost field
312
+ const soc = UI.validNumericInput('constraint-share-of-cost', 'share of cost');
313
+ if(soc === false) return;
314
+ if(soc < 0 || soc > 100) {
315
+ this.soc.focus();
316
+ UI.warn('Share of cost can range from 0% to 100%');
317
+ return;
318
+ }
319
+ // NOTE: share of cost is input as a percentage, but stored as a floating
320
+ // point value between 0 and 1
321
+ this.constraint.share_of_cost = soc / 100;
322
+ }
323
+
324
+ checkLines() {
325
+ // Checks whether cursor is on a bound line and updates the constraint
326
+ // editor status accordingly
327
+ this.on_line = null;
328
+ this.on_point = -1;
329
+ this.seg_points = null;
330
+ // Iterate over all lower bound lines (start with last one added)
331
+ for(let i = this.constraint.bound_lines.length - 1;
332
+ i >= 0 && !this.on_line; i--) {
333
+ const l = this.constraint.bound_lines[i];
334
+ for(let j = 0; j < l.points.length; j++) {
335
+ const
336
+ p = l.points[j],
337
+ dsq = Math.pow(p[0] - this.pos_x, 2) + Math.pow(p[1] - this.pos_y, 2);
338
+ if(dsq < 3) {
339
+ this.on_point = j;
340
+ this.on_line = l;
341
+ this.seg_points = (j > 0 ? [j - 1, j] : [j, j + 1]);
342
+ break;
343
+ } else if(j > 0) {
344
+ this.seg_points = [j - 1, j];
345
+ const pp = l.points[j - 1];
346
+ if(this.pos_x > pp[0] - 1 && this.pos_x < p[0] + 1 &&
347
+ ((this.pos_y > pp[1] - 1 && this.pos_y < p[1] + 1) ||
348
+ (this.pos_y < pp[1] + 1 && this.pos_y > p[1] + 1))) {
349
+ // Cursor lies within rectangle around line segment
350
+ const
351
+ dx = p[0] - pp[0],
352
+ dy = p[1] - pp[1];
353
+ if(Math.abs(dx) < 1 || Math.abs(dy) < 1) {
354
+ // Special case: (near) vertical or (near) horizontal line
355
+ this.on_line = l;
356
+ break;
357
+ } else {
358
+ const
359
+ dpx = this.pos_x - pp[0],
360
+ dpy = this.pos_y - pp[1],
361
+ dxol = Math.abs(pp[0] + dpy * dx / dy - this.pos_x),
362
+ dyol = Math.abs(pp[1] + dpx * dy / dx - this.pos_y);
363
+ if (Math.min(dxol, dyol) < 1) {
364
+ this.on_line = l;
365
+ break;
366
+ }
367
+ }
368
+ }
369
+ }
370
+ }
371
+ }
372
+ this.updateEquation();
373
+ this.updateCursor();
374
+ }
375
+
376
+ updateEquation() {
377
+ var segeq = '';
378
+ if(this.on_line && this.seg_points) {
379
+ const
380
+ p1 = this.on_line.points[this.seg_points[0]],
381
+ p2 = this.on_line.points[this.seg_points[1]],
382
+ dx = p2[0] - p1[0],
383
+ dy = p2[1] - p1[1];
384
+ if(dx === 0) {
385
+ segeq = 'X = ' + p1[0].toPrecision(3);
386
+ } else if(dy === 0) {
387
+ segeq = 'Y = ' + p1[1].toPrecision(3);
388
+ } else {
389
+ const
390
+ slope = (dy === dx ? '' :
391
+ (dy === -dx ? '-' : (dy / dx).toPrecision(3) + ' ')),
392
+ y0 = p2[1] - p2[0] * dy / dx;
393
+ segeq = `Y = ${slope}X` + (y0 === 0 ? '' :
394
+ (y0 < 0 ? ' - ' : ' + ') + Math.abs(y0).toPrecision(3));
395
+ }
396
+ }
397
+ this.equation_div.innerHTML = segeq;
398
+ }
399
+
400
+ movePoint() {
401
+ // Moves the dragged point of the selected bound line
402
+ // Use l as shorthand for the selected line
403
+ const
404
+ l = this.selected,
405
+ pi = this.dragged_point,
406
+ lpi = l.points.length - 1;
407
+ // Check -- just in case
408
+ if(!l || pi < 0 || pi > lpi) return;
409
+ let p = l.points[pi],
410
+ px = p[0],
411
+ py = p[1],
412
+ minx = (pi === 0 ? 0 : (pi === lpi ? 100 : l.points[pi - 1][0])),
413
+ 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));
416
+ // No action needed unless point has been moved
417
+ if(newx !== px || newy !== py) {
418
+ p[0] = newx;
419
+ p[1] = newy;
420
+ this.draw();
421
+ this.updateEquation();
422
+ }
423
+ }
424
+
425
+ updateStatus() {
426
+ // Displays cursor position as X and Y (in chart coordinates), and updates
427
+ // controls
428
+ this.pos_x_div.innerHTML = 'X = ' + this.pos_x.toPrecision(3);
429
+ this.pos_y_div.innerHTML = 'Y = ' + this.pos_y.toPrecision(3);
430
+ const blbtns = 'add-point del-bl';
431
+ if(this.selected) {
432
+ if(this.selected_point >= 0) {
433
+ 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 = '';
438
+ }
439
+ // Check whether selected point is an end point
440
+ const ep = this.selected_point === 0 ||
441
+ this.selected_point === this.selected.points.length - 1;
442
+ // If so, do not allow deletion
443
+ UI.enableButtons(blbtns + (ep ? '' : ' del-point'));
444
+ if(this.adding_point) this.add_point_btn.classList.add('activ');
445
+ this.bl_type.value = this.selected.type;
446
+ this.bl_type.style.color = 'black';
447
+ this.bl_type.disabled = false;
448
+ this.bl_selectors.value = this.selected.selectors;
449
+ this.bl_selectors.style.backgroundColor = 'white';
450
+ this.bl_selectors.disabled = false;
451
+ } else {
452
+ UI.disableButtons(blbtns + ' del-point');
453
+ this.bl_type.value = VM.EQ;
454
+ this.bl_type.style.color = 'silver';
455
+ this.bl_type.disabled = true;
456
+ this.bl_selectors.value = '';
457
+ this.bl_selectors.style.backgroundColor = 'inherit';
458
+ this.bl_selectors.disabled = true;
459
+ }
460
+ }
461
+
462
+ addSVG(lines) {
463
+ // Appends a string or an array of strings to the SVG
464
+ this.svg += (lines instanceof Array ? lines.join('') : lines);
465
+ }
466
+
467
+ draw() {
468
+ // Draws the chart with bound lines and infeasible regions
469
+ // NOTE: since this graph is relatively small, SVG is added as an XML string
470
+ this.svg = ['<svg height="330" version="1.1" width="340"',
471
+ ' xmlns="http://www.w3.org/2000/svg"',
472
+ ' xmlns:xlink="http://www.w3.org/1999/xlink"',
473
+ ' style="overflow: hidden; position: relative;">',
474
+ '<defs>',
475
+ // Fill patterns for infeasible areas differ per bound line type;
476
+ // diagonal for LE and GE, horizontal for EQ, and when selected
477
+ // in the constraint editor, different colors as well (orange,
478
+ // blue or purple)
479
+ '<pattern id="stroke1" x="2" y="2" width="4" height="4"',
480
+ ' patternUnits="userSpaceOnUse"><path d="M0,0L4,4"',
481
+ ' style="stroke: #400000; stroke-width: 0.5"></pattern>',
482
+ '<pattern id="stroke1s" x="2" y="2" width="4" height="4"',
483
+ ' patternUnits="userSpaceOnUse"><path d="M0,0L4,4"',
484
+ ' style="stroke: #f04000; stroke-width: 0.5"></pattern>',
485
+ '<pattern id="stroke2" x="2" y="2" width="4" height="4"',
486
+ ' patternUnits="userSpaceOnUse"><path d="M4,0L0,4"',
487
+ ' style="stroke: #000040; stroke-width: 0.5"></pattern>',
488
+ '<pattern id="stroke2s" x="2" y="2" width="4" height="4"',
489
+ ' patternUnits="userSpaceOnUse"><path d="M4,0L0,4"',
490
+ ' style="stroke: #00a0ff; stroke-width: 0.5"></pattern>',
491
+ '<pattern id="stroke3" x="2" y="2" width="4" height="4"',
492
+ ' patternUnits="userSpaceOnUse"><path d="M0,2L4,2"',
493
+ ' style="stroke: #180030; stroke-width: 0.5"></pattern>',
494
+ '<pattern id="stroke3s" x="2" y="2" width="4" height="4"',
495
+ ' patternUnits="userSpaceOnUse"><path d="M0,2L4,2"',
496
+ ' style="stroke: #c060ff; stroke-width: 0.5"></pattern>',
497
+ '</defs>'].join('');
498
+ // Draw the grid
499
+ this.drawGrid();
500
+ // Use c as shorthand for this.constraint
501
+ const c = this.constraint;
502
+ // Add the SVG for lower and upper bounds
503
+ for(let i = 0; i < c.bound_lines.length; i++) {
504
+ const bl = c.bound_lines[i];
505
+ this.drawContour(bl);
506
+ this.drawLine(bl);
507
+ }
508
+ this.highlightSelectedPoint();
509
+ // Add the SVG disclaimer
510
+ this.addSVG('Sorry, your browser does not support inline SVG.</svg>');
511
+ // Insert the SVG into the designated DIV
512
+ this.container.innerHTML = this.svg;
513
+ this.updateStatus();
514
+ }
515
+
516
+ drawGrid() {
517
+ // Draw the grid area
518
+ const hw = 100 * this.scale;
519
+ this.addSVG(['<rect x="', this.oX, '" y="', this.oY - hw,
520
+ '" width="', hw, '" height="', hw,
521
+ '" fill="white" stroke="gray" stroke-width="1.5"></rect>']);
522
+ // NOTES:
523
+ // (1) font name fixed to Arial on purpose to preserve the look of
524
+ // this dialog
525
+ // (2) d = distance between grid lines, l = left, r = right, t = top,
526
+ // b = bottom, tx = end of right-aligned numbers along vertical axis,
527
+ // ty = middle for numbers along the horizontal axis
528
+ const d = 10 * this.scale, l = this.oX + 1, r = this.oX + hw - 1,
529
+ t = this.oY - hw + 1, b = this.oY - 1,
530
+ tx = this.oX - 3, ty = this.oY + 12;
531
+ // Draw the dashed grid lines and their numbers 10 - 90 along both axes
532
+ for(let i = 1; i < 10; i++) {
533
+ const x = i*d + this.oX, y = this.oY - i*d, n = 10*i;
534
+ this.addSVG(['<path fill="none" stroke="silver" d="M',
535
+ x, ',', t, 'L', x, ',', b,
536
+ '" stroke-width="0.5" stroke-dasharray="5,2.5"></path>',
537
+ '<path fill="none" stroke="silver" d="M', l, ',', y, 'L', r, ',', y,
538
+ '" stroke-width="0.5" stroke-dasharray="5,2.5"></path>',
539
+ '<text x="', x, '" y="', ty,
540
+ '" text-anchor="middle" font-family="Arial"',
541
+ ' font-size="10px" stroke="none" fill="black">', n, '</text>',
542
+ '<text x="', tx, '" y="', y + 4,
543
+ '" text-anchor="end" font-family="Arial"',
544
+ ' font-size="10px" stroke="none" fill="black">', n, '</text>']);
545
+ }
546
+ // also draw scale extremes (0 and 2x 100)
547
+ this.addSVG(['<text x="', tx, '" y="', ty, '" text-anchor="end"',
548
+ ' font-family="Arial" font-size="10px" stroke="none" fill="black">',
549
+ '0</text><text x="', r,'" y="', ty, '" text-anchor="middle"',
550
+ ' font-family="Arial" font-size="10px" stroke="none" fill="black">',
551
+ '100</text><text x="', tx, '" y="', t, '" text-anchor="end"',
552
+ ' font-family="Arial" font-size="10px" stroke="none" fill="black">',
553
+ '100</text>']);
554
+ }
555
+
556
+ drawContour(l) {
557
+ // Draws infeasible area for bound line `l`
558
+ let cp;
559
+ if(l.type === VM.EQ) {
560
+ // Whole area is infeasible except for the bound line itself
561
+ cp = ['M', this.point(0, 0), 'L', this.point(100 ,0), 'L',
562
+ this.point(100, 100), 'L', this.point(0, 100), 'z'].join('');
563
+ } else {
564
+ const base_y = (l.type === VM.GE ? 0 : 100);
565
+ cp = 'M' + this.point(0, base_y);
566
+ for(let i = 0; i < l.points.length; i++) {
567
+ const p = l.points[i];
568
+ cp += `L${this.point(p[0], p[1])}`;
569
+ }
570
+ cp += 'L' + this.point(100, base_y) + 'z';
571
+ }
572
+ // Save the contour for rapid display of thumbnails
573
+ l.contour_path = cp;
574
+ // NOTE: the selected bound lines have their infeasible area filled
575
+ // with a *colored* line pattern
576
+ const sel = l === this.selected;
577
+ this.addSVG(['<path fill="url(#stroke', l.type,
578
+ (sel ? 's' : ''), ')" d="', cp, '" stroke="none" opacity="',
579
+ (sel ? 1 : 0.4), '"></path>']);
580
+ }
581
+
582
+ drawLine(l) {
583
+ let color,
584
+ width,
585
+ pp = [],
586
+ dots = '';
587
+ if(l == this.selected) {
588
+ width = 3;
589
+ color = this.selected_color[l.type];
590
+ } else {
591
+ width = 1.5;
592
+ color = this.line_color[l.type];
593
+ }
594
+ const cfs = `fill="${color}" stroke="${color}" stroke-width="${width}"`;
595
+ for(let i = 0; i < l.points.length; i++) {
596
+ const
597
+ px = l.points[i][0],
598
+ py = l.points[i][1];
599
+ pp.push(this.point(px, py));
600
+ dots += `<circle ${this.circleCenter(px, py)} r="3" ${cfs}></circle>`;
601
+ }
602
+ const cp = 'M' + pp.join('L');
603
+ // For EQ bound lines, the line path is the contour; this will be
604
+ // drawn in miniature black against a silver background
605
+ if(l.type === VM.EQ) l.contour_path = cp;
606
+ this.addSVG(['<path fill="none" stroke="', color, '" d="', cp,
607
+ '" stroke-width="', width, '"></path>', dots]);
608
+ }
609
+
610
+ highlightSelectedPoint() {
611
+ if(this.selected && this.selected_point >= 0) {
612
+ const p = this.selected.points[this.selected_point];
613
+ this.addSVG(['<circle ', this.circleCenter(p[0], p[1]),
614
+ ' r="4.5" fill="none" stroke="black" stroke-width="2px"></circle>']);
615
+ }
616
+ }
617
+
618
+ showDialog(group=[]) {
619
+ this.from_node = MODEL.objectByName(this.from_name.innerHTML);
620
+ this.to_node = MODEL.objectByName(this.to_name.innerHTML);
621
+ // Double-check that these nodes exist
622
+ if(!(this.from_node && this.to_node)) {
623
+ throw 'ERROR: Unknown constraint node(s)';
624
+ }
625
+ // See if existing constraint is edited
626
+ this.edited_constraint = this.from_node.doesConstrain(this.to_node);
627
+ if(this.edited_constraint) {
628
+ // Make a working copy, as the constraint must be changed only when
629
+ // dialog OK is clicked. NOTE: use the GET property "copy", NOT the
630
+ // Javascript function copy() !!
631
+ this.constraint = this.edited_constraint.copy;
632
+ this.group = group;
633
+ this.group_size.innerText = (group.length > 0 ?
634
+ `(N=${md.group.length})`: '');
635
+ } else {
636
+ // Create a new constraint
637
+ this.constraint = new Constraint(this.from_node, this.to_node);
638
+ }
639
+ this.selected = null;
640
+ // Draw the graph
641
+ this.draw();
642
+ // Allow modeler to omit slack variables for this constraint
643
+ // NOTE: this could be expanded to apply to the selected BL only
644
+ UI.setBox('constraint-no-slack', this.constraint.no_slack);
645
+ // NOTE: share of cost can only be transferred between two processes
646
+ // @@TO DO: CHECK WHETHER THIS LIMITATION IS VALID -- for now, allow both
647
+ if(true||this.from_node instanceof Process && this.from_node instanceof Process) {
648
+ this.soc_direct.value = this.constraint.soc_direction;
649
+ // NOTE: share of cost is input as a percentage
650
+ this.soc.value = VM.sig4Dig(100 * this.constraint.share_of_cost);
651
+ this.soc_div.style.display = 'block';
652
+ } else {
653
+ this.soc_direct.value = VM.SOC_X_Y;
654
+ this.soc.value = '0';
655
+ this.soc_div.style.display = 'none';
656
+ }
657
+ UI.modals.constraint.show('soc-direct');
658
+ }
659
+
660
+ updateConstraint() {
661
+ // Updates the edited constraint, or adds a new constraint to the model
662
+ // TO DO: prepare for undo
663
+ if(this.edited_constraint === null) {
664
+ this.edited_constraint = MODEL.addConstraint(this.from_node, this.to_node);
665
+ }
666
+ // Copy properties of the "working copy" to the edited/new constraint
667
+ // except for the comments (as these cannot be added/modified while the
668
+ // constraint editor is visible)
669
+ const cmnts = this.edited_constraint.comments;
670
+ this.edited_constraint.copyPropertiesFrom(this.constraint);
671
+ this.edited_constraint.comments = cmnts;
672
+ // Set the "no slack" property based on the checkbox state
673
+ this.edited_constraint.no_slack = UI.boxChecked('constraint-no-slack');
674
+ // Set the SoC direction property based on the selected option
675
+ this.edited_constraint.soc_direction = parseInt(this.soc_direct.value);
676
+ UI.paper.drawConstraint(this.edited_constraint);
677
+ UI.modals.constraint.hide();
678
+ }
679
+
680
+ } // END of class ConstraintEditor
681
+