linny-r 1.4.3 → 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.
- package/README.md +102 -48
- package/package.json +1 -1
- package/server.js +31 -6
- package/static/images/check-off-not-same-changed.png +0 -0
- package/static/images/check-off-not-same-not-changed.png +0 -0
- package/static/images/check-off-same-changed.png +0 -0
- package/static/images/check-off-same-not-changed.png +0 -0
- package/static/images/check-on-not-same-changed.png +0 -0
- package/static/images/check-on-not-same-not-changed.png +0 -0
- package/static/images/check-on-same-changed.png +0 -0
- package/static/images/check-on-same-not-changed.png +0 -0
- package/static/images/eq-not-same-changed.png +0 -0
- package/static/images/eq-not-same-not-changed.png +0 -0
- package/static/images/eq-same-changed.png +0 -0
- package/static/images/eq-same-not-changed.png +0 -0
- package/static/images/ne-not-same-changed.png +0 -0
- package/static/images/ne-not-same-not-changed.png +0 -0
- package/static/images/ne-same-changed.png +0 -0
- package/static/images/ne-same-not-changed.png +0 -0
- package/static/images/sort-asc-lead.png +0 -0
- package/static/images/sort-asc.png +0 -0
- package/static/images/sort-desc-lead.png +0 -0
- package/static/images/sort-desc.png +0 -0
- package/static/images/sort-not.png +0 -0
- package/static/index.html +51 -35
- package/static/linny-r.css +167 -53
- package/static/scripts/linny-r-gui-actor-manager.js +340 -0
- package/static/scripts/linny-r-gui-chart-manager.js +944 -0
- package/static/scripts/linny-r-gui-constraint-editor.js +681 -0
- package/static/scripts/linny-r-gui-controller.js +4005 -0
- package/static/scripts/linny-r-gui-dataset-manager.js +1176 -0
- package/static/scripts/linny-r-gui-documentation-manager.js +739 -0
- package/static/scripts/linny-r-gui-equation-manager.js +307 -0
- package/static/scripts/linny-r-gui-experiment-manager.js +1944 -0
- package/static/scripts/linny-r-gui-expression-editor.js +449 -0
- package/static/scripts/linny-r-gui-file-manager.js +392 -0
- package/static/scripts/linny-r-gui-finder.js +727 -0
- package/static/scripts/linny-r-gui-model-autosaver.js +230 -0
- package/static/scripts/linny-r-gui-monitor.js +448 -0
- package/static/scripts/linny-r-gui-paper.js +2789 -0
- package/static/scripts/linny-r-gui-receiver.js +323 -0
- package/static/scripts/linny-r-gui-repository-browser.js +819 -0
- package/static/scripts/linny-r-gui-scale-unit-manager.js +244 -0
- package/static/scripts/linny-r-gui-sensitivity-analysis.js +778 -0
- package/static/scripts/linny-r-gui-undo-redo.js +560 -0
- package/static/scripts/linny-r-model.js +24 -11
- package/static/scripts/linny-r-utils.js +10 -0
- package/static/scripts/linny-r-vm.js +21 -12
- package/static/scripts/linny-r-gui.js +0 -16908
@@ -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
|
+
|