linny-r 1.3.0 → 1.3.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/console.js CHANGED
@@ -113,10 +113,14 @@ Possible options are:
113
113
  channel=[identifier] will start listening at the specified channel
114
114
  (FUTURE OPTION)
115
115
  check will report whether current version is up-to-date
116
- model=[path] will load model file in [path]
116
+ data-dir=[path] will look for series data files in [path] instead of
117
+ (main)/user/data
118
+ model=[path] will load model file specified by [path]
117
119
  module=[name@repo] will load model [name] from repository [repo]
118
120
  (if @repo is blank, repository "local host" is used)
119
121
  (FUTURE OPTION)
122
+ report=[name] will write run results to [name]-series.txt and
123
+ [name]-stats.txt in (workspace)/reports
120
124
  run will run the loaded model
121
125
  solver=[name] will select solver [name], or warn if not found
122
126
  (name choices: Gurobi or LP_solve)
@@ -247,11 +251,81 @@ class ConsoleMonitor {
247
251
  } // END of class ConsoleMonitor
248
252
 
249
253
 
254
+ // NOTE: This implementation is very incomplete, still!
255
+ class ConsoleRepositoryBrowser {
256
+ constructor() {
257
+ this.repositories = [];
258
+ this.repository_index = -1;
259
+ this.module_index = -1;
260
+ // Get the repository list from the modules
261
+ this.getRepositories();
262
+ this.reset();
263
+ }
264
+
265
+ reset() {
266
+ this.visible = false;
267
+ }
268
+
269
+ get isLocalHost() {
270
+ // Returns TRUE if first repository on the list is 'local host'
271
+ return this.repositories.length > 0 &&
272
+ this.repositories[0].name === 'local host';
273
+ }
274
+
275
+ getRepositories() {
276
+ // Gets the list of repository names from the server
277
+ this.repositories.length = 0;
278
+ // @@TO DO!!
279
+ }
280
+
281
+ repositoryByName(n) {
282
+ // Returns the repository having name `n` if already known, otherwise NULL
283
+ for(let i = 0; i < this.repositories.length; i++) {
284
+ if(this.repositories[i].name === n) {
285
+ return this.repositories[i];
286
+ }
287
+ }
288
+ return null;
289
+ }
290
+
291
+ asFileName(s) {
292
+ // NOTE: asFileName is implemented as function (see below) to permit
293
+ // its use prior to instantiation of the RepositoryBrowser
294
+ return stringToFileName(s);
295
+ }
296
+
297
+ }
298
+
299
+ function stringToFileName(s) {
300
+ // Returns string `s` with whitespace converted to a single dash, and
301
+ // special characters converted to underscores
302
+ return s.normalize('NFKD').trim()
303
+ .replace(/[\s\-]+/g, '-')
304
+ .replace(/[^A-Za-z0-9_\-]/g, '_')
305
+ .replace(/^[\-\_]+|[\-\_]+$/g, '');
306
+ }
307
+
250
308
  // CLASS ConsoleFileManager allows loading and saving models and diagrams, and
251
309
  // handles the interaction with the MILP solver via `exec` calls and files
252
310
  // stored on the modeler's computer
253
311
  class ConsoleFileManager {
254
312
 
313
+ anyOSpath(p) {
314
+ // Helper function that converts any path notation to platform notation
315
+ // based on the predominant separator
316
+ const
317
+ s_parts = p.split('/'),
318
+ bs_parts = p.split('\\'),
319
+ parts = (s_parts.length > bs_parts.length ? s_parts : bs_parts);
320
+ // On macOS machines, paths start with a slash, so first substring is empty
321
+ if(parts[0].endsWith(':') && path.sep === '\\') {
322
+ // On Windows machines, add a backslash after the disk (if specified)
323
+ parts[0] += path.sep;
324
+ }
325
+ // Reassemble path for the OS of this machine
326
+ return path.join(...parts);
327
+ }
328
+
255
329
  getRemoteData(dataset, url) {
256
330
  // Gets data from a URL, or from a file on the local host
257
331
  if(url === '') return;
@@ -281,7 +355,13 @@ class ConsoleFileManager {
281
355
  console.log('ERROR: Invalid URL', url);
282
356
  }
283
357
  } else {
284
- const fp = anyOSpath(url);
358
+ let fp = this.anyOSpath(url);
359
+ if(!(fp.startsWith('/') || fp.startsWith('\\') || fp.indexOf(':\\') > 0)) {
360
+ // Relative path => add path to specified data path or to the
361
+ // default location user/data
362
+ fp = path.join(SETTINGS.data_path || WORKSPACE.data, fp);
363
+ console.log('Full path: ', fp);
364
+ }
285
365
  fs.readFile(fp, 'utf8', (err, data) => {
286
366
  if(err) {
287
367
  console.log(err);
@@ -354,6 +434,17 @@ class ConsoleFileManager {
354
434
  });
355
435
  }
356
436
 
437
+ writeStringToFile(s, fp) {
438
+ // Write string `s` to path `fp`
439
+ try {
440
+ fs.writeFileSync(fp, s);
441
+ console.log(pluralS(s.length, 'character') + ' written to file ' + fp);
442
+ } catch(err) {
443
+ console.log(err);
444
+ console.log('ERROR: Failed to write data to file ' + fp);
445
+ }
446
+ }
447
+
357
448
  } // END of class ConsoleFileManager
358
449
 
359
450
  // CLASS ConsoleReceiver defines a listener/interpreter for channel commands
@@ -702,7 +793,9 @@ function commandLineSettings() {
702
793
  const settings = {
703
794
  cli_name: (PLATFORM.startsWith('win') ? 'Command Prompt' : 'Terminal'),
704
795
  check: false,
796
+ data_path: '',
705
797
  preferred_solver: '',
798
+ report: '',
706
799
  run: false,
707
800
  solver: '',
708
801
  solver_path: '',
@@ -759,6 +852,31 @@ function commandLineSettings() {
759
852
  console.log(`ERROR: File "${av[1]}" not found`);
760
853
  process.exit();
761
854
  }
855
+ } else if(av[0] === 'data-dir') {
856
+ // Set path (if valid) to override default data directory
857
+ const dp = av[1];
858
+ try {
859
+ // See whether the directory already exists
860
+ try {
861
+ fs.accessSync(dp, fs.constants.R_OK | fs.constants.W_O);
862
+ } catch(err) {
863
+ // If not, try to create it
864
+ fs.mkdirSync(dp);
865
+ console.log('Created data directory:', dp);
866
+ }
867
+ settings.data_path = dp;
868
+ } catch(err) {
869
+ console.log(err.message);
870
+ console.log('ERROR: Failed to create data directory:', dp);
871
+ }
872
+ } else if(av[0] === 'report') {
873
+ // Set report file name (if valid)
874
+ const rfn = stringToFileName(av[1]);
875
+ if(/^[A-Za-z0-9]+/.test(rfn)) {
876
+ settings.report = path.join(settings.user_dir, 'reports', rfn);
877
+ } else {
878
+ console.log(`WARNING: Invalid report file name "{$rfn}"`);
879
+ }
762
880
  } else if(av[0] === 'module') {
763
881
  // Add default repository is none specified
764
882
  if(av[1].indexOf('@') < 0) av[1] += '@local host';
@@ -882,10 +1000,13 @@ function createWorkspace() {
882
1000
  }
883
1001
  // Define the sub-directory paths
884
1002
  const ws = {
1003
+ autosave: path.join(SETTINGS.user_dir, 'autosave'),
885
1004
  channel: path.join(SETTINGS.user_dir, 'channel'),
886
1005
  callback: path.join(SETTINGS.user_dir, 'callback'),
1006
+ data: path.join(SETTINGS.user_dir, 'data'),
887
1007
  diagrams: path.join(SETTINGS.user_dir, 'diagrams'),
888
1008
  modules: path.join(SETTINGS.user_dir, 'modules'),
1009
+ reports: path.join(SETTINGS.user_dir, 'reports'),
889
1010
  solver_output: path.join(SETTINGS.user_dir, 'solver'),
890
1011
  };
891
1012
  // Create these sub-directories if not aready there
@@ -968,7 +1089,7 @@ PROMPTER.questionPrompt = (str) => {
968
1089
  // Initialize the Linny-R console components as global variables
969
1090
  global.UI = new Controller();
970
1091
  global.VM = new VirtualMachine();
971
- //global.REPOSITORY_BROWSER = new ConsoleRepositoryBrowser(), // still to re-code
1092
+ global.REPOSITORY_BROWSER = new ConsoleRepositoryBrowser();
972
1093
  global.FILE_MANAGER = new ConsoleFileManager();
973
1094
  global.DATASET_MANAGER = new DatasetManager();
974
1095
  global.CHART_MANAGER = new ChartManager();
@@ -990,8 +1111,19 @@ if(SETTINGS.model_path) {
990
1111
  MONITOR.show_log = SETTINGS.verbose;
991
1112
  VM.callback = () => {
992
1113
  const od = model.outputData;
993
- console.log(od[0]);
994
- console.log(od[1]);
1114
+ // Output data is two-string list [time series, statistics]
1115
+ if(SETTINGS.report) {
1116
+ // Output time series
1117
+ FILE_MANAGER.writeStringToFile(od[0],
1118
+ SETTINGS.report + '-series.txt');
1119
+ // Output statistics
1120
+ FILE_MANAGER.writeStringToFile(od[1],
1121
+ SETTINGS.report + '-stats.txt');
1122
+ } else {
1123
+ // Output strings to console
1124
+ console.log(od[0]);
1125
+ console.log(od[1]);
1126
+ }
995
1127
  VM.callback = null;
996
1128
  };
997
1129
  VM.solveModel();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "linny-r",
3
- "version": "1.3.0",
3
+ "version": "1.3.1",
4
4
  "description": "Executable graphical language with WYSIWYG editor for MILP models",
5
5
  "main": "server.js",
6
6
  "scripts": {
package/server.js CHANGED
@@ -847,20 +847,19 @@ function repoDelete(res, name, file) {
847
847
  // Dataset dialog
848
848
 
849
849
  function anyOSpath(p) {
850
- // Helper function that converts Unix path notation (with slashes) to
851
- // Windows notation if needed
852
- if(p.indexOf('/') < 0) return p;
853
- p = p.split('/');
850
+ // Helper function that converts any path notation to platform notation
851
+ // based on the predominant separator
852
+ const
853
+ s_parts = p.split('/'),
854
+ bs_parts = p.split('\\'),
855
+ parts = (s_parts.length > bs_parts.length ? s_parts : bs_parts);
854
856
  // On macOS machines, paths start with a slash, so first substring is empty
855
- if(p[0].length === 0) {
856
- // In that case, add the leading slash
857
- return '/' + path.join(...p);
858
- } else if(p[0].endsWith(':') && path.sep === '\\') {
857
+ if(parts[0].endsWith(':') && path.sep === '\\') {
859
858
  // On Windows machines, add a backslash after the disk (if specified)
860
- path[0] += path.sep;
859
+ parts[0] += path.sep;
861
860
  }
862
861
  // Reassemble path for the OS of this machine
863
- return path.join(...p);
862
+ return path.join(...parts);
864
863
  }
865
864
 
866
865
  function loadData(res, url) {
@@ -881,7 +880,11 @@ function loadData(res, url) {
881
880
  servePlainText(res, `ERROR: Invalid URL <tt>${url}</tt>`);
882
881
  }
883
882
  } else {
884
- const fp = anyOSpath(url);
883
+ let fp = anyOSpath(url);
884
+ if(!(fp.startsWith('/') || fp.startsWith('\\') || fp.indexOf(':\\') > 0)) {
885
+ // Relative path => add path to user/data directory
886
+ fp = path.join(WORKSPACE.data, fp);
887
+ }
885
888
  fs.readFile(fp, 'utf8', (err, data) => {
886
889
  if(err) {
887
890
  console.log(err);
@@ -1568,6 +1571,7 @@ function createWorkspace() {
1568
1571
  autosave: path.join(SETTINGS.user_dir, 'autosave'),
1569
1572
  channel: path.join(SETTINGS.user_dir, 'channel'),
1570
1573
  callback: path.join(SETTINGS.user_dir, 'callback'),
1574
+ data: path.join(SETTINGS.user_dir, 'data'),
1571
1575
  diagrams: path.join(SETTINGS.user_dir, 'diagrams'),
1572
1576
  modules: path.join(SETTINGS.user_dir, 'modules'),
1573
1577
  solver_output: path.join(SETTINGS.user_dir, 'solver'),
Binary file
package/static/index.html CHANGED
@@ -1704,8 +1704,11 @@ NOTE: * and ? will be interpreted as wildcards"
1704
1704
  <!-- options are added by DATASET_MANAGER -->
1705
1705
  </select>
1706
1706
  <div id="series-remote">
1707
- <input id="series-url" type="text"
1708
- placeholder="(URL or path on local host)"> </div>
1707
+ <img id="series-clip" src="images/paperclip.png">
1708
+ <input id="series-url" type="text"
1709
+ placeholder="URL or path on local host"
1710
+ title="Path can be absolute or relative to (Linny-R)/user/data">
1711
+ </div>
1709
1712
  <div id="series-data-lbl">Series data:</div>
1710
1713
  <textarea id="series-data" autocomplete="off"
1711
1714
  autocorrect="off" autocapitalize="off" spellcheck="false">
@@ -2468,12 +2468,18 @@ td.equation-expression {
2468
2468
  #series-remote {
2469
2469
  position: absolute;
2470
2470
  top: 134px;
2471
- left: 3px;
2472
- width: calc(100% - 8px);
2471
+ left: 2px;
2472
+ width: calc(100% - 5px);
2473
+ }
2474
+
2475
+ #series-clip {
2476
+ height: 12px;
2477
+ width: 132x;
2478
+ vertical-align: middle;
2473
2479
  }
2474
2480
 
2475
2481
  #series-url {
2476
- width: 100%;
2482
+ width: calc(100% - 17px);
2477
2483
  font-size: 12px;
2478
2484
  }
2479
2485
 
@@ -3555,81 +3555,6 @@ class GUIController extends Controller {
3555
3555
  // Methods related to draggable & resizable dialogs
3556
3556
  //
3557
3557
 
3558
- toggleDialog(e) {
3559
- e = e || window.event;
3560
- e.preventDefault();
3561
- e.stopImmediatePropagation();
3562
- // Infer dialog identifier from target element
3563
- const
3564
- dlg = e.target.id.split('-')[0],
3565
- tde = document.getElementById(dlg + '-dlg'),
3566
- was_hidden = this.hidden(tde.id);
3567
- let mgr = tde.getAttribute('data-manager');
3568
- if(mgr) mgr = window[mgr];
3569
- // NOTE: prevent modeler from viewing charts while an experiment is running
3570
- if(dlg === 'chart' && was_hidden && MODEL.running_experiment) {
3571
- UI.notify(UI.NOTICE.NO_CHARTS);
3572
- mgr.visible = false;
3573
- return;
3574
- }
3575
- this.toggle(tde.id);
3576
- if(mgr) mgr.visible = was_hidden;
3577
- // Open at position after last drag (recorded in DOM data attributes)
3578
- let t = tde.getAttribute('data-top'),
3579
- l = tde.getAttribute('data-left');
3580
- // Make dialog appear in screen center the first time it is shown
3581
- if(t === null || l === null) {
3582
- const cs = window.getComputedStyle(tde);
3583
- t = ((window.innerHeight - parseFloat(cs.height)) / 2) + 'px';
3584
- l = ((window.innerWidth - parseFloat(cs.width)) / 2) + 'px';
3585
- tde.style.top = t;
3586
- tde.style.left = l;
3587
- }
3588
- if(!this.hidden(tde.id)) {
3589
- // Add dialog to "showing" list, and adjust z-indices
3590
- this.dr_dialog_order.push(tde);
3591
- this.reorderDialogs();
3592
- // Update the diagram if its manager has been specified
3593
- if(mgr) {
3594
- mgr.visible = true;
3595
- mgr.updateDialog();
3596
- if(mgr === DOCUMENTATION_MANAGER) {
3597
- if(this.info_line.innerHTML.length === 0) {
3598
- mgr.title.innerHTML = 'About Linny-R';
3599
- mgr.viewer.innerHTML = mgr.about_linny_r;
3600
- mgr.edit_btn.classList.remove('enab');
3601
- mgr.edit_btn.classList.add('disab');
3602
- }
3603
- UI.drawDiagram(MODEL);
3604
- }
3605
- }
3606
- } else {
3607
- const doi = this.dr_dialog_order.indexOf(tde);
3608
- // NOTE: doi should ALWAYS be >= 0 because dialog WAS showing
3609
- if(doi >= 0) {
3610
- this.dr_dialog_order.splice(doi, 1);
3611
- this.reorderDialogs();
3612
- }
3613
- if(mgr) {
3614
- mgr.visible = true;
3615
- if(mgr === DOCUMENTATION_MANAGER) {
3616
- mgr.visible = false;
3617
- mgr.title.innerHTML = 'Documentation';
3618
- UI.drawDiagram(MODEL);
3619
- }
3620
- }
3621
- }
3622
- UI.buttons[dlg].classList.toggle('stay-activ');
3623
- }
3624
-
3625
- reorderDialogs() {
3626
- let z = 10;
3627
- for(let i = 0; i < this.dr_dialog_order.length; i++) {
3628
- this.dr_dialog_order[i].style.zIndex = z;
3629
- z += 5;
3630
- }
3631
- }
3632
-
3633
3558
  draggableDialog(d) {
3634
3559
  // Make dialog draggable
3635
3560
  const
@@ -3755,7 +3680,7 @@ class GUIController extends Controller {
3755
3680
  UI.dr_dialog.style.width = Math.max(minw, w + dw) + 'px';
3756
3681
  UI.dr_dialog.style.height = Math.max(minh, h + dh) + 'px';
3757
3682
  // Update the dialog if its manager has been specified
3758
- const mgr = UI.dr_dialog.getAttribute('data-manager');
3683
+ const mgr = UI.dr_dialog.dataset.manager;
3759
3684
  if(mgr) window[mgr].updateDialog();
3760
3685
  }
3761
3686
 
@@ -3766,6 +3691,90 @@ class GUIController extends Controller {
3766
3691
  }
3767
3692
  }
3768
3693
 
3694
+ toggleDialog(e) {
3695
+ // Hide dialog if visible, or show it if not, and update the
3696
+ // order of appearance so that this dialog appears on top
3697
+ e = e || window.event;
3698
+ e.preventDefault();
3699
+ e.stopImmediatePropagation();
3700
+ // Infer dialog identifier from target element
3701
+ const
3702
+ dlg = e.target.id.split('-')[0],
3703
+ tde = document.getElementById(dlg + '-dlg');
3704
+ // NOTE: manager attribute is a string, e.g. 'MONITOR' or 'CHART_MANAGER'
3705
+ let mgr = tde.dataset.manager,
3706
+ was_hidden = this.hidden(tde.id);
3707
+ if(mgr) {
3708
+ // Dialog has a manager object => let `mgr` point to it
3709
+ mgr = window[mgr];
3710
+ // Manager object attributes are more reliable than DOM element
3711
+ // style attributes, so update the visibility status
3712
+ was_hidden = !mgr.visible;
3713
+ }
3714
+ // NOTE: modeler should not view charts while an experiment is
3715
+ // running, so do NOT toggle when the Chart Manager is NOT visible
3716
+ if(dlg === 'chart' && was_hidden && MODEL.running_experiment) {
3717
+ UI.notify(UI.NOTICE.NO_CHARTS);
3718
+ return;
3719
+ }
3720
+ // Otherwise, toggle the dialog visibility
3721
+ this.toggle(tde.id);
3722
+ UI.buttons[dlg].classList.toggle('stay-activ');
3723
+ if(mgr) mgr.visible = was_hidden;
3724
+ let t, l;
3725
+ if(top in tde.dataset && left in tde.dataset) {
3726
+ // Open at position after last drag (recorded in DOM data attributes)
3727
+ t = tde.dataset.top;
3728
+ l = tde.dataset.left;
3729
+ } else {
3730
+ // Make dialog appear in screen center the first time it is shown
3731
+ const cs = window.getComputedStyle(tde);
3732
+ t = ((window.innerHeight - parseFloat(cs.height)) / 2) + 'px';
3733
+ l = ((window.innerWidth - parseFloat(cs.width)) / 2) + 'px';
3734
+ tde.style.top = t;
3735
+ tde.style.left = l;
3736
+ }
3737
+ if(was_hidden) {
3738
+ // Add activated dialog to "showing" list, and adjust z-indices
3739
+ this.dr_dialog_order.push(tde);
3740
+ this.reorderDialogs();
3741
+ // Update the diagram if its manager has been specified
3742
+ if(mgr) {
3743
+ mgr.updateDialog();
3744
+ if(mgr === DOCUMENTATION_MANAGER) {
3745
+ if(this.info_line.innerHTML.length === 0) {
3746
+ mgr.title.innerHTML = 'About Linny-R';
3747
+ mgr.viewer.innerHTML = mgr.about_linny_r;
3748
+ mgr.edit_btn.classList.remove('enab');
3749
+ mgr.edit_btn.classList.add('disab');
3750
+ }
3751
+ UI.drawDiagram(MODEL);
3752
+ }
3753
+ }
3754
+ } else {
3755
+ const doi = this.dr_dialog_order.indexOf(tde);
3756
+ // NOTE: doi should ALWAYS be >= 0 because dialog WAS showing
3757
+ if(doi >= 0) {
3758
+ this.dr_dialog_order.splice(doi, 1);
3759
+ this.reorderDialogs();
3760
+ }
3761
+ if(mgr === DOCUMENTATION_MANAGER) {
3762
+ mgr.title.innerHTML = 'Documentation';
3763
+ UI.drawDiagram(MODEL);
3764
+ }
3765
+ }
3766
+ }
3767
+
3768
+ reorderDialogs() {
3769
+ // Set z-index of draggable dialogs according to their order
3770
+ // (most recently shown or clicked on top)
3771
+ let z = 10;
3772
+ for(let i = 0; i < this.dr_dialog_order.length; i++) {
3773
+ this.dr_dialog_order[i].style.zIndex = z;
3774
+ z += 5;
3775
+ }
3776
+ }
3777
+
3769
3778
  //
3770
3779
  // Button functionality
3771
3780
  //
@@ -5975,7 +5984,7 @@ class GUIMonitor {
5975
5984
  (event) => {
5976
5985
  const el = event.target;
5977
5986
  el.classList.add('sel-pb');
5978
- MONITOR.showBlock(el.getAttribute('data-blk'));
5987
+ MONITOR.showBlock(el.dataset.blk);
5979
5988
  },
5980
5989
  false);
5981
5990
  this.progress_bar.appendChild(n);
@@ -4856,11 +4856,12 @@ Solver status = ${json.status}`);
4856
4856
  return;
4857
4857
  } else {
4858
4858
  // Wait no longer, but warn user that data may be incomplete
4859
- dsl = [];
4859
+ const dsl = [];
4860
4860
  for(let i = 0; i < MODEL.loading_datasets.length; i++) {
4861
4861
  dsl.push(MODEL.loading_datasets[i].displayName);
4862
4862
  }
4863
- UI.warn(`Loading datasets (${dsl.join(', ')}) takes too long`);
4863
+ UI.warn('Loading of ' + pluralS(dsl.length, 'dataset') + ' (' +
4864
+ dsl.join(', ') + ') takes too long');
4864
4865
  }
4865
4866
  }
4866
4867
  if(MONITOR.connectToServer()) {