linny-r 1.9.2 → 2.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (37) hide show
  1. package/LICENSE +1 -1
  2. package/README.md +4 -4
  3. package/package.json +1 -1
  4. package/server.js +1 -1
  5. package/static/images/eq-negated.png +0 -0
  6. package/static/images/power.png +0 -0
  7. package/static/images/tex.png +0 -0
  8. package/static/index.html +225 -10
  9. package/static/linny-r.css +458 -8
  10. package/static/scripts/linny-r-ctrl.js +6 -4
  11. package/static/scripts/linny-r-gui-actor-manager.js +1 -1
  12. package/static/scripts/linny-r-gui-chart-manager.js +20 -13
  13. package/static/scripts/linny-r-gui-constraint-editor.js +410 -50
  14. package/static/scripts/linny-r-gui-controller.js +127 -12
  15. package/static/scripts/linny-r-gui-dataset-manager.js +28 -20
  16. package/static/scripts/linny-r-gui-documentation-manager.js +11 -3
  17. package/static/scripts/linny-r-gui-equation-manager.js +1 -1
  18. package/static/scripts/linny-r-gui-experiment-manager.js +1 -1
  19. package/static/scripts/linny-r-gui-expression-editor.js +7 -1
  20. package/static/scripts/linny-r-gui-file-manager.js +31 -13
  21. package/static/scripts/linny-r-gui-finder.js +1 -1
  22. package/static/scripts/linny-r-gui-model-autosaver.js +1 -1
  23. package/static/scripts/linny-r-gui-monitor.js +1 -1
  24. package/static/scripts/linny-r-gui-paper.js +108 -25
  25. package/static/scripts/linny-r-gui-power-grid-manager.js +529 -0
  26. package/static/scripts/linny-r-gui-receiver.js +1 -1
  27. package/static/scripts/linny-r-gui-repository-browser.js +1 -1
  28. package/static/scripts/linny-r-gui-scale-unit-manager.js +1 -1
  29. package/static/scripts/linny-r-gui-sensitivity-analysis.js +1 -1
  30. package/static/scripts/linny-r-gui-tex-manager.js +110 -0
  31. package/static/scripts/linny-r-gui-undo-redo.js +1 -1
  32. package/static/scripts/linny-r-milp.js +1 -1
  33. package/static/scripts/linny-r-model.js +1016 -155
  34. package/static/scripts/linny-r-utils.js +3 -3
  35. package/static/scripts/linny-r-vm.js +714 -248
  36. package/static/show-diff.html +1 -1
  37. package/static/show-png.html +1 -1
@@ -11,7 +11,7 @@ dialog for the Linny-R constraint editor.
11
11
  */
12
12
 
13
13
  /*
14
- Copyright (c) 2017-2023 Delft University of Technology
14
+ Copyright (c) 2017-2024 Delft University of Technology
15
15
 
16
16
  Permission is hereby granted, free of charge, to any person obtaining a copy
17
17
  of this software and associated documentation files (the "Software"), to deal
@@ -40,7 +40,6 @@ class ConstraintEditor {
40
40
  this.from_name = document.getElementById('constraint-from-name');
41
41
  this.to_name = document.getElementById('constraint-to-name');
42
42
  this.bl_type = document.getElementById('bl-type');
43
- this.bl_selectors = document.getElementById('bl-selectors');
44
43
  this.soc_direct = document.getElementById('constraint-soc-direct');
45
44
  this.soc = document.getElementById('constraint-share-of-cost');
46
45
  this.soc_div = document.getElementById('constraint-soc');
@@ -78,25 +77,69 @@ class ConstraintEditor {
78
77
  () => CONSTRAINT_EDITOR.addBoundLine());
79
78
  this.bl_type.addEventListener('change',
80
79
  () => CONSTRAINT_EDITOR.changeLineType());
81
- this.bl_selectors.addEventListener('blur',
82
- () => CONSTRAINT_EDITOR.changeLineSelectors());
83
80
  this.soc.addEventListener('blur',
84
81
  () => CONSTRAINT_EDITOR.changeShareOfCost());
82
+ this.bl_data_btn = document.getElementById('bl-data-btn');
83
+ this.bl_data_btn.addEventListener('click',
84
+ () => CONSTRAINT_EDITOR.showBoundLineModal());
85
85
  this.delete_bl_btn = document.getElementById('del-bl-btn');
86
86
  this.delete_bl_btn.addEventListener('click',
87
87
  () => CONSTRAINT_EDITOR.deleteBoundLine());
88
+ // Prepare the "precise point" dialog.
88
89
  this.point_modal = new ModalDialog('boundline-point');
89
90
  this.point_modal.ok.addEventListener(
90
91
  'click', () => CONSTRAINT_EDITOR.setPointPosition());
91
92
  this.point_modal.cancel.addEventListener(
92
93
  'click', () => CONSTRAINT_EDITOR.point_modal.hide());
93
- // The chart is stored as an SVG string
94
+ // Also prepare the boundline modal.
95
+ this.boundline_modal = new ModalDialog('boundline-data');
96
+ this.boundline_modal.close.addEventListener(
97
+ 'click', () => CONSTRAINT_EDITOR.updateBoundLineProperties());
98
+ this.boundline_modal.element('edit-btn').addEventListener(
99
+ 'click', () => CONSTRAINT_EDITOR.startEditing());
100
+ this.boundline_modal.element('save-btn').addEventListener(
101
+ 'click', () => CONSTRAINT_EDITOR.stopEditing(true));
102
+ this.boundline_modal.element('cancel-btn').addEventListener(
103
+ 'click', () => CONSTRAINT_EDITOR.stopEditing(false));
104
+ this.boundline_modal.element('url').addEventListener(
105
+ 'blur', () => CONSTRAINT_EDITOR.loadPointData());
106
+ const bls = this.boundline_modal.element('series');
107
+ bls.addEventListener('keyup', () => CONSTRAINT_EDITOR.updateLine());
108
+ bls.addEventListener('click', () => CONSTRAINT_EDITOR.updateLine());
109
+ // NOTE: Chart should show default line when cursor is not over data.
110
+ this.boundline_modal.element('series-table').addEventListener(
111
+ 'mouseout', () => CONSTRAINT_EDITOR.showDefaultBoundLine());
112
+ // Make boundline selector buttons responsive.
113
+ this.selector_btns = 'bl-rename-sel bl-edit-sel bl-delete-sel';
114
+ document.getElementById('bl-add-sel-btn').addEventListener(
115
+ 'click', () => CONSTRAINT_EDITOR.promptForSelector());
116
+ document.getElementById('bl-rename-sel-btn').addEventListener(
117
+ 'click', () => CONSTRAINT_EDITOR.promptForSelector('rename'));
118
+ document.getElementById('bl-edit-sel-btn').addEventListener(
119
+ 'click', () => CONSTRAINT_EDITOR.editExpression());
120
+ document.getElementById('bl-delete-sel-btn').addEventListener(
121
+ 'click', () => CONSTRAINT_EDITOR.deleteSelector());
122
+ // Prepare boundline selector modals.
123
+ this.new_selector_modal = new ModalDialog('new-selector');
124
+ this.new_selector_modal.ok.addEventListener(
125
+ 'click', () => CONSTRAINT_EDITOR.newSelector());
126
+ this.new_selector_modal.cancel.addEventListener(
127
+ 'click', () => CONSTRAINT_EDITOR.new_selector_modal.hide());
128
+ this.rename_selector_modal = new ModalDialog('rename-selector');
129
+ this.rename_selector_modal.ok.addEventListener(
130
+ 'click', () => CONSTRAINT_EDITOR.renameSelector());
131
+ this.rename_selector_modal.cancel.addEventListener(
132
+ 'click', () => CONSTRAINT_EDITOR.rename_selector_modal.hide());
133
+ // The chart is stored as an SVG string.
94
134
  this.svg = '';
95
- // Scale, origin X and Y assume a 300x300 px square chart area
135
+ // The line path and contour path SVG.
136
+ this.line_path_svg = '';
137
+ this.contour_path_svg = '';
138
+ // Scale, origin X and Y assume a 300x300 px square chart area.
96
139
  this.scale = 3;
97
140
  this.oX = 25;
98
141
  this.oY = 315;
99
- // 0 => silver, LE => orange/red, GE => cyan/blue, EQ => purple
142
+ // 0 => silver, LE => orange/red, GE => cyan/blue, EQ => purple.
100
143
  this.line_color = ['#a0a0a0', '#c04000', '#0040c0', '#9000a0'];
101
144
  // Use brighter shades if selected (darker for gray)
102
145
  this.selected_color = ['#808080', '#ff8040', '#00b0d0', '#a800ff'];
@@ -105,37 +148,41 @@ class ConstraintEditor {
105
148
  // Cursor position in chart coordinates (100 x 100 grid)
106
149
  this.pos_x = 0;
107
150
  this.pos_y = 0;
108
- // `on_line`: the first bound line object detected under the cursor
151
+ // `on_line`: the first bound line object detected under the cursor.
109
152
  this.on_line = null;
110
- // `on_point`: index of point under the cursor
153
+ // `on_point`: index of point under the cursor.
111
154
  this.on_point = -1;
112
155
  this.dragged_point = -1;
113
156
  this.selected_point = -1;
157
+ this.selected_selector = false;
114
158
  this.last_time_clicked = 0;
115
159
  this.cursor = 'default';
160
+ // Start in data viewing mode.
161
+ this.stopEditing(false);
116
162
  // Properties for tracking which constraint is being edited.
117
- this.edited_constraint = null;
118
163
  this.from_node = null;
119
164
  this.to_node = null;
120
- // The constraint object being edited (either a new instance, or a
121
- // copy of edited_constraint).
165
+ // The constraint (model entity) being added or modified.
166
+ this.edited_constraint = null;
167
+ // The constraint object that is being modified: either a new instance,
168
+ // or a *copy* of edited_constraint so changes can be ignored on "canel".
122
169
  this.constraint = null;
123
170
  // List of constraints when multiple constraints are edited.
124
171
  this.group = [];
172
+ // Boundline selector expression being edited.
173
+ this.edited_expression = null;
125
174
  // NOTE: All edits will be ignored unless the modeler clicks OK.
126
175
  }
127
176
 
128
- get doubleClicked() {
177
+ get twoClicks() {
178
+ // Return TRUE iff two mouse clicks occurred within 300 ms.
129
179
  const
130
180
  now = Date.now(),
131
181
  dt = now - this.last_time_clicked;
132
182
  this.last_time_clicked = now;
133
- if(this.on_point === this.selected_point) {
134
- // Consider click to be "double" if it occurred less than 300 ms ago
135
- if(dt < 300) {
136
- this.last_time_clicked = 0;
137
- return true;
138
- }
183
+ if(dt < 300) {
184
+ this.last_time_clicked = 0;
185
+ return true;
139
186
  }
140
187
  return false;
141
188
  }
@@ -162,14 +209,21 @@ class ConstraintEditor {
162
209
 
163
210
  mouseDown(e) {
164
211
  // The onMouseDown response of the constraint editor's graph area.
212
+ const two = this.twoClicks;
165
213
  if(this.adding_point) {
166
214
  this.doAddPointToLine();
167
- } else if((e.altKey || this.doubleClicked) && this.on_point >= 0) {
168
- this.positionPoint();
169
215
  } else if(this.on_line) {
216
+ const
217
+ same_line = two && this.selected === this.on_line,
218
+ same_point = two && this.selected_point === this.on_point;
170
219
  this.selectBoundLine(this.on_line);
171
220
  this.dragged_point = this.on_point;
172
221
  this.selected_point = this.on_point;
222
+ if(this.on_point >= 0 && (e.altKey || same_point)) {
223
+ this.positionPoint();
224
+ } else if(this.on_line && (e.altKey || same_line)) {
225
+ this.showBoundLineModal();
226
+ }
173
227
  } else {
174
228
  this.selected = null;
175
229
  this.dragged_point = -1;
@@ -334,7 +388,7 @@ class ConstraintEditor {
334
388
  }
335
389
 
336
390
  doAddPointToLine() {
337
- // Actually add point to selected line
391
+ // Actually add point to selected line.
338
392
  if(!this.selected) return;
339
393
  const
340
394
  p = [this.pos_x, this.pos_y],
@@ -342,6 +396,7 @@ class ConstraintEditor {
342
396
  let i = 0;
343
397
  while(i < lp.length && lp[i][0] < p[0]) i++;
344
398
  lp.splice(i, 0, p);
399
+ this.selected.storePoints();
345
400
  this.selected_point = i;
346
401
  this.dragged_point = i;
347
402
  this.draw();
@@ -355,31 +410,330 @@ class ConstraintEditor {
355
410
  if(this.selected && this.selected_point > 0 &&
356
411
  this.selected_point < this.selected.points.length - 1) {
357
412
  this.selected.points.splice(this.selected_point, 1);
413
+ this.selected.storePoints();
358
414
  this.selected_point = -1;
359
415
  this.draw();
360
416
  }
361
417
  }
362
418
 
363
419
  changeLineType() {
364
- // Changes type of selected boundline
420
+ // Change type of selected boundline.
365
421
  if(this.selected) {
366
422
  this.selected.type = parseInt(this.bl_type.value);
367
423
  this.draw();
368
424
  }
369
425
  }
370
426
 
371
- changeLineSelectors() {
372
- // Changes experiment run selectors of selected boundline
427
+ loadPointData() {
428
+ const md = this.boundline_modal;
429
+ let url = md.element('url').value.trim();
430
+ if(this.selected && url) {
431
+ FILE_MANAGER.getRemoteData(this.selected, url);
432
+ }
433
+ }
434
+
435
+ startEditing() {
436
+ const
437
+ md = this.boundline_modal,
438
+ edit_btn = md.element('edit-btn'),
439
+ save_btn = md.element('save-btn'),
440
+ cancel_btn = md.element('cancel-btn'),
441
+ tbl = md.element('series-table'),
442
+ txt = md.element('series');
443
+ edit_btn.classList.add('off');
444
+ save_btn.classList.remove('off');
445
+ cancel_btn.classList.remove('off');
446
+ tbl.style.display = 'none';
447
+ txt.value = this.selected.pointDataString;
448
+ txt.style.display = 'block';
449
+ txt.focus();
450
+ txt.selectionStart = 0;
451
+ txt.selectionEnd = 0;
452
+ md.element('line').style.display = 'block';
453
+ this.updateLine();
454
+ UI.disableButtons(this.selector_btns);
455
+ }
456
+
457
+ stopEditing(save=false) {
458
+ if(!this.selected) return;
459
+ const
460
+ bl = this.selected,
461
+ md = this.boundline_modal,
462
+ edit_btn = md.element('edit-btn'),
463
+ save_btn = md.element('save-btn'),
464
+ cancel_btn = md.element('cancel-btn'),
465
+ tbl = md.element('series-table'),
466
+ txt = md.element('series');
467
+ if(save) {
468
+ bl.unpackPointDataString(txt.value);
469
+ }
470
+ edit_btn.classList.remove('off');
471
+ save_btn.classList.add('off');
472
+ cancel_btn.classList.add('off');
473
+ txt.style.display = 'none';
474
+ md.element('line').style.display = 'none';
475
+ tbl.innerHTML = this.boundLineDataTable;
476
+ tbl.style.display = 'block';
477
+ if(this.selected_selector) {
478
+ UI.enableButtons(this.selector_btns);
479
+ } else {
480
+ UI.disableButtons(this.selector_btns);
481
+ }
482
+ }
483
+
484
+ updateLine() {
485
+ const
486
+ md = this.boundline_modal,
487
+ txt = md.element('series'),
488
+ ln = md.element('line-number'),
489
+ lc = md.element('line-count');
490
+ ln.innerHTML = 'line ' + txt.value.substring(0, txt.selectionStart)
491
+ .split(';').length;
492
+ lc.innerHTML = 'of ' + txt.value.split(';').length;
493
+ }
494
+
495
+ get boundLineDataTable() {
496
+ // Return *inner* HTML for point coordinates table.
497
+ if(!this.selected) return ;
498
+ const
499
+ bl = this.selected,
500
+ tr = '<tr class="dataset" onmouseover="CONSTRAINT_EDITOR.' +
501
+ 'showDataBoundLine(%N)"><td class="bl-odnr">%N.</td>' +
502
+ '<td class="bl-od">%D</td></tr>',
503
+ lines = [tr.replaceAll('%N', 0).replace('%D',
504
+ bl.pointsDataString + '<span class="grit">(default)</span>')];
505
+ for(let i = 0; i < bl.point_data.length; i++) {
506
+ lines.push(tr.replaceAll('%N', i + 1)
507
+ .replace('%D', bl.point_data[i].join(' ')));
508
+ }
509
+ return lines.join('');
510
+ }
511
+
512
+ get boundLineSelectorTable() {
513
+ // Return *inner* HTML for the boundline selector table.
514
+ if(!this.selected) return '';
515
+ const
516
+ bl = this.selected,
517
+ html = [],
518
+ onclk = ` onclick="CONSTRAINT_EDITOR.selectSelector(event, '');"`;
519
+ for(let i = 0; i < bl.selectors.length; i++) {
520
+ const
521
+ sel = bl.selectors[i],
522
+ ocr = onclk.replace("''", `'${sel.selector}'`),
523
+ ss = (sel.selector === this.selected_selector ? ' sel-set' : '');
524
+ html.push(`<tr id="blstr${i}" class="dataset-modif${ss}">`,
525
+ '<td class="dataset-selector"');
526
+ if(i === 0) {
527
+ html.push(' style="background-color: #e0e0e0; font-style: italic" ',
528
+ 'title="Default line index will be used when no experiment is running"');
529
+ }
530
+ let ls = '',
531
+ rs = '';
532
+ if(sel.grouping) {
533
+ ls = '<span class="blpoints">';
534
+ rs = '</span>';
535
+ }
536
+ if(!sel.expression.isStatic) {
537
+ ls += '<em>';
538
+ rs = '</em>' + rs;
539
+ }
540
+ html.push(ocr.replace("');", "', false);"), '>', sel.selector,
541
+ '</td><td class="dataset-expression"', ocr, '>',
542
+ ls, sel.expression.text, rs, '</td></tr>');
543
+ }
544
+ return html.join('');
545
+ }
546
+
547
+ selectSelector(event, id, x=true) {
548
+ // Select selector, or when double-clicked, edit its expression when
549
+ // x = TRUE, or the name of the selector when x = FALSE.
550
+ if(!this.selected) return;
551
+ const edit = (event.altKey ||
552
+ (this.twoClicks && id === this.selected_selector));
553
+ this.selected_selector = id;
554
+ if(edit) {
555
+ if(x) {
556
+ this.editExpression();
557
+ } else {
558
+ this.promptForSelector('rename');
559
+ }
560
+ return;
561
+ }
562
+ this.updateSelectorTable();
563
+ UI.enableButtons(this.selector_btns);
564
+ // Do not permit deleting the default selector.
565
+ if(id === '(default)') UI.disableButtons('bl-delete-sel');
566
+ }
567
+
568
+ promptForSelector(dlg) {
569
+ let ms = '',
570
+ md = this.new_selector_modal;
571
+ if(dlg === 'rename') {
572
+ if(this.selected_selector) ms = this.selected_selector;
573
+ md = this.rename_selector_modal;
574
+ }
575
+ md.element('type').innerText = 'boundline selector';
576
+ md.element('name').value = ms;
577
+ md.show('name');
578
+ }
579
+
580
+ newSelector() {
581
+ if(!this.selected) return;
582
+ const md = this.new_selector_modal;
583
+ // NOTE: Selector modal is also used by constraint editor.
584
+ if(md.element('type').innerText !== 'boundline selector') return;
585
+ const
586
+ bl = this.selected,
587
+ sel = md.element('name').value.trim(),
588
+ bls = bl.addSelector(sel);
589
+ if(bls) {
590
+ this.selected_selector = bls.selector;
591
+ // NOTE: Update dimensions only if boundline now has 2 or more
592
+ // selectors.
593
+ const sl = bl.selectorList;
594
+ if(sl.length > 1) MODEL.expandDimension(sl);
595
+ md.hide();
596
+ this.updateSelectorTable();
597
+ }
598
+ }
599
+
600
+ renameSelector() {
601
+ if(!this.selected) return;
602
+ const md = this.rename_selector_modal;
603
+ // NOTE: Selector modal is also used by constraint editor.
604
+ if(md.element('type').innerText !== 'boundline selector') return;
605
+ const
606
+ bl = this.selected,
607
+ sel = MODEL.validSelector(md.element('name').value.trim()),
608
+ bls = bl.selectorByName(this.selected_selector);
609
+ if(bls && sel) {
610
+ bls.selector = sel;
611
+ bl.selectors.sort((a, b) => compareSelectors(a.selector, b.selector));
612
+ }
613
+ md.hide();
614
+ this.updateSelectorTable();
615
+ }
616
+
617
+ editExpression() {
618
+ if(!this.selected) return;
619
+ const
620
+ bl = this.selected,
621
+ bls = bl.selectorByName(this.selected_selector);
622
+ if(bls) {
623
+ this.edited_expression = bls.expression;
624
+ const md = UI.modals.expression;
625
+ md.element('property').innerHTML = 'boundline selector';
626
+ md.element('text').value = bls.expression.text;
627
+ document.getElementById('variable-obj').value = 0;
628
+ X_EDIT.updateVariableBar();
629
+ X_EDIT.clearStatusBar();
630
+ md.show('text');
631
+ }
632
+ }
633
+
634
+ modifyExpression(x, grouping) {
635
+ // Update boundline index expression.
636
+ if(!this.selected) return;
637
+ const
638
+ bl = this.selected,
639
+ bls = bl.selectorByName(this.selected_selector);
640
+ if(!bls) return;
641
+ const blsx = bls.expression;
642
+ // Double-check that selector expression is indeed being edited.
643
+ if(blsx !== this.edited_expression) {
644
+ console.log('ERROR: boundline selector expression mismatch',
645
+ x, bls, this.edited_expression);
646
+ return;
647
+ }
648
+ bls.grouping = grouping;
649
+ // Update and compile expression only if it has been changed.
650
+ if(x != blsx.text) {
651
+ blsx.text = x;
652
+ blsx.compile();
653
+ if(grouping && blsx.isStatic) {
654
+ // Check whether the point coordinates are valid.
655
+ const
656
+ r1 = blsx.result(1),
657
+ r2 = r1.slice();
658
+ bls.boundline.validatePoints(r2);
659
+ if(r1.join(';') !== r2.join(';')) {
660
+ UI.warn('Points expression for <tt>' + bls.selector +
661
+ '</tt> will evaluate as ' + r2.join('; '));
662
+ }
663
+ }
664
+ }
665
+ // Clear expression results, just to be neat.
666
+ blsx.reset();
667
+ // Clear the `selected_expression` property of the constraint editor.
668
+ this.edited_expression = null;
669
+ this.updateSelectorTable();
670
+ }
671
+
672
+ deleteSelector() {
673
+ // Delete modifier from selected dataset
674
+ if(!this.selected) return;
675
+ const
676
+ bl = this.selected,
677
+ bls = this.selected.selectorByName(this.selected_selector);
678
+ if(bls && bls.selector) {
679
+ // If it is not the default selector, simply remove it from the list.
680
+ const i = bl.selectors.indexOf(bls);
681
+ if(i > 0) bl.selectors.splice(i, 1);
682
+ this.selected_selector = false;
683
+ this.updateSelectorTable();
684
+ MODEL.updateDimensions();
685
+ }
686
+ }
687
+
688
+ updateSelectorTable() {
689
+ this.boundline_modal.element('sel-table')
690
+ .innerHTML = this.boundLineSelectorTable;
691
+ }
692
+
693
+ showBoundLineModal() {
694
+ // Open modal to modify data properties of selected boundline.
695
+ if(!this.selected) return;
696
+ // Ensure that bound line does not have a selected or dragged point.
697
+ this.on_point = -1;
698
+ this.dragged_point = -1;
699
+ this.selected_point = -1;
700
+ const
701
+ bl = this.selected,
702
+ md = this.boundline_modal;
703
+ md.element('url').value = bl.url;
704
+ this.updateSelectorTable();
705
+ this.stopEditing();
706
+ md.show();
707
+ }
708
+
709
+ showDefaultBoundLine() {
710
+ // Restore and redraw default bound line.
711
+ if(this.selected) this.selected.restorePoints();
712
+ this.draw();
713
+ }
714
+
715
+ updateBoundLineProperties() {
716
+ // Change experiment run selectors of selected boundline.
373
717
  if(this.selected) {
374
- const sel = this.bl_selectors.value.replace(
375
- /[\;\,]/g, ' ').trim().replace(
376
- /[^a-zA-Z0-9\+\-\%\_\s]/g, '').split(/\s+/).join(' ');
377
- this.selected.selectors = sel;
378
- this.bl_selectors.value = sel;
379
- this.draw();
718
+ const
719
+ bl = this.selected,
720
+ md = this.boundline_modal;
721
+ bl.url = md.element('url').value;
722
+ this.showDefaultBoundLine();
723
+ md.hide();
380
724
  }
381
725
  }
382
726
 
727
+ showDataBoundLine(index) {
728
+ // Redraw diagram with selected boundline now having its points based
729
+ // on data for the given index.
730
+ if(!this.selected) return;
731
+ const bl = this.selected;
732
+ bl.setPointsFromData(index);
733
+ this.draw();
734
+ bl.restorePoints();
735
+ }
736
+
383
737
  changeShareOfCost() {
384
738
  // Validates input of share-of-cost field
385
739
  const soc = UI.validNumericInput('constraint-share-of-cost', 'share of cost');
@@ -485,6 +839,8 @@ class ConstraintEditor {
485
839
  positionPoint() {
486
840
  // Prompt modeler for precise point coordinates.
487
841
  if(this.selected_point < 0) return;
842
+ // Prevent that "drag point" state persists after ESC o cancel.
843
+ this.dragged_point = -1;
488
844
  const
489
845
  md = this.point_modal,
490
846
  pc = this.point_div.innerHTML.split(', ');
@@ -531,7 +887,7 @@ class ConstraintEditor {
531
887
  l = this.selected,
532
888
  pi = this.dragged_point,
533
889
  lpi = l.points.length - 1;
534
- // Check -- just in case
890
+ // Check -- just in case.
535
891
  if(!l || pi < 0 || pi > lpi) return;
536
892
  let p = l.points[pi],
537
893
  px = p[0],
@@ -540,22 +896,23 @@ class ConstraintEditor {
540
896
  maxx = (pi === 0 ? 0 : (pi === lpi ? 100 : l.points[pi + 1][0])),
541
897
  newx = Math.min(maxx, Math.max(minx, x)),
542
898
  newy = Math.min(100, Math.max(0, y));
543
- // No action needed unless point has been moved
899
+ // No action needed unless point has been moved.
544
900
  if(newx !== px || newy !== py) {
545
901
  p[0] = newx;
546
902
  p[1] = newy;
903
+ l.storePoints();
547
904
  this.draw();
548
905
  this.updateEquation();
549
906
  }
550
907
  }
551
908
 
552
909
  updateStatus() {
553
- // Displays cursor position as X and Y (in chart coordinates), and updates
554
- // controls.
910
+ // Display cursor position as X and Y (in chart coordinates), and
911
+ // update controls.
555
912
  this.pos_x_div.innerHTML = 'X = ' + this.pos_x.toPrecision(3);
556
913
  this.pos_y_div.innerHTML = 'Y = ' + this.pos_y.toPrecision(3);
557
914
  this.point_div.innerHTML = '';
558
- const blbtns = 'add-point del-bl';
915
+ const blbtns = 'add-point bl-data del-bl';
559
916
  if(this.selected) {
560
917
  if(this.selected_point >= 0) {
561
918
  const p = this.selected.points[this.selected_point];
@@ -568,26 +925,20 @@ class ConstraintEditor {
568
925
  .replace(/\.$/, '');
569
926
  this.point_div.innerHTML = `(${px}, ${py})`;
570
927
  }
571
- // Check whether selected point is an end point
928
+ // Check whether selected point is an end point.
572
929
  const ep = this.selected_point === 0 ||
573
930
  this.selected_point === this.selected.points.length - 1;
574
- // If so, do not allow deletion
931
+ // If so, do not allow deletion.
575
932
  UI.enableButtons(blbtns + (ep ? '' : ' del-point'));
576
933
  if(this.adding_point) this.add_point_btn.classList.add('activ');
577
934
  this.bl_type.value = this.selected.type;
578
935
  this.bl_type.style.color = 'black';
579
936
  this.bl_type.disabled = false;
580
- this.bl_selectors.value = this.selected.selectors;
581
- this.bl_selectors.style.backgroundColor = 'white';
582
- this.bl_selectors.disabled = false;
583
937
  } else {
584
938
  UI.disableButtons(blbtns + ' del-point');
585
939
  this.bl_type.value = VM.EQ;
586
940
  this.bl_type.style.color = 'silver';
587
941
  this.bl_type.disabled = true;
588
- this.bl_selectors.value = '';
589
- this.bl_selectors.style.backgroundColor = 'inherit';
590
- this.bl_selectors.disabled = true;
591
942
  }
592
943
  }
593
944
 
@@ -685,11 +1036,18 @@ class ConstraintEditor {
685
1036
  '100</text>']);
686
1037
  }
687
1038
 
688
- drawContour(l) {
689
- // Draws infeasible area for bound line `l`
1039
+ setContourPath(l) {
1040
+ // Computes the contour path (which is the line path for EQ bounds)
1041
+ // without drawing them in the chart -- used when drawing thumbnails.
1042
+ this.drawContour(l, false);
1043
+ this.drawLine(l, false);
1044
+ }
1045
+
1046
+ drawContour(l, display=true) {
1047
+ // Draws infeasible area for bound line `l`.
690
1048
  let cp;
691
1049
  if(l.type === VM.EQ) {
692
- // Whole area is infeasible except for the bound line itself
1050
+ // Whole area is infeasible except for the bound line itself.
693
1051
  cp = ['M', this.point(0, 0), 'L', this.point(100 ,0), 'L',
694
1052
  this.point(100, 100), 'L', this.point(0, 100), 'z'].join('');
695
1053
  } else {
@@ -700,9 +1058,10 @@ class ConstraintEditor {
700
1058
  cp += `L${this.point(p[0], p[1])}`;
701
1059
  }
702
1060
  cp += 'L' + this.point(100, base_y) + 'z';
1061
+ // Save the contour for rapid display of thumbnails.
1062
+ l.contour_path = cp;
703
1063
  }
704
- // Save the contour for rapid display of thumbnails
705
- l.contour_path = cp;
1064
+ if(!display) return;
706
1065
  // NOTE: the selected bound lines have their infeasible area filled
707
1066
  // with a *colored* line pattern
708
1067
  const sel = l === this.selected;
@@ -711,7 +1070,7 @@ class ConstraintEditor {
711
1070
  (sel ? 1 : 0.4), '"></path>']);
712
1071
  }
713
1072
 
714
- drawLine(l) {
1073
+ drawLine(l, display=true) {
715
1074
  let color,
716
1075
  width,
717
1076
  pp = [],
@@ -741,6 +1100,7 @@ class ConstraintEditor {
741
1100
  // For EQ bound lines, the line path is the contour; this will be
742
1101
  // drawn in miniature black against a silver background
743
1102
  if(l.type === VM.EQ) l.contour_path = cp;
1103
+ if(!display) return;
744
1104
  this.addSVG(['<path fill="none" stroke="', color, '" d="', cp,
745
1105
  '" stroke-width="', width, '"></path>', dots]);
746
1106
  }