linny-r 1.5.8 → 1.6.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": "1.5.8",
3
+ "version": "1.6.1",
4
4
  "description": "Executable graphical language with WYSIWYG editor for MILP models",
5
5
  "main": "server.js",
6
6
  "scripts": {
package/static/index.html CHANGED
@@ -304,8 +304,12 @@ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
304
304
  </svg>
305
305
  </div>
306
306
  </main>
307
- <!-- the footer displays the status bar with X/Y coordinates, zoom in/out buttons, buttons for moving forward/backward
308
- in time, and the status line that displays info, notifications, warnings, and error messages
307
+ <!-- The footer displays the status bar with X/Y coordinates, zoom
308
+ in/out buttons, buttons for moving forward/backward in time, and
309
+ the status line that displays info, notifications, warnings, and
310
+ error messages. The set-up progress bar shows a growing colored
311
+ needle indicating progress of making Simplex tableau and input
312
+ file for the solver.
309
313
  -->
310
314
  <footer>
311
315
  <div id="set-up-progress">
@@ -2238,7 +2242,7 @@ NOTE: * and ? will be interpreted as wildcards"
2238
2242
  </div>
2239
2243
  <div id="viewer-selectors">
2240
2244
  <label>Variable:</label>
2241
- <select id="viewer-variable" style="margin-right: 10px">
2245
+ <select id="viewer-variable">
2242
2246
  </select>
2243
2247
  <label>Statistic:</label>
2244
2248
  <select id="viewer-statistic">
@@ -2366,6 +2366,24 @@ td.equation-selector {
2366
2366
  text-overflow: ellipsis;
2367
2367
  }
2368
2368
 
2369
+ td.method {
2370
+ color: #1080e0;
2371
+ font-weight: bold;
2372
+ }
2373
+
2374
+ td.no-object {
2375
+ color: #80a0c0;
2376
+ font-weight: normal;
2377
+ }
2378
+
2379
+ td.compile-issue {
2380
+ color: #e00000;
2381
+ }
2382
+
2383
+ td.compute-issue {
2384
+ color: #f08000;
2385
+ }
2386
+
2369
2387
  td.wildcard {
2370
2388
  color: #400080;
2371
2389
  }
@@ -3558,6 +3576,7 @@ td.sa-not-run {
3558
3576
  #viewer-variable {
3559
3577
  font-size: 10px;
3560
3578
  max-width: calc(100% - 160px);
3579
+ margin-right: 10px;
3561
3580
  }
3562
3581
 
3563
3582
  #sensitivity-statistic,
@@ -304,15 +304,15 @@ class ActorManager {
304
304
  if(a.displayName != ali[1]) a.rename(ali[1]);
305
305
  // Set its round flags
306
306
  a.round_flags = ali[2];
307
- // Double-check: parse expression if weight has been changed
307
+ // Double-check: parse expression if weight has been changed.
308
308
  if(a.weight.text != ali[3]) {
309
- xp.expr = ali[3];
309
+ xp.expr = monoSpacedVariables(ali[3]);
310
310
  xp.compile();
311
311
  if(xp.error) {
312
312
  UI.warningInvalidWeightExpression(a, xp.error);
313
313
  ok = false;
314
314
  } else {
315
- a.weight.update(xp);
315
+ a.weight.update(ali[3]);
316
316
  }
317
317
  }
318
318
  // Update import/export status
@@ -574,7 +574,7 @@ class GUIChartManager extends ChartManager {
574
574
  }
575
575
 
576
576
  cloneChart() {
577
- // Creates a new chart that is identical to the current one
577
+ // Create a new chart that is identical to the current one.
578
578
  if(this.chart_index >= 0) {
579
579
  let c = MODEL.charts[this.chart_index],
580
580
  nt = c.title + '-copy';
@@ -582,7 +582,7 @@ class GUIChartManager extends ChartManager {
582
582
  nt += '-copy';
583
583
  }
584
584
  const nc = MODEL.addChart(nt);
585
- // Copy properties of c to nc
585
+ // Copy properties of `c` to `nc`;
586
586
  nc.histogram = c.histogram;
587
587
  nc.bins = c.bins;
588
588
  nc.show_title = c.show_title;
@@ -609,7 +609,7 @@ class GUIChartManager extends ChartManager {
609
609
  }
610
610
 
611
611
  toggleRunStat() {
612
- // Toggles the Boolean property that signals charts that they must
612
+ // Toggle the Boolean property that signals charts that they must
613
613
  // plot the selected statistic for the selected runs if they are
614
614
  // part of the selected experiment chart set.
615
615
  this.setRunsStat(!this.runs_stat);
@@ -618,16 +618,17 @@ class GUIChartManager extends ChartManager {
618
618
  }
619
619
 
620
620
  deleteChart() {
621
- // Deletes the shown chart (if any)
621
+ // Delete the shown chart (if any).
622
622
  if(this.chart_index >= 0) {
623
- // NOTE: do not delete the default chart, but clear it
623
+ // NOTE: Do not delete the default chart, but clear it instead.
624
624
  if(MODEL.charts[this.chart_index].title === this.new_chart_title) {
625
625
  MODEL.charts[this.chart_index].reset();
626
626
  } else {
627
627
  MODEL.charts.splice(this.chart_index, 1);
628
628
  this.chart_index = -1;
629
629
  }
630
- // Also update the experiment viewer (charts define the output variables)
630
+ // Also update the experiment viewer, because this chart may be
631
+ // one of the output charts of the selected experiment.
631
632
  UI.updateControllerDialogs('CFX');
632
633
  }
633
634
  }
@@ -689,8 +690,8 @@ class GUIChartManager extends ChartManager {
689
690
  }
690
691
 
691
692
  addVariable(eq='') {
692
- // Adds the variable specified by the add-variable-dialog to the chart
693
- // NOTE: when defined, `eq` is the selector of the equation to be added
693
+ // Add the variable specified by the add-variable-dialog to the chart.
694
+ // NOTE: When defined, `eq` is the selector of the equation to be added.
694
695
  if(this.chart_index >= 0) {
695
696
  let o = '',
696
697
  a = eq;
@@ -698,7 +699,7 @@ class GUIChartManager extends ChartManager {
698
699
  o = this.add_variable_modal.selectedOption('name').text;
699
700
  a = this.add_variable_modal.selectedOption('attr').text;
700
701
  }
701
- // NOTE: when equation is added, object specifier is empty string
702
+ // NOTE: When equation is added, object specifier is empty string.
702
703
  if(!o && a) o = UI.EQUATIONS_DATASET_NAME;
703
704
  this.variable_index = MODEL.charts[this.chart_index].addVariable(o, a);
704
705
  if(this.variable_index >= 0) {
@@ -2608,10 +2608,11 @@ class GUIController extends Controller {
2608
2608
  document.body.className = '';
2609
2609
  }
2610
2610
 
2611
- setProgressNeedle(fraction) {
2611
+ setProgressNeedle(fraction, color='#500080') {
2612
2612
  // Shows a thin purple line just above the status line to indicate progress
2613
2613
  const el = document.getElementById('set-up-progress-bar');
2614
2614
  el.style.width = Math.round(Math.max(0, Math.min(1, fraction)) * 100) + '%';
2615
+ el.style.backgroundColor = color;
2615
2616
  }
2616
2617
 
2617
2618
  hideStayOnTopDialogs() {
@@ -2934,8 +2935,8 @@ class GUIController extends Controller {
2934
2935
  }
2935
2936
  const err = MODEL.cloneSelection(prefix, actor_name, renumber);
2936
2937
  if(err) {
2937
- // Something went wrong, so do not hide the modal, but focus on the
2938
- // DOM element returned by the model's cloning method
2938
+ // Something went wrong, so do not hide the modal, but focus on
2939
+ // the DOM element returned by the model's cloning method.
2939
2940
  const el = md.element(err);
2940
2941
  if(el) {
2941
2942
  el.focus();
@@ -2955,7 +2956,7 @@ class GUIController extends Controller {
2955
2956
  }
2956
2957
 
2957
2958
  copySelection() {
2958
- // Save selection as XML in local storage of the browser
2959
+ // Save selection as XML in local storage of the browser.
2959
2960
  const xml = MODEL.selectionAsXML;
2960
2961
  if(xml) {
2961
2962
  window.localStorage.setItem('Linny-R-selection-XML', xml);
@@ -2966,20 +2967,22 @@ class GUIController extends Controller {
2966
2967
  }
2967
2968
 
2968
2969
  get canPaste() {
2970
+ // Return TRUE if the browser has a recent selection-as-XML object
2971
+ // in its local storage.
2969
2972
  const xml = window.localStorage.getItem('Linny-R-selection-XML');
2970
2973
  if(xml) {
2971
2974
  const timestamp = xml.match(/<copy timestamp="(\d+)"/);
2972
2975
  if(timestamp) {
2973
2976
  if(Date.now() - parseInt(timestamp[1]) < 8*3600000) return true;
2974
2977
  }
2975
- // Remove XML from local storage if older than 8 hours
2978
+ // Remove XML from local storage if older than 8 hours.
2976
2979
  window.localStorage.removeItem('Linny-R-selection-XML');
2977
2980
  }
2978
2981
  return false;
2979
2982
  }
2980
2983
 
2981
2984
  promptForMapping(mapping) {
2982
- // Prompt user to specify name conflict resolution strategy
2985
+ // Prompt user to specify name conflict resolution strategy.
2983
2986
  const md = this.paste_modal;
2984
2987
  md.mapping = mapping;
2985
2988
  md.element('from-prefix').innerText = mapping.from_prefix || '';
@@ -2999,7 +3002,8 @@ class GUIController extends Controller {
2999
3002
  if(tc.length) {
3000
3003
  sl.push('<div style="font-weight: bold; margin:4px 2px 2px 2px">',
3001
3004
  'Names for top-level clusters:</div>');
3002
- // Add text inputs for selected cluster nodes
3005
+ const sll = sl.length;
3006
+ // Add text inputs for selected cluster nodes.
3003
3007
  for(let i = 0; i < tc.length; i++) {
3004
3008
  const
3005
3009
  ti = mapping.top_clusters[tc[i]],
@@ -3011,11 +3015,14 @@ class GUIController extends Controller {
3011
3015
  '" type="text" style="', state, 'font-size: 12px" value="',
3012
3016
  ti, '"></div></div>');
3013
3017
  }
3018
+ // Remove header when no items were added.
3019
+ if(sl.length === sll) sl.pop();
3014
3020
  }
3015
3021
  if(ft.length) {
3016
3022
  sl.push('<div style="font-weight: bold; margin:4px 2px 2px 2px">',
3017
3023
  'Mapping of nodes to link from/to:</div>');
3018
- // Add selectors for unresolved FROM/TO nodes
3024
+ const sll = sl.length;
3025
+ // Add selectors for unresolved FROM/TO nodes.
3019
3026
  for(let i = 0; i < ft.length; i++) {
3020
3027
  const ti = mapping.from_to[ft[i]];
3021
3028
  if(ft[i] === ti) {
@@ -3035,15 +3042,17 @@ class GUIController extends Controller {
3035
3042
  sl.push('</div>');
3036
3043
  }
3037
3044
  }
3045
+ // Remove header when no items were added.
3046
+ if(sl.length === sll) sl.pop();
3038
3047
  }
3039
3048
  md.element('scroll-area').innerHTML = sl.join('');
3040
- // Open dialog, which will call pasteSelection(...) on OK
3049
+ // Open dialog, which will call pasteSelection(...) on OK.
3041
3050
  this.paste_modal.show();
3042
3051
  }
3043
3052
 
3044
3053
  setPasteMapping() {
3045
- // Updates the paste mapping as specified by the modeler and then
3046
- // proceeds to paste
3054
+ // Update the paste mapping as specified by the modeler and then
3055
+ // proceed to paste.
3047
3056
  const
3048
3057
  md = this.paste_modal,
3049
3058
  mapping = Object.assign(md.mapping, {}),
@@ -3070,7 +3079,7 @@ class GUIController extends Controller {
3070
3079
  pasteSelection(mapping={}) {
3071
3080
  // If selection has been saved as XML in local storage, test to
3072
3081
  // see whether PASTE would result in name conflicts, and if so,
3073
- // open the name conflict resolution window
3082
+ // open the name conflict resolution window.
3074
3083
  let xml = window.localStorage.getItem('Linny-R-selection-XML');
3075
3084
  try {
3076
3085
  xml = parseXML(xml);
@@ -3094,7 +3103,7 @@ class GUIController extends Controller {
3094
3103
  // AUXILIARY FUNCTIONS
3095
3104
 
3096
3105
  function fullName(node) {
3097
- // Returns full entity name inferred from XML node data
3106
+ // Return full entity name inferred from XML node data.
3098
3107
  if(node.nodeName === 'from-to' || node.nodeName === 'selc') {
3099
3108
  const
3100
3109
  n = xmlDecoded(nodeParameterValue(node, 'name')),
@@ -3133,12 +3142,12 @@ class GUIController extends Controller {
3133
3142
  }
3134
3143
 
3135
3144
  function nameAndActor(name) {
3136
- // Returns tuple [entity name, actor name] if `name` ends with
3137
- // a parenthesized string that identifies an actor in the selection
3145
+ // Return tuple [entity name, actor name] if `name` ends with a
3146
+ // parenthesized string that identifies an actor in the selection.
3138
3147
  const ai = name.lastIndexOf(' (');
3139
3148
  if(ai < 0) return [name, ''];
3140
3149
  let actor = name.slice(ai + 2, -1);
3141
- // Test whether parenthesized string denotes an actor
3150
+ // Test whether parenthesized string denotes an actor.
3142
3151
  if(actor_names.indexOf(actor) >= 0 || actor === mapping.actor ||
3143
3152
  actor === mapping.from_actor || actor === mapping.to_actor) {
3144
3153
  name = name.substring(0, ai);
@@ -3149,8 +3158,8 @@ class GUIController extends Controller {
3149
3158
  }
3150
3159
 
3151
3160
  function mappedName(n) {
3152
- // Returns full name `n` modified according to the mapping
3153
- // NOTE: links and constraints require two mappings (recursion!)
3161
+ // Returns full name `n` modified according to the mapping.
3162
+ // NOTE: Links and constraints require two mappings (recursion!).
3154
3163
  if(n.indexOf(UI.LINK_ARROW) > 0) {
3155
3164
  const ft = n.split(UI.LINK_ARROW);
3156
3165
  return mappedName(ft[0]) + UI.LINK_ARROW + mappedName(ft[1]);
@@ -3173,7 +3182,7 @@ class GUIController extends Controller {
3173
3182
  const ai = n.lastIndexOf(mapping.from_actor);
3174
3183
  if(ai > 0) return n.substring(0, ai) + mapping.to_actor;
3175
3184
  }
3176
- // NOTE: specified actor cannot override existing actor
3185
+ // NOTE: specified actor cannot override existing actor.
3177
3186
  if(mapping.actor && !nameAndActor(n)[1]) {
3178
3187
  return `${n} (${mapping.actor})`;
3179
3188
  }
@@ -3190,23 +3199,27 @@ class GUIController extends Controller {
3190
3199
  if(mapping.from_to && mapping.from_to[n]) {
3191
3200
  return mapping.from_to[n];
3192
3201
  }
3193
- // No mapping => return original name
3202
+ // No mapping => return original name.
3194
3203
  return n;
3195
3204
  }
3196
3205
 
3197
3206
  function nameConflicts(node) {
3198
3207
  // Maps names of entities defined by the child nodes of `node`
3199
- // while detecting name conflicts
3208
+ // while detecting name conflicts.
3200
3209
  for(let i = 0; i < node.childNodes.length; i++) {
3201
3210
  const c = node.childNodes[i];
3202
3211
  if(c.nodeName !== 'link' && c.nodeName !== 'constraint') {
3203
3212
  const
3204
3213
  fn = fullName(c),
3205
- mn = mappedName(fn);
3214
+ mn = mappedName(fn),
3215
+ obj = MODEL.objectByName(mn),
3216
+ // Assume that existing products can be added as product
3217
+ // positions if they are not prefixed.
3218
+ add_pp = (obj instanceof Product && mn.indexOf(UI.PREFIXER) < 0);
3206
3219
  // Name conflict occurs when the mapped name is already in use
3207
3220
  // in the target model, or when the original name is mapped onto
3208
- // different names (this might occur due to modeler input)
3209
- if(MODEL.objectByName(mn) || (name_map[fn] && name_map[fn] !== mn)) {
3221
+ // different names (this might occur due to modeler input).
3222
+ if((obj && !add_pp) || (name_map[fn] && name_map[fn] !== mn)) {
3210
3223
  addDistinct(fn, name_conflicts);
3211
3224
  } else {
3212
3225
  name_map[fn] = mn;
@@ -3216,10 +3229,10 @@ class GUIController extends Controller {
3216
3229
  }
3217
3230
 
3218
3231
  function addEntityFromNode(node) {
3219
- // Adds entity to model based on XML node data and mapping
3220
- // NOTE: do not add if an entity having this type and mapped name
3232
+ // Adds entity to model based on XML node data and mapping.
3233
+ // NOTE: Do not add if an entity having this type and mapped name
3221
3234
  // already exists; name conflicts accross entity types may occur
3222
- // and result in error messages
3235
+ // and result in error messages.
3223
3236
  const
3224
3237
  et = node.nodeName,
3225
3238
  fn = fullName(node),
@@ -3281,17 +3294,17 @@ class GUIController extends Controller {
3281
3294
  sp = this.sharedPrefix(cn, fcn),
3282
3295
  fpn = (cn === UI.TOP_CLUSTER_NAME ? '' : cn.replace(sp, '')),
3283
3296
  tpn = (fcn === UI.TOP_CLUSTER_NAME ? '' : fcn.replace(sp, ''));
3284
- // Infer mapping from XML data and focal cluster name & actor name
3297
+ // Infer mapping from XML data and focal cluster name & actor name.
3285
3298
  mapping.shared_prefix = sp;
3286
3299
  mapping.from_prefix = (fpn ? sp + fpn + UI.PREFIXER : sp);
3287
3300
  mapping.to_prefix = (tpn ? sp + tpn + UI.PREFIXER : sp);
3288
3301
  mapping.from_actor = (ca === UI.NO_ACTOR ? '' : ca);
3289
3302
  mapping.to_actor = (fca === UI.NO_ACTOR ? '' : fca);
3290
- // Prompt for mapping when pasting to the same model and cluster
3303
+ // Prompt for mapping when pasting to the same model and cluster.
3291
3304
  if(parseInt(mts) === MODEL.time_created.getTime() &&
3292
3305
  ca === fca && mapping.from_prefix === mapping.to_prefix &&
3293
3306
  !(mapping.prefix || mapping.actor || mapping.increment)) {
3294
- // Prompt for names of selected cluster nodes
3307
+ // Prompt for names of selected cluster nodes.
3295
3308
  if(selc_node.childNodes.length && !mapping.prefix) {
3296
3309
  mapping.top_clusters = {};
3297
3310
  for(let i = 0; i < selc_node.childNodes.length; i++) {
@@ -3306,7 +3319,7 @@ class GUIController extends Controller {
3306
3319
  return;
3307
3320
  }
3308
3321
  // Also prompt if FROM and/or TO nodes are not selected, and map to
3309
- // existing entities
3322
+ // existing entities.
3310
3323
  if(from_tos_node.childNodes.length && !mapping.from_to) {
3311
3324
  const
3312
3325
  ft_map = {},
@@ -3322,7 +3335,7 @@ class GUIController extends Controller {
3322
3335
  'Data' : nodeParameterValue(c, 'type'));
3323
3336
  }
3324
3337
  }
3325
- // Prompt only for FROM/TO nodes that map to existing nodes
3338
+ // Prompt only for FROM/TO nodes that map to existing nodes.
3326
3339
  if(Object.keys(ft_map).length) {
3327
3340
  mapping.from_to = ft_map;
3328
3341
  mapping.from_to_type = ft_type;
@@ -3333,7 +3346,7 @@ class GUIController extends Controller {
3333
3346
 
3334
3347
  // Only check for selected entities; from-to's and extra's should be
3335
3348
  // used if they exist, or should be created when copying to a different
3336
- // model
3349
+ // model.
3337
3350
  name_map.length = 0;
3338
3351
  nameConflicts(entities_node);
3339
3352
  if(name_conflicts.length) {
@@ -3352,20 +3365,20 @@ console.log('HERE name conflicts', name_conflicts, mapping);
3352
3365
  for(let i = 0; i < entities_node.childNodes.length; i++) {
3353
3366
  addEntityFromNode(entities_node.childNodes[i]);
3354
3367
  }
3355
- // Update diagram, showing newly added nodes as selection
3368
+ // Update diagram, showing newly added nodes as selection.
3356
3369
  MODEL.clearSelection();
3357
3370
  for(let i = 0; i < selection_node.childNodes.length; i++) {
3358
3371
  const
3359
3372
  n = xmlDecoded(nodeContent(selection_node.childNodes[i])),
3360
3373
  obj = MODEL.objectByName(mappedName(n));
3361
3374
  if(obj) {
3362
- // NOTE: selected products must be positioned
3375
+ // NOTE: Selected products must be positioned.
3363
3376
  if(obj instanceof Product) MODEL.focal_cluster.addProductPosition(obj);
3364
3377
  MODEL.select(obj);
3365
3378
  }
3366
3379
  }
3367
3380
  // Force redrawing the selection to ensure that links to positioned
3368
- // products are displayed as arrows instead of block arrows
3381
+ // products are displayed as arrows instead of block arrows.
3369
3382
  fc.clearAllProcesses();
3370
3383
  UI.drawDiagram(MODEL);
3371
3384
  this.paste_modal.hide();
@@ -486,12 +486,14 @@ class GUIDatasetManager extends DatasetManager {
486
486
  m = sd.modifiers[UI.nameToID(msl[i])],
487
487
  wild = m.hasWildcards,
488
488
  defsel = (m.selector === sd.default_selector),
489
+ issue = (m.expression.compile_issue ? ' compile-issue' :
490
+ (m.expression.compute_issue ? ' compute-issue' : '')),
489
491
  clk = '" onclick="DATASET_MANAGER.selectModifier(event, \'' +
490
492
  m.selector + '\'';
491
493
  if(m === sm) smid += i;
492
494
  ml.push(['<tr id="dsmtr', i, '" class="dataset-modif',
493
495
  (m === sm ? ' sel-set' : ''),
494
- '"><td class="dataset-selector',
496
+ '"><td class="dataset-selector', issue,
495
497
  (wild ? ' wildcard' : ''),
496
498
  '" title="Shift-click to ', (defsel ? 'clear' : 'set as'),
497
499
  ' default modifier',
@@ -499,7 +501,10 @@ class GUIDatasetManager extends DatasetManager {
499
501
  (defsel ? '<img src="images/solve.png" style="height: 14px;' +
500
502
  ' width: 14px; margin: 0 1px -3px -1px;">' : ''),
501
503
  (wild ? wildcardFormat(m.selector, true) : m.selector),
502
- '</td><td class="dataset-expression',
504
+ '</td><td class="dataset-expression', issue,
505
+ (issue ? '"title="' +
506
+ safeDoubleQuotes(m.expression.compile_issue ||
507
+ m.expression.compute_issue) : ''),
503
508
  clk, ');">', m.expression.text, '</td></tr>'].join(''));
504
509
  }
505
510
  this.modifier_table.innerHTML = ml.join('');
@@ -654,14 +659,14 @@ class GUIDatasetManager extends DatasetManager {
654
659
  }
655
660
 
656
661
  renameDataset() {
657
- // Changes the name of the selected dataset
662
+ // Change the name of the selected dataset.
658
663
  if(this.selected_dataset) {
659
664
  const
660
665
  inp = this.rename_modal.element('name'),
661
666
  n = UI.cleanName(inp.value);
662
- // Show modeler the "cleaned" new name
667
+ // Show modeler the "cleaned" new name.
663
668
  inp.value = n;
664
- // Then try to rename -- this may generate a warning
669
+ // Then try to rename -- this may generate a warning.
665
670
  if(this.selected_dataset.rename(n)) {
666
671
  this.rename_modal.hide();
667
672
  if(EXPERIMENT_MANAGER.selected_experiment) {
@@ -670,59 +675,22 @@ class GUIDatasetManager extends DatasetManager {
670
675
  UI.updateControllerDialogs('CDEFJX');
671
676
  }
672
677
  } else if(this.selected_prefix_row) {
673
- // Create a list of datasets to be renamed
678
+ // Create a list of datasets to be renamed.
674
679
  let e = this.rename_modal.element('name'),
675
680
  prefix = e.value.trim();
676
681
  e.focus();
677
- // Trim trailing colon if user entered it
682
+ // Trim trailing colon if user added it.
678
683
  while(prefix.endsWith(':')) prefix = prefix.slice(0, -1);
679
- // NOTE: prefix may be empty string, but otherwise should be a valid name
684
+ // NOTE: Prefix may be empty string, but otherwise should be a
685
+ // valid name.
680
686
  if(prefix && !UI.validName(prefix)) {
681
687
  UI.warn('Invalid prefix');
682
688
  return;
683
689
  }
684
- // Now add the colon-plus-space prefix separator
690
+ // Now add the colon-plus-space prefix separator.
685
691
  prefix += UI.PREFIXER;
686
- const
687
- oldpref = this.selectedPrefix,
688
- key = oldpref.toLowerCase().split(UI.PREFIXER).join(':_'),
689
- newkey = prefix.toLowerCase().split(UI.PREFIXER).join(':_'),
690
- dsl = [];
691
- // No change if new prefix is identical to old prefix
692
- if(oldpref !== prefix) {
693
- for(let k in MODEL.datasets) if(MODEL.datasets.hasOwnProperty(k)) {
694
- if(k.startsWith(key)) dsl.push(k);
695
- }
696
- // NOTE: no check needed for mere upper/lower case changes
697
- if(newkey !== key) {
698
- let nc = 0;
699
- for(let i = 0; i < dsl.length; i++) {
700
- let nk = newkey + dsl[i].substring(key.length);
701
- if(MODEL.datasets[nk]) nc++;
702
- }
703
- if(nc) {
704
- UI.warn('Renaming ' + pluralS(dsl.length, 'dataset').toLowerCase() +
705
- ' would cause ' + pluralS(nc, 'name conflict'));
706
- return;
707
- }
708
- }
709
- // Reset counts of effects of a rename operation
710
- this.entity_count = 0;
711
- this.expression_count = 0;
712
- // Rename datasets one by one, suppressing notifications
713
- for(let i = 0; i < dsl.length; i++) {
714
- const d = MODEL.datasets[dsl[i]];
715
- d.rename(d.displayName.replace(oldpref, prefix), false);
716
- }
717
- let msg = 'Renamed ' + pluralS(dsl.length, 'dataset').toLowerCase();
718
- if(MODEL.variable_count) msg += ', and updated ' +
719
- pluralS(MODEL.variable_count, 'variable') + ' in ' +
720
- pluralS(MODEL.expression_count, 'expression');
721
- UI.notify(msg);
722
- if(EXPERIMENT_MANAGER.selected_experiment) {
723
- EXPERIMENT_MANAGER.selected_experiment.inferVariables();
724
- }
725
- UI.updateControllerDialogs('CDEFJX');
692
+ // Perform the renaming operation.
693
+ if(MODEL.renamePrefixedDatasets(this.selectedPrefix, prefix)) {
726
694
  this.selectPrefixRow(prefix);
727
695
  }
728
696
  }