linny-r 2.0.8 → 2.0.10
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 +3 -40
- package/package.json +6 -2
- package/server.js +19 -157
- package/static/images/solve-not-same-changed.png +0 -0
- package/static/images/solve-not-same-not-changed.png +0 -0
- package/static/images/solve-same-changed.png +0 -0
- package/static/images/solve-same-not-changed.png +0 -0
- package/static/index.html +137 -20
- package/static/linny-r.css +260 -23
- package/static/scripts/iro.min.js +7 -7
- package/static/scripts/linny-r-ctrl.js +126 -85
- package/static/scripts/linny-r-gui-actor-manager.js +23 -33
- package/static/scripts/linny-r-gui-chart-manager.js +56 -53
- package/static/scripts/linny-r-gui-constraint-editor.js +10 -14
- package/static/scripts/linny-r-gui-controller.js +644 -260
- package/static/scripts/linny-r-gui-dataset-manager.js +64 -66
- package/static/scripts/linny-r-gui-documentation-manager.js +11 -17
- package/static/scripts/linny-r-gui-equation-manager.js +22 -22
- package/static/scripts/linny-r-gui-experiment-manager.js +124 -141
- package/static/scripts/linny-r-gui-expression-editor.js +26 -12
- package/static/scripts/linny-r-gui-file-manager.js +42 -48
- package/static/scripts/linny-r-gui-finder.js +294 -55
- package/static/scripts/linny-r-gui-model-autosaver.js +2 -4
- package/static/scripts/linny-r-gui-monitor.js +35 -41
- package/static/scripts/linny-r-gui-paper.js +42 -70
- package/static/scripts/linny-r-gui-power-grid-manager.js +31 -34
- package/static/scripts/linny-r-gui-receiver.js +1 -2
- package/static/scripts/linny-r-gui-repository-browser.js +44 -46
- package/static/scripts/linny-r-gui-scale-unit-manager.js +32 -32
- package/static/scripts/linny-r-gui-sensitivity-analysis.js +61 -67
- package/static/scripts/linny-r-gui-undo-redo.js +94 -95
- package/static/scripts/linny-r-milp.js +20 -24
- package/static/scripts/linny-r-model.js +1932 -2274
- package/static/scripts/linny-r-utils.js +27 -27
- package/static/scripts/linny-r-vm.js +900 -998
- package/static/show-png.html +0 -113
@@ -75,6 +75,11 @@ class ModalDialog {
|
|
75
75
|
// Make this modal dialog invisible.
|
76
76
|
this.modal.style.display = 'none';
|
77
77
|
}
|
78
|
+
|
79
|
+
get showing() {
|
80
|
+
// Return TRUE iff this modal dialog is visible.
|
81
|
+
return this.modal.style.display === 'block';
|
82
|
+
}
|
78
83
|
|
79
84
|
} // END of class ModalDialog
|
80
85
|
|
@@ -85,15 +90,19 @@ class ModalDialog {
|
|
85
90
|
// such that fields[name] is the entity property name that corresponds
|
86
91
|
// with the DOM input element for that property. For example, for the
|
87
92
|
// process group properties dialog, fields['LB'] = 'lower_bound' to
|
88
|
-
// indicate that the DOM element having id="
|
93
|
+
// indicate that the DOM element having id="process-LB" corresponds to
|
89
94
|
// the property `p.lower_bound` of process `p`.
|
90
95
|
class GroupPropertiesDialog extends ModalDialog {
|
91
96
|
constructor(id, fields) {
|
92
97
|
super(id);
|
98
|
+
// `fields` is the object that relates HTML elements to properties.
|
93
99
|
this.fields = fields;
|
94
100
|
// `group` holds the entities (all of the same type) that should be
|
95
101
|
// updated when the OK-button of the dialog is clicked.
|
96
102
|
this.group = [];
|
103
|
+
// `selected_ds` is the dataset that was selected in the Finder when
|
104
|
+
// opening this dialog, or the first dataset in the group list.
|
105
|
+
this.selected_ds = null;
|
97
106
|
// `initial_values` is a "dictionary" with (field name, value) entries
|
98
107
|
// that hold the initial values of the group-editable properties.
|
99
108
|
this.initial = {};
|
@@ -123,28 +132,58 @@ class GroupPropertiesDialog extends ModalDialog {
|
|
123
132
|
e.addEventListener('keydown', fnc);
|
124
133
|
}
|
125
134
|
}
|
135
|
+
const spe = this.element('prefix');
|
136
|
+
if(spe) {
|
137
|
+
spe.addEventListener('keydown', fnc);
|
138
|
+
document.getElementById('dsg-add-modif-btn').addEventListener(
|
139
|
+
'click', () => UI.modals.datasetgroup.promptForSelector('Add'));
|
140
|
+
document.getElementById('dsg-rename-modif-btn').addEventListener(
|
141
|
+
'click', () => UI.modals.datasetgroup.promptForSelector('Rename'));
|
142
|
+
document.getElementById('dsg-edit-modif-btn').addEventListener(
|
143
|
+
'click', () => UI.modals.datasetgroup.editExpression());
|
144
|
+
document.getElementById('dsg-delete-modif-btn').addEventListener(
|
145
|
+
'click', () => UI.modals.datasetgroup.deleteModifier());
|
146
|
+
this.selector_modal = new ModalDialog('group-selector');
|
147
|
+
this.selector_modal.ok.addEventListener(
|
148
|
+
'click', () => UI.modals.datasetgroup.selectorAction());
|
149
|
+
this.selector_modal.cancel.addEventListener(
|
150
|
+
'click', () => UI.modals.datasetgroup.selector_modal.hide());
|
151
|
+
}
|
126
152
|
}
|
127
153
|
|
128
154
|
resetFields() {
|
129
155
|
// Remove all class names from fields that relate to their "same"
|
130
156
|
// and "changed status, and reset group-related properties.
|
131
|
-
|
132
|
-
|
133
|
-
|
134
|
-
cl.
|
157
|
+
function stripClassList(e) {
|
158
|
+
if(e) {
|
159
|
+
const cl = e.classList;
|
160
|
+
while(cl.length > 0 && cl.item(cl.length - 1).indexOf('same-') >= 0) {
|
161
|
+
cl.remove(cl.item(cl.length - 1));
|
162
|
+
}
|
135
163
|
}
|
136
164
|
}
|
165
|
+
for(let name in this.fields) if(this.initial.hasOwnProperty(name)) {
|
166
|
+
stripClassList(this.element(name));
|
167
|
+
}
|
168
|
+
stripClassList(this.element('prefix'));
|
137
169
|
this.element('group').innerText = '';
|
138
|
-
|
139
|
-
|
140
|
-
|
141
|
-
|
142
|
-
e = this.element('
|
143
|
-
if(e) e.
|
170
|
+
for(const id of ['name', 'actor', 'cluster']) {
|
171
|
+
const e = this.element(id);
|
172
|
+
if(e) e.disabled = false;
|
173
|
+
}
|
174
|
+
const e = this.element('io');
|
175
|
+
if(e) e.style.display = 'block';
|
144
176
|
this.group.length = 0;
|
145
177
|
this.initial = {};
|
146
178
|
this.same = {};
|
147
179
|
this.changed = {};
|
180
|
+
this.shared_prefix = '';
|
181
|
+
this.selectors = {};
|
182
|
+
this.selected_selector = '';
|
183
|
+
this.default_selectors = [];
|
184
|
+
this.new_defsel = false;
|
185
|
+
this.same_defsel = true;
|
186
|
+
this.last_time_clicked = 0;
|
148
187
|
}
|
149
188
|
|
150
189
|
setFields(obj) {
|
@@ -196,23 +235,212 @@ class GroupPropertiesDialog extends ModalDialog {
|
|
196
235
|
this.same[name] = same;
|
197
236
|
}
|
198
237
|
}
|
238
|
+
// For the dataset group dialog, more fields must be determined.
|
239
|
+
if(obj instanceof Dataset) {
|
240
|
+
// Determine the longest prefix shared by ALL datasets in the group.
|
241
|
+
this.shared_prefix = UI.sharedPrefix(obj.name, obj.name);
|
242
|
+
for(const ds of this.group) {
|
243
|
+
const sp = UI.sharedPrefix(obj.name, ds.name);
|
244
|
+
if(sp && this.shared_prefix.startsWith(sp)) {
|
245
|
+
this.shared_prefix = sp;
|
246
|
+
} else if(!sp.startsWith(this.shared_prefix)) {
|
247
|
+
this.shared_prefix = '';
|
248
|
+
break;
|
249
|
+
}
|
250
|
+
}
|
251
|
+
this.element('prefix').value = this.shared_prefix;
|
252
|
+
// Determine the set of all dataset modifier selectors while counting
|
253
|
+
// the number of occurrences of each selector and checking whether
|
254
|
+
// the modifier expressions are identical.
|
255
|
+
// NOTE: Here, too, take `obj` as the reference object.
|
256
|
+
this.selectors = {};
|
257
|
+
this.selected_selector = '';
|
258
|
+
this.default_selectors = [];
|
259
|
+
this.new_defsel = false;
|
260
|
+
this.same_defsel = true;
|
261
|
+
if(obj.default_selector) {
|
262
|
+
this.default_selectors.push(UI.nameToID(obj.default_selector));
|
263
|
+
}
|
264
|
+
for(const k of Object.keys(obj.modifiers)) {
|
265
|
+
const dsm = obj.modifiers[k];
|
266
|
+
this.selectors[k] = {
|
267
|
+
count: 1,
|
268
|
+
sel: dsm.selector,
|
269
|
+
expr: dsm.expression.text,
|
270
|
+
same_x: true,
|
271
|
+
new_s: false,
|
272
|
+
new_x: false,
|
273
|
+
deleted: false
|
274
|
+
};
|
275
|
+
}
|
276
|
+
// Then iterate over all datasets, excluding `obj`.
|
277
|
+
for(const ds of this.group) if(ds !== obj) {
|
278
|
+
const defsel = UI.nameToID(ds.default_selector);
|
279
|
+
if(this.default_selectors.indexOf(defsel) < 0) this.same_defsel = false;
|
280
|
+
if(defsel) addDistinct(defsel, this.default_selectors);
|
281
|
+
for(const k of Object.keys(ds.modifiers)) {
|
282
|
+
const
|
283
|
+
dsm = ds.modifiers[k],
|
284
|
+
s = this.selectors[k];
|
285
|
+
if(s) {
|
286
|
+
s.count++;
|
287
|
+
s.same_x = s.same_x && dsm.expression.text === s.expr;
|
288
|
+
} else {
|
289
|
+
this.selectors[k] = {
|
290
|
+
count: 1,
|
291
|
+
sel: dsm.selector,
|
292
|
+
expr: dsm.expression.text,
|
293
|
+
same_x: true,
|
294
|
+
new_s: false,
|
295
|
+
new_x: false,
|
296
|
+
deleted: false
|
297
|
+
};
|
298
|
+
}
|
299
|
+
}
|
300
|
+
}
|
301
|
+
// Selectors are not "same" when they do not apply to all datasets
|
302
|
+
// in the group.
|
303
|
+
const n = this.group.length;
|
304
|
+
for(const k of Object.keys(this.selectors)) {
|
305
|
+
const s = this.selectors[k];
|
306
|
+
s.same_s = s.count === n;
|
307
|
+
}
|
308
|
+
this.updateModifierList();
|
309
|
+
}
|
310
|
+
}
|
311
|
+
|
312
|
+
updateModifierList() {
|
313
|
+
// Display the modifier set for a dataset group.
|
314
|
+
const
|
315
|
+
trl = [],
|
316
|
+
not = (x) => { return (x === false ? 'not-' : ''); },
|
317
|
+
mdef = (this.new_defsel !== false ? this.new_defsel :
|
318
|
+
(this.default_selectors.length ? this.default_selectors[0] : '')),
|
319
|
+
sdef = not(this.same_defsel),
|
320
|
+
cdef = not(this.new_defsel);
|
321
|
+
for(const k of Object.keys(this.selectors)) {
|
322
|
+
const
|
323
|
+
s = this.selectors[k],
|
324
|
+
ms = (s.new_s === false ? s.sel : s.new_s),
|
325
|
+
mx = (s.new_x === false ? s.expr : s.new_x),
|
326
|
+
wild = (ms.indexOf('*') >= 0 || ms.indexOf('?') >= 0),
|
327
|
+
clk = `" onclick="UI.modals.datasetgroup.selectGroupSelector(event, \'${k}\'`;
|
328
|
+
// Do not display deleted modifiers.
|
329
|
+
if(s.deleted) continue;
|
330
|
+
trl.push(['<tr id="dsgs-', k, '" class="dataset-modif',
|
331
|
+
(k === this.selected_selector ? ' sel-set' : ''),
|
332
|
+
'"><td class="dataset-selector',
|
333
|
+
` ${not(s.same_s)}same-${not(s.new_s)}changed`,
|
334
|
+
(wild ? ' wildcard' : ''),
|
335
|
+
'" title="Shift-click to ', (s.defsel ? 'clear' : 'set as'),
|
336
|
+
' default modifier', clk, ', false);">',
|
337
|
+
(k === mdef ||
|
338
|
+
(this.new_defsel === false && this.default_selectors.indexOf(k) >= 0) ?
|
339
|
+
`<img src="images/solve-${sdef}same-${cdef}changed.png" ` +
|
340
|
+
'style="height: 14px; width: 14px; margin: 0 1px -3px -1px;">' : ''),
|
341
|
+
(wild ? wildcardFormat(ms, true) : ms),
|
342
|
+
'</td><td class="dataset-expression',
|
343
|
+
` ${not(s.same_x)}same-${not(s.new_x)}changed`, clk,
|
344
|
+
', true);">', mx, '</td></tr>'
|
345
|
+
].join(''));
|
346
|
+
}
|
347
|
+
this.element('modif-table').innerHTML = trl.join('');
|
348
|
+
if(this.selected_selector) UI.scrollIntoView(
|
349
|
+
document.getElementById('dsg-' + this.selected_selector));
|
350
|
+
const btns = 'dsg-rename-modif dsg-edit-modif dsg-delete-modif';
|
351
|
+
if(this.selected_selector) {
|
352
|
+
UI.enableButtons(btns);
|
353
|
+
} else {
|
354
|
+
UI.disableButtons(btns);
|
355
|
+
}
|
199
356
|
}
|
200
357
|
|
358
|
+
selectGroupSelector(event, id, x=true) {
|
359
|
+
// Select modifier selector, or when double-clicked, edit its expression when
|
360
|
+
// x = TRUE, or the name of the modifier when x = FALSE.
|
361
|
+
const edit = event.altKey || this.doubleClicked(id);
|
362
|
+
this.selected_selector = id;
|
363
|
+
if(edit) {
|
364
|
+
this.last_time_clicked = 0;
|
365
|
+
if(x) {
|
366
|
+
this.editExpression();
|
367
|
+
} else {
|
368
|
+
this.promptForSelector('Rename');
|
369
|
+
}
|
370
|
+
return;
|
371
|
+
}
|
372
|
+
if(event.shiftKey) {
|
373
|
+
// Toggle new default selector.
|
374
|
+
this.new_defsel = (this.new_defsel === id ? '' : id);
|
375
|
+
}
|
376
|
+
this.updateModifierList();
|
377
|
+
}
|
378
|
+
|
379
|
+
doubleClicked(s) {
|
380
|
+
const
|
381
|
+
now = Date.now(),
|
382
|
+
dt = now - this.last_time_clicked;
|
383
|
+
this.last_time_clicked = now;
|
384
|
+
if(s === this.selected_selector) {
|
385
|
+
// Consider click to be "double" if it occurred less than 300 ms ago.
|
386
|
+
if(dt < 300) {
|
387
|
+
this.last_time_clicked = 0;
|
388
|
+
return true;
|
389
|
+
}
|
390
|
+
}
|
391
|
+
this.clicked_selector = s;
|
392
|
+
return false;
|
393
|
+
}
|
394
|
+
|
395
|
+
enterKey() {
|
396
|
+
// Open "edit" dialog for the selected modifier.
|
397
|
+
const
|
398
|
+
tbl = this.element('modif-table'),
|
399
|
+
srl = tbl.getElementsByClassName('sel-set');
|
400
|
+
if(srl.length > 0) {
|
401
|
+
const r = tbl.rows[srl[0].rowIndex];
|
402
|
+
if(r) {
|
403
|
+
// Emulate a double-click on the second cell to edit the expression.
|
404
|
+
this.last_time_clicked = Date.now();
|
405
|
+
r.cells[1].dispatchEvent(new Event('click'));
|
406
|
+
}
|
407
|
+
}
|
408
|
+
}
|
409
|
+
|
410
|
+
upDownKey(dir) {
|
411
|
+
// Select row above or below the selected one (if possible).
|
412
|
+
const
|
413
|
+
tbl = this.element('modif-table'),
|
414
|
+
srl = tbl.getElementsByClassName('sel-set');
|
415
|
+
if(srl.length > 0) {
|
416
|
+
let r = tbl.rows[srl[0].rowIndex + dir];
|
417
|
+
while(r && r.style.display === 'none') {
|
418
|
+
r = (dir > 0 ? r.nextSibling : r.previousSibling);
|
419
|
+
}
|
420
|
+
if(r) {
|
421
|
+
UI.scrollIntoView(r);
|
422
|
+
// NOTE: Cell, not row, listens for onclick event.
|
423
|
+
r.cells[1].dispatchEvent(new Event('click'));
|
424
|
+
}
|
425
|
+
}
|
426
|
+
}
|
427
|
+
|
201
428
|
show(attr, obj) {
|
202
429
|
// Make dialog visible with same/changed status and disabled name,
|
203
430
|
// actor and cluster fields.
|
204
431
|
// NOTE: Cluster dialog is also used to *add* a new cluster, and in
|
205
|
-
// that case no fields should be set
|
432
|
+
// that case no fields should be set.
|
206
433
|
if(obj) this.setFields(obj);
|
207
434
|
if(obj && this.group.length > 0) {
|
208
435
|
this.element('group').innerText = `(N=${this.group.length})`;
|
209
|
-
// Disable name, actor and cluster fields if they exist.
|
210
|
-
|
211
|
-
|
212
|
-
|
213
|
-
|
214
|
-
|
215
|
-
|
436
|
+
// Disable name, actor, and cluster fields if they exist.
|
437
|
+
for(const id of ['name', 'actor', 'cluster']) {
|
438
|
+
const e = this.element(id);
|
439
|
+
if(e) e.disabled = true;
|
440
|
+
}
|
441
|
+
// Hide io field if it exists.
|
442
|
+
const e = this.element('io');
|
443
|
+
if(e) e.style.display = 'none';
|
216
444
|
// Set the right colors to reflect same and changed status.
|
217
445
|
this.highlightModifiedFields();
|
218
446
|
}
|
@@ -231,6 +459,7 @@ class GroupPropertiesDialog extends ModalDialog {
|
|
231
459
|
// Set the CSS classes of fields so that they reflect their "same"
|
232
460
|
// and "changed" status.
|
233
461
|
if(this.group.length === 0) return;
|
462
|
+
const not = {false: 'not-', true: ''};
|
234
463
|
for(let name in this.initial) if(this.initial.hasOwnProperty(name)) {
|
235
464
|
const
|
236
465
|
iv = this.initial[name],
|
@@ -238,7 +467,6 @@ class GroupPropertiesDialog extends ModalDialog {
|
|
238
467
|
// for which `same[name]` is TRUE iff all entities had identical
|
239
468
|
// values for the property identified by `name` when the dialog
|
240
469
|
// was opened.
|
241
|
-
not = {false: 'not-', true: ''},
|
242
470
|
same = `${not[this.same[name]]}same`,
|
243
471
|
el = this.element(name);
|
244
472
|
let changed = false,
|
@@ -261,10 +489,20 @@ class GroupPropertiesDialog extends ModalDialog {
|
|
261
489
|
// Compute current value as Boolean.
|
262
490
|
const v = (type === 'box' ? state ==='checked' : state === 'eq');
|
263
491
|
changed = (v !== iv);
|
492
|
+
// When array box for dataset group is (un)checked, the time aspects
|
493
|
+
// cover div must be hidden (shown).
|
494
|
+
if(name === 'array') {
|
495
|
+
this.element('no-time-msg').style.display = (v ? 'block' : 'none');
|
496
|
+
}
|
264
497
|
}
|
265
498
|
this.changed[name] = changed;
|
266
499
|
el.className = `${type} ${state} ${same}-${not[changed]}changed`.trim();
|
267
500
|
}
|
501
|
+
const spe = this.element('prefix');
|
502
|
+
if(spe) {
|
503
|
+
const changed = spe.value !== this.shared_prefix;
|
504
|
+
spe.className = `same-${not[changed]}changed`;
|
505
|
+
}
|
268
506
|
}
|
269
507
|
|
270
508
|
validLinkProperty(link, property, value=0) {
|
@@ -298,8 +536,7 @@ class GroupPropertiesDialog extends ModalDialog {
|
|
298
536
|
const
|
299
537
|
propname = this.fields[name],
|
300
538
|
prop = obj[propname];
|
301
|
-
for(
|
302
|
-
const ge = this.group[i];
|
539
|
+
for(const ge of this.group) {
|
303
540
|
// NOTE: For links, special care must be taken.
|
304
541
|
if(!(ge instanceof Link) ||
|
305
542
|
this.validLinkProperty(ge, propname, prop)) {
|
@@ -314,7 +551,105 @@ class GroupPropertiesDialog extends ModalDialog {
|
|
314
551
|
}
|
315
552
|
}
|
316
553
|
}
|
554
|
+
|
555
|
+
promptForSelector(action) {
|
556
|
+
// Open the group selector modal for the specified action.
|
557
|
+
let ms = '',
|
558
|
+
md = this.selector_modal;
|
559
|
+
if(action === 'Rename') {
|
560
|
+
ms = this.selectors[this.selected_selector].sel;
|
561
|
+
}
|
562
|
+
md.element('action').innerText = action;
|
563
|
+
md.element('name').value = ms;
|
564
|
+
md.show('name');
|
565
|
+
}
|
566
|
+
|
567
|
+
selectorAction() {
|
568
|
+
// Perform the specified selector action.
|
569
|
+
const
|
570
|
+
md = this.selector_modal,
|
571
|
+
action = md.element('action').innerText,
|
572
|
+
ne = md.element('name'),
|
573
|
+
ms = MODEL.validSelector(ne.value);
|
574
|
+
if(!ms) {
|
575
|
+
ne.focus();
|
576
|
+
return;
|
577
|
+
}
|
578
|
+
const ok = (action === 'Add' ? this.addSelector(ms) : this.renameSelector(ms));
|
579
|
+
if(ok) {
|
580
|
+
md.hide();
|
581
|
+
this.updateModifierList();
|
582
|
+
}
|
583
|
+
}
|
584
|
+
|
585
|
+
addSelector(ms) {
|
586
|
+
// Create a new selector and adds it to the list.
|
587
|
+
const k = UI.nameToID(ms);
|
588
|
+
if(!this.selectors.hasOwnProperty(k)) {
|
589
|
+
this.selectors[k] = {
|
590
|
+
count: 1,
|
591
|
+
sel: ms,
|
592
|
+
expr: '',
|
593
|
+
same_x: true,
|
594
|
+
new_s: ms,
|
595
|
+
new_x: '',
|
596
|
+
deleted: false
|
597
|
+
};
|
598
|
+
}
|
599
|
+
this.selected_selector = k;
|
600
|
+
return true;
|
601
|
+
}
|
602
|
+
|
603
|
+
renameSelector(ms) {
|
604
|
+
// Record the new name for this selector as property `new_s`.
|
605
|
+
if(this.selected_selector) {
|
606
|
+
const sel = this.selectors[this.selected_selector];
|
607
|
+
// NOTES:
|
608
|
+
// (1) When renaming, the old name is be preserved.
|
609
|
+
// (2) Name changes do not affect the key of the selector.
|
610
|
+
// (3) When the new name is identical to the original, record this
|
611
|
+
// by setting `new_s` to FALSE.
|
612
|
+
sel.new_s = (ms === sel.sel ? false : ms);
|
613
|
+
}
|
614
|
+
return true;
|
615
|
+
}
|
317
616
|
|
617
|
+
editExpression() {
|
618
|
+
// Open the Expression editor for the selected expression.
|
619
|
+
const sel = this.selectors[this.selected_selector];
|
620
|
+
if(sel) {
|
621
|
+
const md = UI.modals.expression;
|
622
|
+
md.element('property').innerHTML = '(dataset group)' +
|
623
|
+
UI.OA_SEPARATOR + sel.sel;
|
624
|
+
md.element('text').value = sel.new_x || sel.expr;
|
625
|
+
document.getElementById('variable-obj').value = 0;
|
626
|
+
X_EDIT.updateVariableBar();
|
627
|
+
X_EDIT.clearStatusBar();
|
628
|
+
X_EDIT.showPrefix(this.shared_prefix);
|
629
|
+
md.show('text');
|
630
|
+
}
|
631
|
+
}
|
632
|
+
|
633
|
+
modifyExpression(x) {
|
634
|
+
// Record the new expression for the selected selector.
|
635
|
+
// NOTE: Expressions are compiled when changes are saved.
|
636
|
+
const sel = this.selectors[this.selected_selector];
|
637
|
+
// NOTE: When the new expression is identical to the original,
|
638
|
+
// record this by setting `new_x` to FALSE.
|
639
|
+
if(sel) sel.new_x = (x === sel.expr ? false : x);
|
640
|
+
this.updateModifierList();
|
641
|
+
}
|
642
|
+
|
643
|
+
deleteModifier() {
|
644
|
+
// Record that the selected modifier should be deleted.
|
645
|
+
const sel = this.selectors[this.selected_selector];
|
646
|
+
if(sel) {
|
647
|
+
sel.deleted = true;
|
648
|
+
this.selected_selector = '';
|
649
|
+
this.updateModifierList();
|
650
|
+
}
|
651
|
+
}
|
652
|
+
|
318
653
|
} // END of class GroupPropertiesDialog
|
319
654
|
|
320
655
|
|
@@ -332,12 +667,9 @@ class GUIController extends Controller {
|
|
332
667
|
['chrome', 'Chrome'],
|
333
668
|
['firefox', 'Firefox'],
|
334
669
|
['safari', 'Safari']];
|
335
|
-
for(
|
336
|
-
|
337
|
-
|
338
|
-
this.browser_name = b[1];
|
339
|
-
break;
|
340
|
-
}
|
670
|
+
for(const b of browsers) if(ua.indexOf(b[0]) >= 0) {
|
671
|
+
this.browser_name = b[1];
|
672
|
+
break;
|
341
673
|
}
|
342
674
|
// Display version number as clickable link just below the Linny-R logo.
|
343
675
|
this.version_number = LINNY_R_VERSION;
|
@@ -363,12 +695,22 @@ class GUIController extends Controller {
|
|
363
695
|
this.mouse_y = 0;
|
364
696
|
this.mouse_down_x = 0;
|
365
697
|
this.mouse_down_y = 0;
|
698
|
+
// When clicking on a node, difference between cursor coordinates
|
699
|
+
// and node coordinates is recorded.
|
366
700
|
this.move_dx = 0;
|
367
701
|
this.move_dy = 0;
|
368
|
-
|
369
|
-
|
702
|
+
// When moving the cursor, the cumulative movement since the last
|
703
|
+
// mouse DOWN or UP event is recorded.
|
704
|
+
this.net_move_x = 0;
|
705
|
+
this.net_move_y = 0;
|
706
|
+
// When mouse button is pressed while some add button is active,
|
707
|
+
// the coordinates of the cursor are recorded.
|
370
708
|
this.add_x = 0;
|
371
709
|
this.add_y = 0;
|
710
|
+
// When mouse button is pressed while no node is under the cursor,
|
711
|
+
// cursor coordinates are recorded as origin of the drag rectangle.
|
712
|
+
this.start_sel_x = -1;
|
713
|
+
this.start_sel_y = -1;
|
372
714
|
this.on_node = null;
|
373
715
|
this.on_arrow = null;
|
374
716
|
this.on_link = null;
|
@@ -393,7 +735,6 @@ class GUIController extends Controller {
|
|
393
735
|
'D': 'dataset',
|
394
736
|
'E': 'equation',
|
395
737
|
'F': 'finder',
|
396
|
-
'G': 'savediagram', // G for "Graph" (as Scalable Vector Graphics image)
|
397
738
|
'H': 'receiver', // activate receiver (H for "Host")
|
398
739
|
'I': 'documentation',
|
399
740
|
'J': 'sensitivity', // J for "Jitter"
|
@@ -421,7 +762,7 @@ class GUIController extends Controller {
|
|
421
762
|
this.edit_btns = ['replace', 'clone', 'paste', 'delete', 'undo', 'redo'];
|
422
763
|
this.model_btns = ['settings', 'save', 'repository', 'actors',
|
423
764
|
'dataset', 'equation', 'chart', 'sensitivity', 'experiment',
|
424
|
-
'
|
765
|
+
'savediagram', 'finder', 'monitor', 'tex', 'solve'];
|
425
766
|
this.other_btns = ['new', 'load', 'receiver', 'documentation',
|
426
767
|
'parent', 'lift', 'solve', 'stop', 'reset', 'zoomin', 'zoomout',
|
427
768
|
'stepback', 'stepforward', 'autosave', 'recall'];
|
@@ -429,8 +770,7 @@ class GUIController extends Controller {
|
|
429
770
|
this.edit_btns, this.model_btns, this.other_btns);
|
430
771
|
|
431
772
|
// Add all button DOM elements as controller properties.
|
432
|
-
for(
|
433
|
-
const b = this.all_btns[i];
|
773
|
+
for(const b of this.all_btns) {
|
434
774
|
this.buttons[b] = document.getElementById(b + '-btn');
|
435
775
|
}
|
436
776
|
this.active_button = null;
|
@@ -450,10 +790,9 @@ class GUIController extends Controller {
|
|
450
790
|
const main_modals = ['logon', 'model', 'load', 'password', 'settings',
|
451
791
|
'actors', 'add-process', 'add-product', 'move', 'note', 'clone',
|
452
792
|
'replace', 'expression', 'server', 'solver'];
|
453
|
-
for(
|
454
|
-
this.modals[main_modals[i]] = new ModalDialog(main_modals[i]);
|
455
|
-
}
|
793
|
+
for(const m of main_modals) this.modals[m] = new ModalDialog(m);
|
456
794
|
|
795
|
+
// Property dialogs for entities may permit group editing.
|
457
796
|
this.modals.cluster = new GroupPropertiesDialog('cluster', {
|
458
797
|
'collapsed': 'collapsed',
|
459
798
|
'ignore': 'ignore',
|
@@ -496,6 +835,17 @@ class GUIController extends Controller {
|
|
496
835
|
'no-links': 'no_links'
|
497
836
|
});
|
498
837
|
|
838
|
+
// The Dataset group modal.
|
839
|
+
this.modals.datasetgroup = new GroupPropertiesDialog('datasetgroup', {
|
840
|
+
'default': 'default_value',
|
841
|
+
'unit': 'scale_unit',
|
842
|
+
'periodic': 'periodic',
|
843
|
+
'array': 'array',
|
844
|
+
'time-scale': 'time_scale',
|
845
|
+
'time-unit': 'time_unit',
|
846
|
+
'method': 'method'
|
847
|
+
});
|
848
|
+
|
499
849
|
// Initially, no dialog being dragged or resized.
|
500
850
|
this.dr_dialog = null;
|
501
851
|
|
@@ -534,12 +884,18 @@ class GUIController extends Controller {
|
|
534
884
|
this.cc.addEventListener('drop', (event) => UI.drop(event));
|
535
885
|
|
536
886
|
// Disable dragging on all images.
|
537
|
-
const
|
538
|
-
|
539
|
-
|
540
|
-
for(let i = 0; i < imgs.length; i++) {
|
541
|
-
imgs[i].addEventListener('dragstart', nodrag);
|
887
|
+
const nodrag = (event) => { event.preventDefault(); return false; };
|
888
|
+
for(const img of document.getElementsByTagName('img')) {
|
889
|
+
img.addEventListener('dragstart', nodrag);
|
542
890
|
}
|
891
|
+
|
892
|
+
// Moving cursor over Linny-R logo etc. should display information
|
893
|
+
// in Information & Documentation manager.
|
894
|
+
const lrf = () => DOCUMENTATION_MANAGER.clearEntity(true);
|
895
|
+
document.getElementById('static-icon').addEventListener('mousemove', lrf);
|
896
|
+
document.getElementById('linny-r-name').addEventListener('mousemove', lrf);
|
897
|
+
document.getElementById('linny-r-version-number')
|
898
|
+
.addEventListener('mousemove', lrf);
|
543
899
|
|
544
900
|
// Make all buttons respond to a mouse click.
|
545
901
|
this.buttons['new'].addEventListener('click',
|
@@ -552,10 +908,8 @@ class GUIController extends Controller {
|
|
552
908
|
() => FILE_MANAGER.saveModel(event.shiftKey));
|
553
909
|
this.buttons.actors.addEventListener('click',
|
554
910
|
() => ACTOR_MANAGER.showDialog());
|
555
|
-
this.buttons.diagram.addEventListener('click',
|
556
|
-
() => FILE_MANAGER.renderDiagramAsPNG(event.shiftKey));
|
557
911
|
this.buttons.savediagram.addEventListener('click',
|
558
|
-
() => FILE_MANAGER.saveDiagramAsSVG(event
|
912
|
+
() => FILE_MANAGER.saveDiagramAsSVG(event));
|
559
913
|
this.buttons.receiver.addEventListener('click',
|
560
914
|
() => RECEIVER.toggle());
|
561
915
|
// NOTE: All draggable & resizable dialogs "toggle" show/hide.
|
@@ -651,11 +1005,9 @@ class GUIController extends Controller {
|
|
651
1005
|
() => AUTO_SAVE.getAutoSavedModels());
|
652
1006
|
|
653
1007
|
// Make "stay active" buttons respond to Shift-click.
|
654
|
-
const
|
655
|
-
|
656
|
-
|
657
|
-
for(let i = 0; i < tbs.length; i++) {
|
658
|
-
tbs[i].addEventListener('click', tf);
|
1008
|
+
const tf = (event) => UI.toggleButton(event);
|
1009
|
+
for(const tb of document.getElementsByClassName('toggle')) {
|
1010
|
+
tb.addEventListener('click', tf);
|
659
1011
|
}
|
660
1012
|
|
661
1013
|
// Add listeners to OK and CANCEL buttons on main modal dialogs.
|
@@ -805,6 +1157,11 @@ class GUIController extends Controller {
|
|
805
1157
|
this.modals.product.element('io').addEventListener('click',
|
806
1158
|
() => UI.toggleImportExportBox('product'));
|
807
1159
|
|
1160
|
+
this.modals.datasetgroup.ok.addEventListener('click',
|
1161
|
+
() => FINDER.updateDatasetGroupProperties());
|
1162
|
+
this.modals.datasetgroup.cancel.addEventListener('click',
|
1163
|
+
() => UI.modals.datasetgroup.hide());
|
1164
|
+
|
808
1165
|
this.modals.link.ok.addEventListener('click',
|
809
1166
|
() => UI.updateLinkProperties());
|
810
1167
|
this.modals.link.cancel.addEventListener('click',
|
@@ -857,18 +1214,16 @@ class GUIController extends Controller {
|
|
857
1214
|
|
858
1215
|
// Make checkboxes respond to click.
|
859
1216
|
// NOTE: Checkbox-specific events must be bound AFTER this general setting.
|
860
|
-
const
|
861
|
-
|
862
|
-
|
863
|
-
for(let i = 0; i < cbs.length; i++) {
|
864
|
-
cbs[i].addEventListener('click', cbf);
|
1217
|
+
const cbf = (event) => UI.toggleBox(event);
|
1218
|
+
for(const cb of document.getElementsByClassName('box')) {
|
1219
|
+
cb.addEventListener('click', cbf);
|
865
1220
|
}
|
866
|
-
// Make infoline respond to `mouseenter
|
1221
|
+
// Make infoline respond to `mouseenter`.
|
867
1222
|
this.info_line = document.getElementById('info-line');
|
868
1223
|
this.info_line.addEventListener('mouseenter',
|
869
1224
|
(event) => DOCUMENTATION_MANAGER.showInfoMessages(event.shiftKey));
|
870
1225
|
// Ensure that all modal windows respond to ESCape
|
871
|
-
// (and more in general to other special keys)
|
1226
|
+
// (and more in general to other special keys).
|
872
1227
|
document.addEventListener('keydown', (event) => UI.checkModals(event));
|
873
1228
|
}
|
874
1229
|
|
@@ -993,8 +1348,7 @@ class GUIController extends Controller {
|
|
993
1348
|
|
994
1349
|
drawLinkArrows(cluster, link) {
|
995
1350
|
// Draw all arrows in `cluster` that represent `link`.
|
996
|
-
for(
|
997
|
-
const a = cluster.arrows[i];
|
1351
|
+
for(const a of cluster.arrows) {
|
998
1352
|
if(a.links.indexOf(link) >= 0) this.paper.drawArrow(a);
|
999
1353
|
}
|
1000
1354
|
}
|
@@ -1011,8 +1365,7 @@ class GUIController extends Controller {
|
|
1011
1365
|
if(VM.server === 'local host') {
|
1012
1366
|
host.title = 'Linny-R directory is ' + VM.working_directory;
|
1013
1367
|
}
|
1014
|
-
for(
|
1015
|
-
const s = VM.solver_list[i];
|
1368
|
+
for(const s of VM.solver_list) {
|
1016
1369
|
html.push(['<option value="', s,
|
1017
1370
|
(s === VM.solver_id ? '"selected="selected' : ''),
|
1018
1371
|
'">', VM.solver_names[s], '</option>'].join(''));
|
@@ -1562,10 +1915,10 @@ class GUIController extends Controller {
|
|
1562
1915
|
|
1563
1916
|
reorderDialogs() {
|
1564
1917
|
// Set z-index of draggable dialogs according to their order
|
1565
|
-
// (most recently shown or clicked on top)
|
1918
|
+
// (most recently shown or clicked on top).
|
1566
1919
|
let z = 10;
|
1567
|
-
for(
|
1568
|
-
|
1920
|
+
for(const dd of this.dr_dialog_order) {
|
1921
|
+
dd.style.zIndex = z;
|
1569
1922
|
z += 5;
|
1570
1923
|
}
|
1571
1924
|
}
|
@@ -1575,18 +1928,16 @@ class GUIController extends Controller {
|
|
1575
1928
|
//
|
1576
1929
|
|
1577
1930
|
enableButtons(btns) {
|
1578
|
-
|
1579
|
-
|
1580
|
-
const b = document.getElementById(btns[i] + '-btn');
|
1931
|
+
for(const btn of btns.trim().split(/\s+/)) {
|
1932
|
+
const b = document.getElementById(btn + '-btn');
|
1581
1933
|
b.classList.remove('disab', 'activ');
|
1582
1934
|
b.classList.add('enab');
|
1583
1935
|
}
|
1584
1936
|
}
|
1585
1937
|
|
1586
1938
|
disableButtons(btns) {
|
1587
|
-
|
1588
|
-
|
1589
|
-
const b = document.getElementById(btns[i] + '-btn');
|
1939
|
+
for(const btn of btns.trim().split(/\s+/)) {
|
1940
|
+
const b = document.getElementById(btn + '-btn');
|
1590
1941
|
b.classList.remove('enab', 'activ', 'stay-activ');
|
1591
1942
|
b.classList.add('disab');
|
1592
1943
|
}
|
@@ -1598,7 +1949,7 @@ class GUIController extends Controller {
|
|
1598
1949
|
node_btns = 'process product link constraint cluster note ',
|
1599
1950
|
edit_btns = 'replace clone paste delete undo redo ',
|
1600
1951
|
model_btns = 'settings save actors dataset equation chart ' +
|
1601
|
-
'
|
1952
|
+
'savediagram finder monitor solve';
|
1602
1953
|
if(MODEL === null) {
|
1603
1954
|
this.disableButtons(node_btns + edit_btns + model_btns);
|
1604
1955
|
return;
|
@@ -1680,10 +2031,9 @@ class GUIController extends Controller {
|
|
1680
2031
|
}
|
1681
2032
|
|
1682
2033
|
get stayActiveButton() {
|
1683
|
-
// Return the button that is "stay active", or NULL if none
|
1684
|
-
const
|
1685
|
-
|
1686
|
-
const b = document.getElementById(btns[i] + '-btn');
|
2034
|
+
// Return the button that is "stay active", or NULL if none .
|
2035
|
+
for(const btn of ['process', 'product', 'link', 'constraint', 'cluster', 'note']) {
|
2036
|
+
const b = document.getElementById(btn + '-btn');
|
1687
2037
|
if(b.classList.contains('stay-activ')) return b;
|
1688
2038
|
}
|
1689
2039
|
return null;
|
@@ -1707,12 +2057,20 @@ class GUIController extends Controller {
|
|
1707
2057
|
//
|
1708
2058
|
|
1709
2059
|
updateCursorPosition(e) {
|
1710
|
-
//
|
2060
|
+
// Update the cursor coordinates, and display them on the status bar.
|
1711
2061
|
const cp = this.paper.cursorPosition(e.pageX, e.pageY);
|
2062
|
+
// Keep track of the cumulative relative movement since the last
|
2063
|
+
// mousedown event.
|
2064
|
+
this.net_move_x += cp[0] - this.mouse_x;
|
2065
|
+
this.net_move_y += cp[1] - this.mouse_y;
|
2066
|
+
// Only now update the mouse coordinates.
|
1712
2067
|
this.mouse_x = cp[0];
|
1713
2068
|
this.mouse_y = cp[1];
|
2069
|
+
// Show the coordinates on the status bar.
|
1714
2070
|
document.getElementById('pos-x').innerHTML = 'X = ' + this.mouse_x;
|
1715
|
-
document.getElementById('pos-y').innerHTML = 'Y = ' + this.mouse_y;
|
2071
|
+
document.getElementById('pos-y').innerHTML = 'Y = ' + this.mouse_y;
|
2072
|
+
// Reset all "object under cursor detection variables" so that they
|
2073
|
+
// will be re-established correctly by mouseMove.
|
1716
2074
|
this.on_note = null;
|
1717
2075
|
this.on_node = null;
|
1718
2076
|
this.on_cluster = null;
|
@@ -1723,76 +2081,82 @@ class GUIController extends Controller {
|
|
1723
2081
|
}
|
1724
2082
|
|
1725
2083
|
mouseMove(e) {
|
1726
|
-
//
|
2084
|
+
// Respond to mouse cursor moving over Linny-R diagram area.
|
2085
|
+
// First translate browser cursor coordinates to diagram coordinates.
|
1727
2086
|
this.updateCursorPosition(e);
|
1728
2087
|
|
1729
|
-
// NOTE:
|
2088
|
+
// NOTE: Prevent errors in case MODEL is still undefined.
|
1730
2089
|
if(!MODEL) return;
|
1731
2090
|
|
1732
2091
|
//console.log(e);
|
1733
2092
|
const fc = MODEL.focal_cluster;
|
2093
|
+
// NOTE: Proceed from last added to first added node.
|
1734
2094
|
for(let i = fc.processes.length-1; i >= 0; i--) {
|
1735
|
-
const
|
1736
|
-
if(
|
1737
|
-
this.on_node =
|
2095
|
+
const p = fc.processes[i];
|
2096
|
+
if(p.containsPoint(this.mouse_x, this.mouse_y)) {
|
2097
|
+
this.on_node = p;
|
1738
2098
|
break;
|
1739
2099
|
}
|
1740
2100
|
}
|
1741
2101
|
if(!this.on_node) {
|
1742
2102
|
for(let i = fc.product_positions.length-1; i >= 0; i--) {
|
1743
|
-
|
1744
|
-
|
1745
|
-
|
2103
|
+
// NOTE: Set product coordinates to its position in focal cluster.
|
2104
|
+
const p = fc.product_positions[i].product.setPositionInFocalCluster();
|
2105
|
+
if(p.product.containsPoint(this.mouse_x, this.mouse_y)) {
|
2106
|
+
this.on_node = p.product;
|
1746
2107
|
break;
|
1747
2108
|
}
|
1748
2109
|
}
|
1749
2110
|
}
|
1750
|
-
for(
|
1751
|
-
const arr = fc.arrows[i];
|
2111
|
+
for(const arr of fc.arrows) {
|
1752
2112
|
if(arr) {
|
1753
2113
|
this.on_arrow = arr;
|
1754
|
-
// NOTE:
|
1755
|
-
|
1756
|
-
|
1757
|
-
|
2114
|
+
// NOTE: Arrow may represent multiple links, and `containsPoint`
|
2115
|
+
// returns the link if this can be established unambiguously, or
|
2116
|
+
// NULL otherwise.
|
2117
|
+
const l = arr.containsPoint(this.mouse_x, this.mouse_y);
|
2118
|
+
if(l) {
|
2119
|
+
this.on_link = l;
|
1758
2120
|
break;
|
1759
2121
|
}
|
1760
2122
|
}
|
1761
2123
|
}
|
1762
2124
|
this.on_constraint = this.constraintStillUnderCursor();
|
1763
2125
|
if(fc.related_constraints != null) {
|
1764
|
-
for(
|
1765
|
-
|
1766
|
-
|
1767
|
-
this.on_constraint = obj;
|
2126
|
+
for(const c of fc.related_constraints) {
|
2127
|
+
if(c.containsPoint(this.mouse_x, this.mouse_y)) {
|
2128
|
+
this.on_constraint = c;
|
1768
2129
|
break;
|
1769
2130
|
}
|
1770
2131
|
}
|
1771
2132
|
}
|
1772
2133
|
for(let i = fc.sub_clusters.length-1; i >= 0; i--) {
|
1773
|
-
const
|
1774
|
-
|
1775
|
-
|
1776
|
-
|
1777
|
-
|
1778
|
-
|
1779
|
-
|
1780
|
-
|
2134
|
+
const c = fc.sub_clusters[i];
|
2135
|
+
if(c.containsPoint(this.mouse_x, this.mouse_y)) {
|
2136
|
+
// NOTE: Cluster that is being dragged is superseded by other clusters
|
2137
|
+
// so that a cluster it is being dragged over will be detected instead.
|
2138
|
+
if(!this.on_cluster || c !== this.dragged_node) {
|
2139
|
+
this.on_cluster = c;
|
2140
|
+
// NOTE: Cluster edge responds differently to doubble-click.
|
2141
|
+
this.on_cluster_edge = c.onEdge(this.mouse_x, this.mouse_y);
|
2142
|
+
}
|
1781
2143
|
}
|
1782
2144
|
}
|
1783
2145
|
// Unset and redraw target cluster if cursor no longer over it.
|
1784
|
-
if(
|
2146
|
+
if(this.on_cluster !== this.target_cluster) {
|
1785
2147
|
const c = this.target_cluster;
|
1786
2148
|
this.target_cluster = null;
|
1787
|
-
|
1788
|
-
|
1789
|
-
|
1790
|
-
|
2149
|
+
if(c) {
|
2150
|
+
UI.paper.drawCluster(c);
|
2151
|
+
// NOTE: Element is persistent, so semi-transparency must also be
|
2152
|
+
// undone.
|
2153
|
+
c.shape.element.setAttribute('opacity', 1);
|
2154
|
+
}
|
1791
2155
|
}
|
1792
2156
|
for(let i = fc.notes.length-1; i >= 0; i--) {
|
1793
|
-
const
|
1794
|
-
if(
|
1795
|
-
this.on_note =
|
2157
|
+
const n = fc.notes[i];
|
2158
|
+
if(n.containsPoint(this.mouse_x, this.mouse_y)) {
|
2159
|
+
this.on_note = n;
|
1796
2160
|
break;
|
1797
2161
|
}
|
1798
2162
|
}
|
@@ -1857,10 +2221,12 @@ class GUIController extends Controller {
|
|
1857
2221
|
this.setMessage('');
|
1858
2222
|
}
|
1859
2223
|
}
|
1860
|
-
// When dragging selection
|
2224
|
+
// When dragging a selection over a cluster, change cursor to "cell" to
|
1861
2225
|
// indicate that selected process(es) will be moved into the cluster.
|
1862
2226
|
if(this.dragged_node) {
|
1863
|
-
|
2227
|
+
// NOTE: Cursor will always be over the dragged node, so do not indicate
|
2228
|
+
// "drop here?" unless dragged over a different cluster.
|
2229
|
+
if(this.on_cluster && this.on_cluster !== this.dragged_node) {
|
1864
2230
|
cr = 'cell';
|
1865
2231
|
this.target_cluster = this.on_cluster;
|
1866
2232
|
// Redraw the target cluster so it will appear on top (and highlighted).
|
@@ -1874,10 +2240,16 @@ class GUIController extends Controller {
|
|
1874
2240
|
}
|
1875
2241
|
|
1876
2242
|
mouseDown(e) {
|
1877
|
-
//
|
1878
|
-
//
|
1879
|
-
//
|
2243
|
+
// Respond to mousedown event in model diagram area.
|
2244
|
+
// NOTE: While dragging the selection rectangle, the mouseup event will
|
2245
|
+
// not be observed when it occurred outside the drawing area. In such
|
2246
|
+
// cases, the mousedown event must be ignored so that only the mouseup
|
2247
|
+
// will be processed.
|
1880
2248
|
if(this.start_sel_x >= 0 && this.start_sel_y >= 0) return;
|
2249
|
+
// Reset the cumulative movement since mousedown.
|
2250
|
+
this.net_move_x = 0;
|
2251
|
+
this.net_move_y = 0;
|
2252
|
+
// Get the paper coordinates indicated by the cursor.
|
1881
2253
|
const cp = this.paper.cursorPosition(e.pageX, e.pageY);
|
1882
2254
|
this.mouse_down_x = cp[0];
|
1883
2255
|
this.mouse_down_y = cp[1];
|
@@ -1891,7 +2263,7 @@ class GUIController extends Controller {
|
|
1891
2263
|
}
|
1892
2264
|
// NOTE: Only left button is detected (browser catches right menu button).
|
1893
2265
|
if(e.ctrlKey) {
|
1894
|
-
// Remove clicked item from selection
|
2266
|
+
// Remove clicked item from selection.
|
1895
2267
|
if(MODEL.selection) {
|
1896
2268
|
// NOTE: First check constraints -- see mouseMove() for motivation.
|
1897
2269
|
if(this.on_constraint) {
|
@@ -1943,37 +2315,15 @@ class GUIController extends Controller {
|
|
1943
2315
|
UI.drawDiagram(MODEL);
|
1944
2316
|
}
|
1945
2317
|
|
1946
|
-
// If one of the top six sidebar buttons is active, prompt for new node
|
1947
|
-
//
|
2318
|
+
// If one of the top six sidebar buttons is active, prompt for new node.
|
2319
|
+
// Note that this does not apply for links or constraints.
|
1948
2320
|
if(this.active_button && this.active_button !== this.buttons.link &&
|
1949
2321
|
this.active_button !== this.buttons.constraint) {
|
1950
2322
|
this.add_x = this.mouse_x;
|
1951
2323
|
this.add_y = this.mouse_y;
|
1952
|
-
const
|
2324
|
+
const ot = this.active_button.id.split('-')[0];
|
1953
2325
|
if(!this.stayActive) this.resetActiveButton();
|
1954
|
-
if(
|
1955
|
-
setTimeout(() => {
|
1956
|
-
const md = UI.modals['add-process'];
|
1957
|
-
md.element('name').value = '';
|
1958
|
-
md.element('actor').value = '';
|
1959
|
-
md.show('name');
|
1960
|
-
});
|
1961
|
-
} else if(obj === 'product') {
|
1962
|
-
setTimeout(() => {
|
1963
|
-
const md = UI.modals['add-product'];
|
1964
|
-
md.element('name').value = '';
|
1965
|
-
md.element('unit').value = MODEL.default_unit;
|
1966
|
-
UI.setBox('add-product-data', false);
|
1967
|
-
md.show('name');
|
1968
|
-
});
|
1969
|
-
} else if(obj === 'cluster') {
|
1970
|
-
setTimeout(() => {
|
1971
|
-
const md = UI.modals.cluster;
|
1972
|
-
md.element('name').value = '';
|
1973
|
-
md.element('actor').value = '';
|
1974
|
-
md.show('name');
|
1975
|
-
});
|
1976
|
-
} else if(obj === 'note') {
|
2326
|
+
if(ot === 'note') {
|
1977
2327
|
setTimeout(() => {
|
1978
2328
|
const md = UI.modals.note;
|
1979
2329
|
md.element('action').innerHTML = 'Add';
|
@@ -1981,6 +2331,33 @@ class GUIController extends Controller {
|
|
1981
2331
|
md.element('text').value = '';
|
1982
2332
|
md.show('text');
|
1983
2333
|
});
|
2334
|
+
} else {
|
2335
|
+
// Align position to the grid.
|
2336
|
+
this.add_x = MODEL.aligned(this.add_x);
|
2337
|
+
this.add_y = MODEL.aligned(this.add_y);
|
2338
|
+
if(ot === 'process') {
|
2339
|
+
setTimeout(() => {
|
2340
|
+
const md = UI.modals['add-process'];
|
2341
|
+
md.element('name').value = '';
|
2342
|
+
md.element('actor').value = '';
|
2343
|
+
md.show('name');
|
2344
|
+
});
|
2345
|
+
} else if(ot === 'product') {
|
2346
|
+
setTimeout(() => {
|
2347
|
+
const md = UI.modals['add-product'];
|
2348
|
+
md.element('name').value = '';
|
2349
|
+
md.element('unit').value = MODEL.default_unit;
|
2350
|
+
UI.setBox('add-product-data', false);
|
2351
|
+
md.show('name');
|
2352
|
+
});
|
2353
|
+
} else if(ot === 'cluster') {
|
2354
|
+
setTimeout(() => {
|
2355
|
+
const md = UI.modals.cluster;
|
2356
|
+
md.element('name').value = '';
|
2357
|
+
md.element('actor').value = '';
|
2358
|
+
md.show('name');
|
2359
|
+
});
|
2360
|
+
}
|
1984
2361
|
}
|
1985
2362
|
return;
|
1986
2363
|
}
|
@@ -2020,7 +2397,7 @@ class GUIController extends Controller {
|
|
2020
2397
|
} else if(this.on_node) {
|
2021
2398
|
if(this.active_button === this.buttons.link) {
|
2022
2399
|
this.linking_node = this.on_node;
|
2023
|
-
// NOTE:
|
2400
|
+
// NOTE: Return without updating buttons.
|
2024
2401
|
return;
|
2025
2402
|
} else if(this.active_button === this.buttons.constraint) {
|
2026
2403
|
// Allow constraints only on nodes having upper bounds defined.
|
@@ -2031,6 +2408,7 @@ class GUIController extends Controller {
|
|
2031
2408
|
}
|
2032
2409
|
} else {
|
2033
2410
|
this.dragged_node = this.on_node;
|
2411
|
+
// NOTE: Keep track of relative movement of the dragged node.
|
2034
2412
|
this.move_dx = this.mouse_x - this.on_node.x;
|
2035
2413
|
this.move_dy = this.mouse_y - this.on_node.y;
|
2036
2414
|
if(MODEL.selection.indexOf(this.on_node) < 0) MODEL.select(this.on_node);
|
@@ -2055,6 +2433,10 @@ class GUIController extends Controller {
|
|
2055
2433
|
mouseUp(e) {
|
2056
2434
|
// Responds to mouseup event.
|
2057
2435
|
const cp = this.paper.cursorPosition(e.pageX, e.pageY);
|
2436
|
+
// Keep track of the cumulative relative movement since the last
|
2437
|
+
// mousedown event.
|
2438
|
+
this.net_move_x += cp[0] - this.mouse_x;
|
2439
|
+
this.net_move_y += cp[1] - this.mouse_y;
|
2058
2440
|
this.mouse_up_x = cp[0];
|
2059
2441
|
this.mouse_up_y = cp[1];
|
2060
2442
|
// First check whether user is selecting a rectangle.
|
@@ -2071,44 +2453,32 @@ class GUIController extends Controller {
|
|
2071
2453
|
// If rectangle has size greater than 2x2 pixels, select all elements
|
2072
2454
|
// having their center inside the selection rectangle.
|
2073
2455
|
if(brx - tlx > 2 && bry - tly > 2) {
|
2074
|
-
const
|
2075
|
-
|
2076
|
-
|
2077
|
-
|
2078
|
-
|
2079
|
-
}
|
2456
|
+
const
|
2457
|
+
ol = [],
|
2458
|
+
fc = MODEL.focal_cluster;
|
2459
|
+
for(const p of fc.processes) {
|
2460
|
+
if(p.x >= tlx && p.x <= brx && p.y >= tly && p.y < bry) ol.push(p);
|
2080
2461
|
}
|
2081
|
-
for(
|
2082
|
-
|
2083
|
-
|
2084
|
-
ol.push(obj.product);
|
2462
|
+
for(const pp of fc.product_positions) {
|
2463
|
+
if(pp.x >= tlx && pp.x <= brx && pp.y >= tly && pp.y < bry) {
|
2464
|
+
ol.push(pp.product);
|
2085
2465
|
}
|
2086
2466
|
}
|
2087
|
-
for(
|
2088
|
-
|
2089
|
-
if(obj.x >= tlx && obj.x <= brx && obj.y >= tly && obj.y < bry) {
|
2090
|
-
ol.push(obj);
|
2091
|
-
}
|
2467
|
+
for(const c of fc.sub_clusters) {
|
2468
|
+
if(c.x >= tlx && c.x <= brx && c.y >= tly && c.y < bry) ol.push(c);
|
2092
2469
|
}
|
2093
|
-
for(
|
2094
|
-
|
2095
|
-
if(obj.x >= tlx && obj.x <= brx && obj.y >= tly && obj.y < bry) {
|
2096
|
-
ol.push(obj);
|
2097
|
-
}
|
2470
|
+
for(const n of fc.notes) {
|
2471
|
+
if(n.x >= tlx && n.x <= brx && n.y >= tly && n.y < bry) ol.push(n);
|
2098
2472
|
}
|
2099
|
-
for(let
|
2100
|
-
const
|
2473
|
+
for(let k in MODEL.links) if(MODEL.links.hasOwnProperty(k)) {
|
2474
|
+
const l = MODEL.links[k];
|
2101
2475
|
// Only add a link if both its nodes are selected as well.
|
2102
|
-
if(fc.linkInList(
|
2103
|
-
ol.push(obj);
|
2104
|
-
}
|
2476
|
+
if(fc.linkInList(l, ol)) ol.push(l);
|
2105
2477
|
}
|
2106
|
-
for(let
|
2107
|
-
const
|
2478
|
+
for(let k in MODEL.constraints) if(MODEL.constraints.hasOwnProperty(k)) {
|
2479
|
+
const c = MODEL.constraints[k];
|
2108
2480
|
// Only add a constraint if both its nodes are selected as well.
|
2109
|
-
if(fc.linkInList(
|
2110
|
-
ol.push(obj);
|
2111
|
-
}
|
2481
|
+
if(fc.linkInList(c, ol)) ol.push(c);
|
2112
2482
|
}
|
2113
2483
|
// Having compiled the object list, actually select them.
|
2114
2484
|
MODEL.selectList(ol);
|
@@ -2123,9 +2493,9 @@ class GUIController extends Controller {
|
|
2123
2493
|
} else if(this.linking_node) {
|
2124
2494
|
// If so, check whether the cursor is over a node of the appropriate type.
|
2125
2495
|
if(this.on_node && MODEL.canLink(this.linking_node, this.on_node)) {
|
2126
|
-
const
|
2127
|
-
UNDO_STACK.push('add',
|
2128
|
-
MODEL.select(
|
2496
|
+
const l = MODEL.addLink(this.linking_node, this.on_node);
|
2497
|
+
UNDO_STACK.push('add', l);
|
2498
|
+
MODEL.select(l);
|
2129
2499
|
this.paper.drawModel(MODEL);
|
2130
2500
|
}
|
2131
2501
|
this.linking_node = null;
|
@@ -2136,9 +2506,9 @@ class GUIController extends Controller {
|
|
2136
2506
|
// dragging its endpoint).
|
2137
2507
|
} else if(this.constraining_node) {
|
2138
2508
|
if(this.on_node && this.constraining_node.canConstrain(this.on_node)) {
|
2139
|
-
//
|
2140
|
-
CONSTRAINT_EDITOR.from_name.
|
2141
|
-
CONSTRAINT_EDITOR.to_name.
|
2509
|
+
// Display constraint editor.
|
2510
|
+
CONSTRAINT_EDITOR.from_name.innerText = this.constraining_node.displayName;
|
2511
|
+
CONSTRAINT_EDITOR.to_name.innerText = this.on_node.displayName;
|
2142
2512
|
CONSTRAINT_EDITOR.showDialog();
|
2143
2513
|
}
|
2144
2514
|
this.linking_node = null;
|
@@ -2149,31 +2519,25 @@ class GUIController extends Controller {
|
|
2149
2519
|
// Then check whether the user is moving a node (possibly part of a
|
2150
2520
|
// larger selection).
|
2151
2521
|
} else if(this.dragged_node) {
|
2152
|
-
//
|
2153
|
-
//
|
2154
|
-
|
2155
|
-
|
2156
|
-
// Set cursor to pointer, as it should be on some node while dragging.
|
2157
|
-
this.paper.container.style.cursor = 'pointer';
|
2158
|
-
// @@TO DO: if on top of a cluster, move it there.
|
2159
|
-
// NOTE: Cursor will always be over the selected cluster (while dragging).
|
2160
|
-
if(this.on_cluster && !this.on_cluster.selected) {
|
2161
|
-
UNDO_STACK.push('drop', this.on_cluster);
|
2162
|
-
MODEL.dropSelectionIntoCluster(this.on_cluster);
|
2163
|
-
this.on_node = null;
|
2164
|
-
this.on_note = null;
|
2165
|
-
this.target_cluster = null;
|
2166
|
-
// Redraw cluster to erase its orange "target corona".
|
2167
|
-
UI.paper.drawCluster(this.on_cluster);
|
2168
|
-
}
|
2169
|
-
|
2170
|
-
// Check wether the cursor has been moved.
|
2522
|
+
// NOTE: When double-clicking with a sensitive mouse, the cursor
|
2523
|
+
// may move a few pixels, and then this should NOT be considered
|
2524
|
+
// as an intentional move. Hence, check wether the cursor has been
|
2525
|
+
// moved *significantly* since the mouseDown event.
|
2171
2526
|
const
|
2172
|
-
|
2173
|
-
|
2174
|
-
|
2175
|
-
|
2176
|
-
|
2527
|
+
mdx = this.mouse_down_x - this.mouse_x,
|
2528
|
+
mdy = this.mouse_down_y - this.mouse_y,
|
2529
|
+
absdx = Math.abs(this.net_move_x),
|
2530
|
+
absdy = Math.abs(this.net_move_y),
|
2531
|
+
sigmv = (MODEL.align_to_grid ? MODEL.grid_pixels / 4 : 2.5);
|
2532
|
+
if(this.doubleClicked) {
|
2533
|
+
// Ignore insignificant move.
|
2534
|
+
if(absdx < sigmv && absdy < sigmv) {
|
2535
|
+
// Undo the move and remove the action from the UNDO-stack.
|
2536
|
+
// NOTE: Do not use the regular `undo` routine as this would
|
2537
|
+
// make the action redoable.
|
2538
|
+
MODEL.moveSelection(mdx, mdy);
|
2539
|
+
UNDO_STACK.pop('move');
|
2540
|
+
}
|
2177
2541
|
// Double-clicking opens properties dialog, except for clusters;
|
2178
2542
|
// then "drill down", i.e., make the double-clicked cluster focal.
|
2179
2543
|
if(this.dragged_node instanceof Cluster) {
|
@@ -2197,6 +2561,30 @@ class GUIController extends Controller {
|
|
2197
2561
|
} else {
|
2198
2562
|
this.showNotePropertiesDialog(this.dragged_node);
|
2199
2563
|
}
|
2564
|
+
} else {
|
2565
|
+
// Move the selection, even if the movement is very small, because the
|
2566
|
+
// final movement since last mouse event may make the *cumulative*
|
2567
|
+
// movement since the last mouseDown significant.
|
2568
|
+
MODEL.moveSelection(
|
2569
|
+
this.mouse_up_x - this.mouse_x, this.mouse_up_y - this.mouse_y);
|
2570
|
+
if(this.net_move_x < 0.5 && this.net_move_y < 0.5) {
|
2571
|
+
// No effective move of the selection => remove the UNDO.
|
2572
|
+
UNDO_STACK.pop('move');
|
2573
|
+
}
|
2574
|
+
// Set cursor to pointer, as it should be on some node while dragging.
|
2575
|
+
this.paper.container.style.cursor = 'pointer';
|
2576
|
+
// NOTE: Cursor will always be over the selected cluster (while dragging).
|
2577
|
+
if(this.on_cluster && !this.on_cluster.selected) {
|
2578
|
+
UNDO_STACK.push('drop', this.on_cluster);
|
2579
|
+
MODEL.dropSelectionIntoCluster(this.on_cluster);
|
2580
|
+
this.on_node = null;
|
2581
|
+
this.on_note = null;
|
2582
|
+
this.target_cluster = null;
|
2583
|
+
// Redraw cluster to erase its orange "target corona".
|
2584
|
+
UI.paper.drawCluster(this.on_cluster);
|
2585
|
+
}
|
2586
|
+
// Only now align to grid.
|
2587
|
+
MODEL.alignToGrid();
|
2200
2588
|
}
|
2201
2589
|
this.dragged_node = null;
|
2202
2590
|
|
@@ -2210,6 +2598,8 @@ class GUIController extends Controller {
|
|
2210
2598
|
this.showConstraintPropertiesDialog(this.on_constraint);
|
2211
2599
|
}
|
2212
2600
|
}
|
2601
|
+
// Finally, reset "selecting with rectangle" (just to be sure), and
|
2602
|
+
// update the UI button states.
|
2213
2603
|
this.start_sel_x = -1;
|
2214
2604
|
this.start_sel_y = -1;
|
2215
2605
|
this.updateButtons();
|
@@ -2253,9 +2643,8 @@ class GUIController extends Controller {
|
|
2253
2643
|
topmod = null,
|
2254
2644
|
code = e.code,
|
2255
2645
|
alt = e.altKey;
|
2256
|
-
for(
|
2646
|
+
for(const m of modals) {
|
2257
2647
|
const
|
2258
|
-
m = modals[i],
|
2259
2648
|
cs = window.getComputedStyle(m),
|
2260
2649
|
z = parseInt(cs.zIndex);
|
2261
2650
|
if(cs.display !== 'none' && z > maxz) {
|
@@ -2275,15 +2664,16 @@ class GUIController extends Controller {
|
|
2275
2664
|
while(i < inp.length && inp[i].disabled) i++;
|
2276
2665
|
if(i < inp.length) {
|
2277
2666
|
inp[i].focus();
|
2278
|
-
} else if(['constraint-modal', 'boundline-data-modal',
|
2667
|
+
} else if(['datasetgroup-modal', 'constraint-modal', 'boundline-data-modal',
|
2279
2668
|
'xp-clusters-modal'].indexOf(topmod.id) >= 0) {
|
2280
|
-
// NOTE:
|
2281
|
-
//
|
2669
|
+
// NOTE: These modals must NOT close when Enter is pressed, but only
|
2670
|
+
// de-focus the input field.
|
2282
2671
|
e.target.blur();
|
2283
2672
|
} else {
|
2284
2673
|
const btns = topmod.getElementsByClassName('ok-btn');
|
2285
2674
|
if(btns.length > 0) btns[0].dispatchEvent(new Event('click'));
|
2286
2675
|
}
|
2676
|
+
if(topmod.id === 'datasetgroup-modal') UI.modals.datasetgroup.enterKey();
|
2287
2677
|
} else if(this.dr_dialog_order.length > 0) {
|
2288
2678
|
// Send ENTER key event to the top draggable dialog.
|
2289
2679
|
const last = this.dr_dialog_order.length - 1;
|
@@ -2297,15 +2687,22 @@ class GUIController extends Controller {
|
|
2297
2687
|
// Prevent backspace to be interpreted (by FireFox) as "go back in browser".
|
2298
2688
|
e.preventDefault();
|
2299
2689
|
} else if(ttag === 'BODY') {
|
2300
|
-
// Constraint Editor
|
2301
|
-
if(topmod
|
2302
|
-
if(code.startsWith('Arrow')) {
|
2690
|
+
// Dataset group modal and Constraint Editor accept arrow keys.
|
2691
|
+
if(topmod) {
|
2692
|
+
if(topmod.id === 'constraint-modal' && code.startsWith('Arrow')) {
|
2303
2693
|
e.preventDefault();
|
2304
2694
|
CONSTRAINT_EDITOR.arrowKey(e);
|
2305
2695
|
return;
|
2306
2696
|
}
|
2697
|
+
if(topmod.id === 'datasetgroup-modal' &&
|
2698
|
+
(code === 'ArrowUp' || code === 'ArrowDown')) {
|
2699
|
+
e.preventDefault();
|
2700
|
+
// NOTE: Pass key direction as -1 for UP and +1 for DOWN.
|
2701
|
+
UI.modals.datasetgroup.upDownKey(e.keyCode - 39);
|
2702
|
+
return;
|
2703
|
+
}
|
2307
2704
|
}
|
2308
|
-
//
|
2705
|
+
// Lists in draggable dialogs respond to up and down arrow keys.
|
2309
2706
|
if(code === 'ArrowUp' || code === 'ArrowDown') {
|
2310
2707
|
e.preventDefault();
|
2311
2708
|
// Send event to the top draggable dialog.
|
@@ -2569,7 +2966,7 @@ class GUIController extends Controller {
|
|
2569
2966
|
validNames(nn, an='') {
|
2570
2967
|
// Check whether names meet conventions; if not, warn user
|
2571
2968
|
if(!UI.validName(nn) || nn.indexOf(UI.BLACK_BOX) >= 0) {
|
2572
|
-
|
2969
|
+
this.warningInvalidName(nn);
|
2573
2970
|
return false;
|
2574
2971
|
}
|
2575
2972
|
if(an === '' || an === UI.NO_ACTOR) return true;
|
@@ -2627,12 +3024,12 @@ class GUIController extends Controller {
|
|
2627
3024
|
}
|
2628
3025
|
|
2629
3026
|
updateScaleUnitList() {
|
2630
|
-
// Update the HTML datalist element to reflect all scale units
|
3027
|
+
// Update the HTML datalist element to reflect all scale units.
|
2631
3028
|
const
|
2632
3029
|
ul = [],
|
2633
3030
|
keys = Object.keys(MODEL.scale_units).sort(ciCompare);
|
2634
|
-
for(
|
2635
|
-
ul.push(`<option value="${MODEL.scale_units[
|
3031
|
+
for(const k of keys) {
|
3032
|
+
ul.push(`<option value="${MODEL.scale_units[k].name}">`);
|
2636
3033
|
}
|
2637
3034
|
document.getElementById('units-data').innerHTML = ul.join('');
|
2638
3035
|
}
|
@@ -3001,6 +3398,7 @@ class GUIController extends Controller {
|
|
3001
3398
|
const vn = this.validName(nn);
|
3002
3399
|
if(!vn) {
|
3003
3400
|
UNDO_STACK.pop();
|
3401
|
+
this.warningInvalidName(nn);
|
3004
3402
|
return false;
|
3005
3403
|
}
|
3006
3404
|
// NOTE: Pre-check if product exists.
|
@@ -3229,8 +3627,8 @@ class GUIController extends Controller {
|
|
3229
3627
|
if(elig.length) {
|
3230
3628
|
sl.push('<div class="paste-select"><select id="paste-ft-', i,
|
3231
3629
|
'" style="font-size: 12px">');
|
3232
|
-
for(
|
3233
|
-
const dn =
|
3630
|
+
for(const e of elig) {
|
3631
|
+
const dn = e.displayName;
|
3234
3632
|
sl.push('<option value="', dn, '">', dn, '</option>');
|
3235
3633
|
}
|
3236
3634
|
sl.push('</select></div>');
|
@@ -3404,8 +3802,7 @@ class GUIController extends Controller {
|
|
3404
3802
|
function nameConflicts(node) {
|
3405
3803
|
// Maps names of entities defined by the child nodes of `node`
|
3406
3804
|
// while detecting name conflicts.
|
3407
|
-
for(
|
3408
|
-
const c = node.childNodes[i];
|
3805
|
+
for(const c of node.childNodes) {
|
3409
3806
|
if(c.nodeName !== 'link' && c.nodeName !== 'constraint') {
|
3410
3807
|
const
|
3411
3808
|
fn = fullName(c),
|
@@ -3505,9 +3902,8 @@ class GUIController extends Controller {
|
|
3505
3902
|
// Prompt for names of selected cluster nodes.
|
3506
3903
|
if(selc_node.childNodes.length && !mapping.prefix) {
|
3507
3904
|
mapping.top_clusters = {};
|
3508
|
-
for(
|
3905
|
+
for(const c of selc_node.childNodes) {
|
3509
3906
|
const
|
3510
|
-
c = selc_node.childNodes[i],
|
3511
3907
|
fn = fullName(c),
|
3512
3908
|
mn = mappedName(fn);
|
3513
3909
|
mapping.top_clusters[fn] = mn;
|
@@ -3522,9 +3918,8 @@ class GUIController extends Controller {
|
|
3522
3918
|
const
|
3523
3919
|
ft_map = {},
|
3524
3920
|
ft_type = {};
|
3525
|
-
for(
|
3921
|
+
for(const c of from_tos_node.childNodes) {
|
3526
3922
|
const
|
3527
|
-
c = from_tos_node.childNodes[i],
|
3528
3923
|
fn = fullName(c),
|
3529
3924
|
mn = mappedName(fn);
|
3530
3925
|
if(MODEL.objectByName(mn)) {
|
@@ -3554,20 +3949,14 @@ console.log('HERE name conflicts', name_conflicts, mapping);
|
|
3554
3949
|
}
|
3555
3950
|
|
3556
3951
|
// No conflicts => add all
|
3557
|
-
for(
|
3558
|
-
|
3559
|
-
|
3560
|
-
for(let i = 0; i < from_tos_node.childNodes.length; i++) {
|
3561
|
-
addEntityFromNode(from_tos_node.childNodes[i]);
|
3562
|
-
}
|
3563
|
-
for(let i = 0; i < entities_node.childNodes.length; i++) {
|
3564
|
-
addEntityFromNode(entities_node.childNodes[i]);
|
3565
|
-
}
|
3952
|
+
for(const c of extras_node.childNodes) addEntityFromNode(c);
|
3953
|
+
for(const c of from_tos_node.childNodes) addEntityFromNode(c);
|
3954
|
+
for(const c of entities_node.childNodes) addEntityFromNode(c);
|
3566
3955
|
// Update diagram, showing newly added nodes as selection.
|
3567
3956
|
MODEL.clearSelection();
|
3568
|
-
for(
|
3957
|
+
for(const c of selection_node.childNodes) {
|
3569
3958
|
const
|
3570
|
-
n = xmlDecoded(nodeContent(
|
3959
|
+
n = xmlDecoded(nodeContent(c)),
|
3571
3960
|
obj = MODEL.objectByName(mappedName(n));
|
3572
3961
|
if(obj) {
|
3573
3962
|
// NOTE: Selected products must be positioned.
|
@@ -3681,6 +4070,12 @@ console.log('HERE name conflicts', name_conflicts, mapping);
|
|
3681
4070
|
cb = UI.boxChecked('settings-power');
|
3682
4071
|
redraw = redraw || cb !== model.with_power_flow;
|
3683
4072
|
model.with_power_flow = cb;
|
4073
|
+
// NOTE: Clear the "ignore" options if no power flow constraints.
|
4074
|
+
if(!model.with_power_flow) {
|
4075
|
+
model.ignore_grid_capacity = false;
|
4076
|
+
model.ignore_KVL = false;
|
4077
|
+
model.ignore_power_losses = false;
|
4078
|
+
}
|
3684
4079
|
cb = UI.boxChecked('settings-cost-prices');
|
3685
4080
|
redraw = redraw || cb !== model.infer_cost_prices;
|
3686
4081
|
model.infer_cost_prices = cb;
|
@@ -3749,8 +4144,7 @@ console.log('HERE name conflicts', name_conflicts, mapping);
|
|
3749
4144
|
const
|
3750
4145
|
md = this.modals.solver,
|
3751
4146
|
html = ['<option value="">(default)</option>'];
|
3752
|
-
for(
|
3753
|
-
const s = VM.solver_list[i];
|
4147
|
+
for(const s of VM.solver_list) {
|
3754
4148
|
html.push(['<option value="', s,
|
3755
4149
|
(s === MODEL.preferred_solver ? '"selected="selected' : ''),
|
3756
4150
|
'">', VM.solver_names[s], '</option>'].join(''));
|
@@ -3885,17 +4279,13 @@ console.log('HERE name conflicts', name_conflicts, mapping);
|
|
3885
4279
|
plate.innerHTML = pg.voltage;
|
3886
4280
|
overlay.style.display = 'block';
|
3887
4281
|
// Disable tab stop for the properties that are now not shown.
|
3888
|
-
for(
|
3889
|
-
md.element(notab[i]).tabIndex = -1;
|
3890
|
-
}
|
4282
|
+
for(const nt of notab) md.element(nt).tabIndex = -1;
|
3891
4283
|
} else {
|
3892
4284
|
plate.innerHTML = '(↯)';
|
3893
4285
|
plate.className = 'no-grid-plate';
|
3894
4286
|
overlay.style.display = 'none';
|
3895
4287
|
// Enable tab stop for the properties that are now not shown.
|
3896
|
-
for(
|
3897
|
-
md.element(notab[i]).tabIndex = 0;
|
3898
|
-
}
|
4288
|
+
for(const nt of notab) md.element(nt).tabIndex = 0;
|
3899
4289
|
}
|
3900
4290
|
this.hideGridPlateMenu('process');
|
3901
4291
|
// Show plate "button" only when power grids option is set for model.
|
@@ -4131,9 +4521,7 @@ console.log('HERE name conflicts', name_conflicts, mapping);
|
|
4131
4521
|
|
4132
4522
|
showClusterPropertiesDialog(c, group=[]) {
|
4133
4523
|
let bb = false;
|
4134
|
-
for(
|
4135
|
-
bb = group[i].is_black_boxed;
|
4136
|
-
}
|
4524
|
+
for(const g of group) bb = bb || g.is_black_boxed;
|
4137
4525
|
if(bb || c.is_black_boxed) {
|
4138
4526
|
this.notify('Black-boxed clusters cannot be edited');
|
4139
4527
|
return;
|
@@ -4375,31 +4763,27 @@ console.log('HERE name conflicts', name_conflicts, mapping);
|
|
4375
4763
|
this.drawObject(p);
|
4376
4764
|
// Make list of nodes related to P by links
|
4377
4765
|
const rel_nodes = [];
|
4378
|
-
for(
|
4379
|
-
|
4380
|
-
}
|
4381
|
-
for(let i = 0; i < p.outputs.length; i++) {
|
4382
|
-
rel_nodes.push(p.outputs[i].to_node);
|
4383
|
-
}
|
4766
|
+
for(const l of p.inputs) rel_nodes.push(l.from_node);
|
4767
|
+
for(const l of p.outputs) rel_nodes.push(l.to_node);
|
4384
4768
|
const options = [];
|
4385
|
-
for(let
|
4769
|
+
for(let k in MODEL.products) if(MODEL.products.hasOwnProperty(k) &&
|
4386
4770
|
// NOTE: do not show "black-boxed" products
|
4387
|
-
!
|
4388
|
-
const po = MODEL.products[
|
4771
|
+
!k.startsWith(UI.BLACK_BOX)) {
|
4772
|
+
const po = MODEL.products[k];
|
4389
4773
|
// Skip the product that is to be replaced, an also products having a
|
4390
4774
|
// different type (regular product or data product)
|
4391
4775
|
if(po !== p && po.is_data === p.is_data) {
|
4392
4776
|
// NOTE: also skip products PO that are linked to a node Q that is
|
4393
4777
|
// already linked to P (as replacing would then create a two-way link)
|
4394
4778
|
let no_rel = true;
|
4395
|
-
for(
|
4396
|
-
if(rel_nodes.indexOf(
|
4779
|
+
for(const l of po.inputs) {
|
4780
|
+
if(rel_nodes.indexOf(l.from_node) >= 0) {
|
4397
4781
|
no_rel = false;
|
4398
4782
|
break;
|
4399
4783
|
}
|
4400
4784
|
}
|
4401
|
-
for(
|
4402
|
-
if(rel_nodes.indexOf(
|
4785
|
+
for(const l of po.outputs) {
|
4786
|
+
if(rel_nodes.indexOf(l.to_node) >= 0) {
|
4403
4787
|
no_rel = false;
|
4404
4788
|
break;
|
4405
4789
|
}
|