linny-r 2.0.12 → 2.1.0

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.0",
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
  }
@@ -637,13 +637,7 @@ class GUIChartManager extends ChartManager {
637
637
  deleteChart() {
638
638
  // Delete the shown chart (if any).
639
639
  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
- }
640
+ MODEL.deleteChart(this.chart_index);
647
641
  // Also update the experiment viewer, because this chart may be
648
642
  // one of the output charts of the selected experiment.
649
643
  UI.updateControllerDialogs('CFX');
@@ -2979,7 +2979,7 @@ class GUIController extends Controller {
2979
2979
  //
2980
2980
 
2981
2981
  validNames(nn, an='') {
2982
- // Check whether names meet conventions; if not, warn user
2982
+ // Check whether names meet conventions. If not, warn user.
2983
2983
  if(!UI.validName(nn) || nn.indexOf(UI.BLACK_BOX) >= 0) {
2984
2984
  this.warningInvalidName(nn);
2985
2985
  return false;
@@ -3148,10 +3148,10 @@ class GUIController extends Controller {
3148
3148
  UI.info_line.classList.remove(...UI.info_line.classList);
3149
3149
  }
3150
3150
 
3151
- setMessage(msg, type=null) {
3151
+ setMessage(msg, type=null, cause=null) {
3152
3152
  // Display `msg` on infoline unless no type (= plain text) and some
3153
3153
  // info, warning or error message is already displayed.
3154
- super.setMessage(msg, type);
3154
+ super.setMessage(msg, type, cause);
3155
3155
  const types = ['notification', 'warning', 'error'];
3156
3156
  let d = new Date(),
3157
3157
  t = d.getTime(),
@@ -3666,7 +3666,7 @@ class GUIController extends Controller {
3666
3666
  // proceed to paste.
3667
3667
  const
3668
3668
  md = this.paste_modal,
3669
- mapping = Object.assign(md.mapping, {}),
3669
+ mapping = Object.assign({}, md.mapping),
3670
3670
  tc = (mapping.top_clusters ?
3671
3671
  Object.keys(mapping.top_clusters).sort(ciCompare) : []),
3672
3672
  ft = (mapping.from_to ?
@@ -3908,8 +3908,8 @@ class GUIController extends Controller {
3908
3908
  mapping.shared_prefix = sp;
3909
3909
  mapping.from_prefix = (fpn ? sp + fpn + UI.PREFIXER : sp);
3910
3910
  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);
3911
+ mapping.from_actor = UI.realActorName(ca);
3912
+ mapping.to_actor = UI.realActorName(fca);
3913
3913
  // Prompt for mapping when pasting to the same model and cluster.
3914
3914
  if(parseInt(mts) === MODEL.time_created.getTime() &&
3915
3915
  ca === fca && mapping.from_prefix === mapping.to_prefix &&
@@ -4546,8 +4546,7 @@ console.log('HERE name conflicts', name_conflicts, mapping);
4546
4546
  md.group = group;
4547
4547
  md.element('action').innerText = 'Edit';
4548
4548
  md.element('name').value = c.name;
4549
- md.element('actor').value = (c.actor.name == UI.NO_ACTOR ?
4550
- '' : c.actor.name);
4549
+ md.element('actor').value = UI.realActorName(c.actor.name);
4551
4550
  md.element('options').style.display = 'block';
4552
4551
  this.setBox('cluster-collapsed', c.collapsed);
4553
4552
  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.
@@ -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];