linny-r 2.0.12 → 2.1.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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "linny-r",
3
- "version": "2.0.12",
3
+ "version": "2.1.1",
4
4
  "description": "Executable graphical language with WYSIWYG editor for MILP models",
5
5
  "main": "server.js",
6
6
  "scripts": {
Binary file
package/static/index.html CHANGED
@@ -914,6 +914,8 @@ NOTE: Products directly linked to such processes should have a proportional unit
914
914
  title="Include module in current model">
915
915
  <img id="repo-load-btn" class="btn disab" src="images/open.png"
916
916
  title="Load model from repository">
917
+ <img id="repo-update-btn" class="btn disab" src="images/update.png"
918
+ title="Update clusters that have been included in the model">
917
919
  <img id="repo-store-btn" class="btn enab" src="images/store.png"
918
920
  title="Store model as module in repository">
919
921
  <img id="repo-black-box-btn" class="btn enab" src="images/black-box.png"
@@ -1061,9 +1063,6 @@ NOTE: Products directly linked to such processes should have a proportional unit
1061
1063
  <img class="cancel-btn" src="images/cancel.png">
1062
1064
  <img class="ok-btn" src="images/ok.png">
1063
1065
  </div>
1064
- <div style="margin:2px; background-color: Yellow">
1065
- <strong>NOTE:</strong> <em>This action cannot be undone!</em>
1066
- </div>
1067
1066
  <div style="margin:2px">
1068
1067
  <label>Cluster:</label>
1069
1068
  <input id="include-prefix" type="text"
@@ -1082,6 +1081,38 @@ NOTE: Products directly linked to such processes should have a proportional unit
1082
1081
  </div>
1083
1082
  </div>
1084
1083
 
1084
+ <!-- the UPDATE dialog prompts for a module name -->
1085
+ <div id="update-modal" class="modal">
1086
+ <div id="update-dlg" class="inp-dlg" style="min-width: 320px">
1087
+ <div class="dlg-title">
1088
+ Update clusters previously included from a module
1089
+ <img class="cancel-btn" src="images/cancel.png">
1090
+ <img class="ok-btn" src="images/ok.png">
1091
+ </div>
1092
+ <div style="padding: 2px">
1093
+ <div>
1094
+ Update using module: <span id="update-name"></span>
1095
+ </div>
1096
+ <div>
1097
+ <label>Clusters based on:</label>
1098
+ <select id="update-module">
1099
+ </select>
1100
+ <div id="update-count" class="update-cc"></div>
1101
+ </div>
1102
+ <div id="update-issues">
1103
+ <div id="update-issues-header"></div>
1104
+ <div id="update-issues-area"></div>
1105
+ </div>
1106
+ <div id="update-remove">
1107
+ <div id="update-remove-header">
1108
+ <span id="update-remove-count"></span> will be removed
1109
+ </div>
1110
+ <div id="update-remove-area"></div>
1111
+ </div>
1112
+ </div>
1113
+ </div>
1114
+ </div>
1115
+
1085
1116
  <!-- the CONFIRM LOAD modal asks to confirm to load model from repository -->
1086
1117
  <div id="confirm-load-from-repo-modal" class="modal">
1087
1118
  <div id="confirm-load-from-repo-dlg" class="inp-dlg">
@@ -607,6 +607,11 @@ span.node-details {
607
607
  margin-left: 15px;
608
608
  }
609
609
 
610
+ span.mod-name {
611
+ font-style: normal;
612
+ color: #403080;
613
+ }
614
+
610
615
  #issue-panel {
611
616
  display: none;
612
617
  background-color: Yellow;
@@ -707,6 +712,11 @@ img.inline-cancel-btn:hover {
707
712
  filter: brightness(120%);
708
713
  }
709
714
 
715
+ img.ok-btn.disab {
716
+ filter: saturate(0%) brightness(150%) contrast(70%);
717
+ pointer-events: none;
718
+ }
719
+
710
720
  #actor-group,
711
721
  #constraint-group,
712
722
  #cluster-group,
@@ -4152,8 +4162,9 @@ td.sa-not-run {
4152
4162
 
4153
4163
  #xp-ignore-count {
4154
4164
  position: absolute;
4155
- right: 14px;
4165
+ right: 2px;
4156
4166
  bottom: 3px;
4167
+ width: 18px;
4157
4168
  height: 16px;
4158
4169
  font-size: 9px;
4159
4170
  color: #806070;
@@ -4882,6 +4893,7 @@ span.sd-clear {
4882
4893
  }
4883
4894
 
4884
4895
  #paste-dlg,
4896
+ #update-dlg,
4885
4897
  #include-dlg {
4886
4898
  width: 320px;
4887
4899
  height: min-content;
@@ -4907,9 +4919,29 @@ span.sd-clear {
4907
4919
  margin-left: 9px;
4908
4920
  }
4909
4921
 
4922
+ #update-name {
4923
+ font-family: monospace;
4924
+ font-size: 14px;
4925
+ }
4926
+
4927
+ div.update-cc {
4928
+ display: inline-block;
4929
+ margin-left: 6px;
4930
+ font-style: italic;
4931
+ color: Gray;
4932
+ }
4933
+
4934
+ #update-remove-header,
4935
+ #update-issues-header {
4936
+ font-weight: bold;
4937
+ margin: 4px 2px 2px 2px;
4938
+ }
4939
+
4910
4940
  #paste-scroll-area,
4911
- #include-scroll-area {
4912
- margin: 2px;
4941
+ #include-scroll-area,
4942
+ #update-remove-area,
4943
+ #update-issues-area {
4944
+ margin: 0 2px;
4913
4945
  height: min-content;
4914
4946
  max-height: 350px !important;
4915
4947
  width: calc(100% - 4px);
@@ -4918,6 +4950,13 @@ span.sd-clear {
4918
4950
  overflow-y: auto;
4919
4951
  }
4920
4952
 
4953
+ #update-remove-area,
4954
+ #update-issues-area {
4955
+ max-height: 150px !important;
4956
+ background-color: white;
4957
+ border: 1px solid Silver;
4958
+ }
4959
+
4921
4960
  div.paste-tactic {
4922
4961
  display: inline-block;
4923
4962
  vertical-align: top;
@@ -311,6 +311,12 @@ class Controller {
311
311
  (name.startsWith(this.BLACK_BOX) || name[0].match(/[\w]/));
312
312
  }
313
313
 
314
+ realActorName(name) {
315
+ // Return `name` unless it is '(no actor)'; then return empty string.
316
+ if(name === this.NO_ACTOR) return '';
317
+ return name;
318
+ }
319
+
314
320
  prefixesAndName(name, key=false) {
315
321
  // Returns name split exclusively at '[non-space]: [non-space]'
316
322
  let sep = this.PREFIXER,
@@ -224,10 +224,10 @@ class ActorManager {
224
224
  }
225
225
 
226
226
  showEditActorDialog(name, expr) {
227
- // Display modal for editing properties of one actor
227
+ // Display modal for editing properties of one actor.
228
228
  this.actor_span.innerHTML = name;
229
229
  this.actor_name.value = name;
230
- // Do not allow modification of the name '(no actor)'
230
+ // Do not allow modification of the name '(no actor)'.
231
231
  if(name === UI.NO_ACTOR) {
232
232
  this.actor_name.disabled = true;
233
233
  this.actor_io.style.display = 'none';
@@ -242,22 +242,22 @@ class ActorManager {
242
242
 
243
243
  modifyActorEntry() {
244
244
  // This method is called when the modeler submits the "actor properties"
245
- // dialog
245
+ // dialog.
246
246
  let n = this.actor_span.innerHTML,
247
247
  nn = UI.NO_ACTOR,
248
248
  x = this.actor_weight.value.trim(),
249
249
  xp = new ExpressionParser(x);
250
250
  if(n !== UI.NO_ACTOR) {
251
251
  nn = this.actor_name.value.trim();
252
- // NOTE: prohibit colons in actor names to avoid confusion with
253
- // prefixed entities
252
+ // NOTE: Prohibit colons in actor names to avoid confusion with
253
+ // prefixed entities.
254
254
  if(!UI.validName(nn) || nn.indexOf(':') >= 0) {
255
255
  UI.warn(UI.WARNING.INVALID_ACTOR_NAME);
256
256
  return false;
257
257
  }
258
258
  }
259
259
  if(xp.error) {
260
- // NOTE: do not pass the actor, as its name is being edited as well
260
+ // NOTE: Do not pass the actor, as its name is being edited as well.
261
261
  UI.warningInvalidWeightExpression(null, xp.error);
262
262
  return false;
263
263
  }
@@ -112,6 +112,10 @@ class GUIChartManager extends ChartManager {
112
112
  this.svg_container.addEventListener(
113
113
  'mouseleave', (event) => CHART_MANAGER.updateTimeStep(event, false));
114
114
  this.time_step = document.getElementById('chart-time-step');
115
+ this.prefix_div = document.getElementById('chart-prefix-div');
116
+ this.prefix_selector = document.getElementById('chart-prefix');
117
+ this.prefix_selector.addEventListener(
118
+ 'change', () => CHART_MANAGER.selectPrefix());
115
119
  document.getElementById('chart-toggle-chevron').addEventListener(
116
120
  'click', () => CHART_MANAGER.toggleControlPanel());
117
121
  document.getElementById('chart-stats-btn').addEventListener(
@@ -354,8 +358,9 @@ class GUIChartManager extends ChartManager {
354
358
  }
355
359
 
356
360
  updateDialog() {
357
- // Refreshe all dialog fields to display actual MODEL chart properties.
361
+ // Refresh all dialog fields to display actual MODEL chart properties.
358
362
  this.updateSelector();
363
+ this.prefix_div.style.display = 'none';
359
364
  let c = null;
360
365
  if(this.chart_index >= 0) {
361
366
  c = MODEL.charts[this.chart_index];
@@ -390,6 +395,19 @@ class GUIChartManager extends ChartManager {
390
395
  '</td></tr>'].join(''));
391
396
  }
392
397
  this.variables_table.innerHTML = ol.join('');
398
+ const
399
+ cp = c.prefix,
400
+ pp = c.possiblePrefixes,
401
+ html = [];
402
+ if(pp.length) {
403
+ for(const p of pp) {
404
+ const cap = capitalized(p);
405
+ html.push('<option value="', cap, '"', (cap === cp ? ' selected' : ''), '>',
406
+ cap, '</option>');
407
+ }
408
+ this.prefix_div.style.display = 'inline-block';
409
+ }
410
+ this.prefix_selector.innerHTML = html.join('');
393
411
  } else {
394
412
  this.variable_index = -1;
395
413
  }
@@ -439,6 +457,15 @@ class GUIChartManager extends ChartManager {
439
457
  this.stretchChart(0);
440
458
  }
441
459
 
460
+ selectPrefix() {
461
+ // Set the preferred prefix for this chart. This will override the
462
+ // title prefix (if any).
463
+ if(this.chart_index >= 0) {
464
+ MODEL.charts[this.chart_index].preferred_prefix = this.prefix_selector.value;
465
+ }
466
+ this.updateDialog();
467
+ }
468
+
442
469
  showSortingMenu() {
443
470
  // Show the pane with sort type buttons only if variable is selected.
444
471
  this.sorting_menu.style.display =
@@ -594,27 +621,31 @@ class GUIChartManager extends ChartManager {
594
621
 
595
622
  cloneChart() {
596
623
  // Create a new chart that is identical to the current one.
597
- if(this.chart_index >= 0) {
598
- let c = MODEL.charts[this.chart_index],
599
- nt = c.title + '-copy';
600
- while(MODEL.indexOfChart(nt) >= 0) {
601
- nt += '-copy';
602
- }
603
- const nc = MODEL.addChart(nt);
604
- // Copy properties of `c` to `nc`;
605
- nc.histogram = c.histogram;
606
- nc.bins = c.bins;
607
- nc.show_title = c.show_title;
608
- nc.legend_position = c.legend_position;
609
- for(const cv of c.variables) {
610
- const nv = new ChartVariable(nc);
611
- nv.setProperties(cv.object, cv.attribute, cv.stacked,
612
- cv.color, cv.scale_factor, cv.line_width, cv.sorted);
613
- nc.variables.push(nv);
614
- }
615
- this.chart_index = MODEL.indexOfChart(nc.title);
616
- this.updateDialog();
617
- }
624
+ if(this.chart_index < 0) return;
625
+ const
626
+ c = MODEL.charts[this.chart_index],
627
+ pp = c.possiblePrefixes;
628
+ let nt = c.title;
629
+ if(pp) {
630
+ // Remove title prefix (if any), and add selected one.
631
+ nt = c.prefix + UI.PREFIXER + nt.split(UI.PREFIXER).pop();
632
+ }
633
+ // If title is not new, keep adding a suffix until it is new.
634
+ while(MODEL.indexOfChart(nt) >= 0) nt += '-copy';
635
+ const nc = MODEL.addChart(nt);
636
+ // Copy properties of `c` to `nc`;
637
+ nc.histogram = c.histogram;
638
+ nc.bins = c.bins;
639
+ nc.show_title = c.show_title;
640
+ nc.legend_position = c.legend_position;
641
+ for(const cv of c.variables) {
642
+ const nv = new ChartVariable(nc);
643
+ nv.setProperties(cv.object, cv.attribute, cv.stacked,
644
+ cv.color, cv.scale_factor, cv.line_width, cv.sorted);
645
+ nc.variables.push(nv);
646
+ }
647
+ this.chart_index = MODEL.indexOfChart(nc.title);
648
+ this.updateDialog();
618
649
  }
619
650
 
620
651
  toggleRunResults() {
@@ -637,13 +668,7 @@ class GUIChartManager extends ChartManager {
637
668
  deleteChart() {
638
669
  // Delete the shown chart (if any).
639
670
  if(this.chart_index >= 0) {
640
- // NOTE: Do not delete the default chart, but clear it instead.
641
- if(MODEL.charts[this.chart_index].title === this.new_chart_title) {
642
- MODEL.charts[this.chart_index].reset();
643
- } else {
644
- MODEL.charts.splice(this.chart_index, 1);
645
- this.chart_index = -1;
646
- }
671
+ MODEL.deleteChart(this.chart_index);
647
672
  // Also update the experiment viewer, because this chart may be
648
673
  // one of the output charts of the selected experiment.
649
674
  UI.updateControllerDialogs('CFX');
@@ -742,9 +767,6 @@ class GUIChartManager extends ChartManager {
742
767
  if(indices.length < 2) {
743
768
  if(indices.length) {
744
769
  chart.addWildcardVariables(dsm, indices);
745
- } else if(dsm.selector.startsWith(':')) {
746
- UI.notify('Plotting methods is work-in-progress!');
747
- console.log('HERE dsm', dsm, 'expr', dsm.expression.text, 'indices', indices);
748
770
  } else {
749
771
  UI.notify(`Variable "${dsm.displayName}" cannot be plotted`);
750
772
  }
@@ -726,7 +726,7 @@ class GUIController extends Controller {
726
726
  this.dbl_clicked_node = null;
727
727
  this.target_cluster = null;
728
728
  this.constraint_under_cursor = null;
729
- this.last_up_down_without_move = Date.now();
729
+ this.last_up_down_without_move = {up: 0, down: 0};
730
730
  // Keyboard shortcuts: Ctrl-x associates with menu button ID.
731
731
  this.shortcuts = {
732
732
  'A': 'actors',
@@ -1672,15 +1672,15 @@ class GUIController extends Controller {
1672
1672
  }
1673
1673
  }
1674
1674
 
1675
- get doubleClicked() {
1676
- // Return TRUE when a "double-click" occurred
1675
+ doubleClicked(ud) {
1676
+ // Return TRUE when a "double-click" occurred.
1677
1677
  const
1678
1678
  now = Date.now(),
1679
- dt = now - this.last_up_down_without_move;
1680
- this.last_up_down_without_move = now;
1679
+ dt = now - this.last_up_down_without_move[ud];
1680
+ this.last_up_down_without_move[ud] = now;
1681
1681
  // Consider click to be "double" if it occurred less than 300 ms ago
1682
1682
  if(dt < 300) {
1683
- this.last_up_down_without_move = 0;
1683
+ this.last_up_down_without_move[ud] = 0;
1684
1684
  return true;
1685
1685
  }
1686
1686
  return false;
@@ -2230,7 +2230,9 @@ class GUIController extends Controller {
2230
2230
  }
2231
2231
  // When dragging a selection over a cluster, change cursor to "cell" to
2232
2232
  // indicate that selected process(es) will be moved into the cluster.
2233
- if(this.dragged_node) {
2233
+ // NOTE: Do not do this when the dragged selection is just a single note!
2234
+ if(this.dragged_node &&
2235
+ !(this.dragged_node instanceof Note && MODEL.selection.length < 2)) {
2234
2236
  // NOTE: Cursor will always be over the dragged node, so do not indicate
2235
2237
  // "drop here?" unless dragged over a different cluster.
2236
2238
  if(this.on_cluster && this.on_cluster !== this.dragged_node) {
@@ -2312,14 +2314,8 @@ class GUIController extends Controller {
2312
2314
  return;
2313
2315
  } // END IF Ctrl
2314
2316
 
2315
- // Clear selection unless SHIFT pressed or mouseDown while hovering
2316
- // over a SELECTED node or link.
2317
- if(!(e.shiftKey ||
2318
- (this.on_node && MODEL.selection.indexOf(this.on_node) >= 0) ||
2319
- (this.on_cluster && MODEL.selection.indexOf(this.on_cluster) >= 0) ||
2320
- (this.on_note && MODEL.selection.indexOf(this.on_note) >= 0) ||
2321
- (this.on_link && MODEL.selection.indexOf(this.on_link) >= 0) ||
2322
- (this.on_constraint && MODEL.selection.indexOf(this.on_constraint) >= 0))) {
2317
+ // Clear selection unless SHIFT pressed or double-clicking.
2318
+ if(!(this.doubleClicked('down') || e.shiftKey)) {
2323
2319
  MODEL.clearSelection();
2324
2320
  UI.drawDiagram(MODEL);
2325
2321
  }
@@ -2538,7 +2534,7 @@ class GUIController extends Controller {
2538
2534
  absdx = Math.abs(this.net_move_x),
2539
2535
  absdy = Math.abs(this.net_move_y),
2540
2536
  sigmv = (MODEL.align_to_grid ? MODEL.grid_pixels / 4 : 2.5);
2541
- if(this.doubleClicked) {
2537
+ if(this.doubleClicked('up')) {
2542
2538
  // Ignore insignificant move.
2543
2539
  if(absdx < sigmv && absdy < sigmv) {
2544
2540
  // Undo the move and remove the action from the UNDO-stack.
@@ -2584,13 +2580,15 @@ class GUIController extends Controller {
2584
2580
  this.paper.container.style.cursor = 'pointer';
2585
2581
  // NOTE: Cursor will always be over the selected cluster (while dragging).
2586
2582
  if(this.on_cluster && !this.on_cluster.selected) {
2587
- UNDO_STACK.push('drop', this.on_cluster);
2588
- MODEL.dropSelectionIntoCluster(this.on_cluster);
2589
- this.on_node = null;
2590
- this.on_note = null;
2591
- this.target_cluster = null;
2592
- // Redraw cluster to erase its orange "target corona".
2593
- UI.paper.drawCluster(this.on_cluster);
2583
+ if(!(this.dragged_node instanceof Note && MODEL.selection.length < 2)) {
2584
+ UNDO_STACK.push('drop', this.on_cluster);
2585
+ MODEL.dropSelectionIntoCluster(this.on_cluster);
2586
+ // Redraw cluster to erase its orange "target corona".
2587
+ UI.paper.drawCluster(this.on_cluster);
2588
+ this.on_node = null;
2589
+ this.on_note = null;
2590
+ this.target_cluster = null;
2591
+ }
2594
2592
  }
2595
2593
  // Only now align to grid.
2596
2594
  MODEL.alignToGrid();
@@ -2599,11 +2597,11 @@ class GUIController extends Controller {
2599
2597
 
2600
2598
  // Then check whether the user is clicking on a link.
2601
2599
  } else if(this.on_link) {
2602
- if(this.doubleClicked) {
2600
+ if(this.doubleClicked('up')) {
2603
2601
  this.showLinkPropertiesDialog(this.on_link);
2604
2602
  }
2605
2603
  } else if(this.on_constraint) {
2606
- if(this.doubleClicked) {
2604
+ if(this.doubleClicked('up')) {
2607
2605
  this.showConstraintPropertiesDialog(this.on_constraint);
2608
2606
  }
2609
2607
  }
@@ -2979,7 +2977,7 @@ class GUIController extends Controller {
2979
2977
  //
2980
2978
 
2981
2979
  validNames(nn, an='') {
2982
- // Check whether names meet conventions; if not, warn user
2980
+ // Check whether names meet conventions. If not, warn user.
2983
2981
  if(!UI.validName(nn) || nn.indexOf(UI.BLACK_BOX) >= 0) {
2984
2982
  this.warningInvalidName(nn);
2985
2983
  return false;
@@ -3148,10 +3146,10 @@ class GUIController extends Controller {
3148
3146
  UI.info_line.classList.remove(...UI.info_line.classList);
3149
3147
  }
3150
3148
 
3151
- setMessage(msg, type=null) {
3149
+ setMessage(msg, type=null, cause=null) {
3152
3150
  // Display `msg` on infoline unless no type (= plain text) and some
3153
3151
  // info, warning or error message is already displayed.
3154
- super.setMessage(msg, type);
3152
+ super.setMessage(msg, type, cause);
3155
3153
  const types = ['notification', 'warning', 'error'];
3156
3154
  let d = new Date(),
3157
3155
  t = d.getTime(),
@@ -3666,7 +3664,7 @@ class GUIController extends Controller {
3666
3664
  // proceed to paste.
3667
3665
  const
3668
3666
  md = this.paste_modal,
3669
- mapping = Object.assign(md.mapping, {}),
3667
+ mapping = Object.assign({}, md.mapping),
3670
3668
  tc = (mapping.top_clusters ?
3671
3669
  Object.keys(mapping.top_clusters).sort(ciCompare) : []),
3672
3670
  ft = (mapping.from_to ?
@@ -3713,6 +3711,14 @@ class GUIController extends Controller {
3713
3711
 
3714
3712
  // AUXILIARY FUNCTIONS
3715
3713
 
3714
+ function namedObjects() {
3715
+ // Return TRUE iff XML contains named objects.
3716
+ for(const cn of entities_node.childNodes) {
3717
+ if(cn.nodeName !== 'note') return true;
3718
+ }
3719
+ return false;
3720
+ }
3721
+
3716
3722
  function fullName(node) {
3717
3723
  // Return full entity name inferred from XML node data.
3718
3724
  if(node.nodeName === 'from-to' || node.nodeName === 'selc') {
@@ -3848,7 +3854,18 @@ class GUIController extends Controller {
3848
3854
  fn = fullName(node),
3849
3855
  mn = mappedName(fn);
3850
3856
  let obj;
3851
- if(et === 'process' && !MODEL.processByID(UI.nameToID(mn))) {
3857
+ if(et === 'note') {
3858
+ // Ensure that copy had new time stamp.
3859
+ let cn = childNodeByTag(node, 'timestamp').firstChild;
3860
+ cn.nodeValue = new Date().getTime().toString();
3861
+ cn = childNodeByTag(node, 'x-coord').firstChild;
3862
+ // Move note a bit right and down.
3863
+ cn.nodeValue = (safeStrToInt(cn.nodeValue, 0) + 12).toString();
3864
+ cn = childNodeByTag(node, 'y-coord').firstChild;
3865
+ cn.nodeValue = (safeStrToInt(cn.nodeValue, 0) + 12).toString();
3866
+ obj = MODEL.addNote(node);
3867
+ if(obj) new_entities.push(obj);
3868
+ } else if(et === 'process' && !MODEL.processByID(UI.nameToID(mn))) {
3852
3869
  const
3853
3870
  na = nameAndActor(mn),
3854
3871
  new_actor = !MODEL.actorByID(UI.nameToID(na[1]));
@@ -3908,12 +3925,13 @@ class GUIController extends Controller {
3908
3925
  mapping.shared_prefix = sp;
3909
3926
  mapping.from_prefix = (fpn ? sp + fpn + UI.PREFIXER : sp);
3910
3927
  mapping.to_prefix = (tpn ? sp + tpn + UI.PREFIXER : sp);
3911
- mapping.from_actor = (ca === UI.NO_ACTOR ? '' : ca);
3912
- mapping.to_actor = (fca === UI.NO_ACTOR ? '' : fca);
3928
+ mapping.from_actor = UI.realActorName(ca);
3929
+ mapping.to_actor = UI.realActorName(fca);
3913
3930
  // Prompt for mapping when pasting to the same model and cluster.
3914
3931
  if(parseInt(mts) === MODEL.time_created.getTime() &&
3915
3932
  ca === fca && mapping.from_prefix === mapping.to_prefix &&
3916
- !(mapping.prefix || mapping.actor || mapping.increment)) {
3933
+ !(mapping.prefix || mapping.actor || mapping.increment) &&
3934
+ namedObjects()) {
3917
3935
  // Prompt for names of selected cluster nodes.
3918
3936
  if(selc_node.childNodes.length && !mapping.prefix) {
3919
3937
  mapping.top_clusters = {};
@@ -4546,8 +4564,7 @@ console.log('HERE name conflicts', name_conflicts, mapping);
4546
4564
  md.group = group;
4547
4565
  md.element('action').innerText = 'Edit';
4548
4566
  md.element('name').value = c.name;
4549
- md.element('actor').value = (c.actor.name == UI.NO_ACTOR ?
4550
- '' : c.actor.name);
4567
+ md.element('actor').value = UI.realActorName(c.actor.name);
4551
4568
  md.element('options').style.display = 'block';
4552
4569
  this.setBox('cluster-collapsed', c.collapsed);
4553
4570
  this.setBox('cluster-ignore', c.ignore);
@@ -289,17 +289,6 @@ class GUIDatasetManager extends DatasetManager {
289
289
  return null;
290
290
  }
291
291
 
292
- datasetsByPrefix(prefix) {
293
- // Return the list of datasets having the specified prefix.
294
- const
295
- pid = UI.nameToID(prefix + UI.PREFIXER),
296
- dsl = [];
297
- for(const k of Object.keys(MODEL.datasets)) {
298
- if(k.startsWith(pid)) dsl.push(k);
299
- }
300
- return dsl;
301
- }
302
-
303
292
  selectPrefixRow(e) {
304
293
  // Select expand/collapse prefix row.
305
294
  this.focal_table = this.dataset_table;
@@ -310,7 +299,7 @@ class GUIDatasetManager extends DatasetManager {
310
299
  const toggle = r.classList.contains('tree-btn');
311
300
  while(r.tagName !== 'TR') r = r.parentNode;
312
301
  this.selected_prefix_row = r;
313
- this.prefixed_datasets = this.datasetsByPrefix(r.dataset.prefix);
302
+ this.prefixed_datasets = MODEL.datasetKeysByPrefix(r.dataset.prefix);
314
303
  const sel = this.dataset_table.getElementsByClassName('sel-set');
315
304
  this.selected_dataset = null;
316
305
  if(sel.length > 0) {
@@ -746,7 +735,7 @@ class GUIDatasetManager extends DatasetManager {
746
735
  }
747
736
 
748
737
  get selectedAsList() {
749
- // Return list of datasets selected directly or by prefix.
738
+ // Return list of datasets selected directly or by prefix.
750
739
  const dsl = [];
751
740
  // Prevent including the equations dataset (just in case).
752
741
  if(this.selected_dataset && this.selected_dataset !== MODEL.equations_dataset) {
@@ -1243,12 +1232,14 @@ class GUIDatasetManager extends DatasetManager {
1243
1232
  for(let j = 0; j < ncol; j++) {
1244
1233
  const
1245
1234
  v = dsv[j].trim(),
1246
- sf = safeStrToFloat(v, '');
1247
- if(sf === '' && v !== '') {
1235
+ sf = safeStrToFloat(v, NaN);
1236
+ // NOTE: Ignore empty strings, but this may "shift up" numerical values on later rows.
1237
+ if(isNaN(sf) && v !== '') {
1248
1238
  UI.warn(`Invalid numerical value "${v}" for <strong>${dsn[j]}</strong> on line ${i}`);
1249
1239
  return false;
1240
+ } else if(!isNaN(sf)) {
1241
+ dsa[j].push(sf);
1250
1242
  }
1251
- dsa[j].push(sf);
1252
1243
  }
1253
1244
  }
1254
1245
  // Add or update datasets.
@@ -209,8 +209,9 @@ class GUIMonitor {
209
209
  `ERROR at t=${t}: ` + VM.errorMessage(err);
210
210
  for(const x of VM.call_stack) {
211
211
  // For equations, only show the attribute.
212
- const ons = (x.object === MODEL.equations_dataset ? '' :
213
- x.object.displayName + '|');
212
+ const ons = (x.object === MODEL.equations_dataset ?
213
+ (x.attribute.startsWith(':') ? x.method_object_prefix : '') :
214
+ x.object.displayName + '|');
214
215
  vlist.push(ons + x.attribute);
215
216
  // Trim spaces around all object-attribute separators in the expression.
216
217
  xlist.push(x.text.replace(/\s*\|\s*/g, '|'));
@@ -1955,7 +1955,7 @@ class Paper {
1955
1955
  let l = (MODEL.solved ? proc.actualLevel(MODEL.t) : VM.NOT_COMPUTED),
1956
1956
  lb = proc.lower_bound.result(MODEL.t),
1957
1957
  ub = (proc.equal_bounds ? lb : proc.upper_bound.result(MODEL.t));
1958
- // NOTE: by default, lower bound = 0 (but do show exceptional values)
1958
+ // NOTE: By default, lower bound = 0 (but do show exceptional values).
1959
1959
  if(lb === VM.UNDEFINED && !proc.lower_bound.defined) lb = 0;
1960
1960
  let hw,
1961
1961
  hh,
@@ -2766,6 +2766,15 @@ class Paper {
2766
2766
  'h-', w - shadow_width, 'v-', shadow_width,
2767
2767
  'h', w - 2*shadow_width, 'z'],
2768
2768
  {fill:stroke_color, stroke:stroke_color, 'stroke-width':stroke_width});
2769
+ if(clstr.module) {
2770
+ // Add three white dots at middle of bottom shade.
2771
+ const
2772
+ ely = y + hh - shadow_width / 2,
2773
+ elfill = {fill: 'white'};
2774
+ clstr.shape.addEllipse(x - 4, ely, 1, 1, elfill);
2775
+ clstr.shape.addEllipse(x, ely, 1, 1, elfill);
2776
+ clstr.shape.addEllipse(x + 4, ely, 1, 1, elfill);
2777
+ }
2769
2778
  // Set fill color if slack used by some product contained by this cluster
2770
2779
  if(MODEL.t in clstr.slack_info) {
2771
2780
  const s = clstr.slack_info[MODEL.t];