linny-r 1.3.0 → 1.3.2
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 +162 -7
- package/package.json +1 -1
- package/server.js +41 -16
- package/static/images/paperclip.png +0 -0
- package/static/index.html +64 -3
- package/static/linny-r.css +65 -3
- package/static/scripts/linny-r-ctrl.js +16 -1
- package/static/scripts/linny-r-gui.js +421 -84
- package/static/scripts/linny-r-milp.js +144 -13
- package/static/scripts/linny-r-model.js +109 -21
- package/static/scripts/linny-r-utils.js +10 -2
- package/static/scripts/linny-r-vm.js +205 -89
package/console.js
CHANGED
@@ -86,6 +86,9 @@ console.log('Platform:', PLATFORM, '(' + os.type() + ')');
|
|
86
86
|
console.log('Module directory:', MODULE_DIRECTORY);
|
87
87
|
console.log('Working directory:', WORKING_DIRECTORY);
|
88
88
|
|
89
|
+
// Currently, these external solvers are supported:
|
90
|
+
const SUPPORTED_SOLVERS = ['gurobi', 'scip', 'lp_solve'];
|
91
|
+
|
89
92
|
const
|
90
93
|
// Load the MILP solver (dependent on Node.js: `fs`, `os` and `path`)
|
91
94
|
MILPSolver = require('./static/scripts/linny-r-milp.js'),
|
@@ -113,13 +116,17 @@ Possible options are:
|
|
113
116
|
channel=[identifier] will start listening at the specified channel
|
114
117
|
(FUTURE OPTION)
|
115
118
|
check will report whether current version is up-to-date
|
116
|
-
|
119
|
+
data-dir=[path] will look for series data files in [path] instead of
|
120
|
+
(main)/user/data
|
121
|
+
model=[path] will load model file specified by [path]
|
117
122
|
module=[name@repo] will load model [name] from repository [repo]
|
118
123
|
(if @repo is blank, repository "local host" is used)
|
119
124
|
(FUTURE OPTION)
|
125
|
+
report=[name] will write run results to [name]-series.txt and
|
126
|
+
[name]-stats.txt in (workspace)/reports
|
120
127
|
run will run the loaded model
|
121
128
|
solver=[name] will select solver [name], or warn if not found
|
122
|
-
(name choices: Gurobi or LP_solve)
|
129
|
+
(name choices: Gurobi, SCIP or LP_solve)
|
123
130
|
user=[identifier] user ID will be used to log onto remote servers
|
124
131
|
verbose will output solver messages to the console
|
125
132
|
workspace=[path] will create workspace in [path] instead of (main)/user
|
@@ -247,11 +254,81 @@ class ConsoleMonitor {
|
|
247
254
|
} // END of class ConsoleMonitor
|
248
255
|
|
249
256
|
|
257
|
+
// NOTE: This implementation is very incomplete, still!
|
258
|
+
class ConsoleRepositoryBrowser {
|
259
|
+
constructor() {
|
260
|
+
this.repositories = [];
|
261
|
+
this.repository_index = -1;
|
262
|
+
this.module_index = -1;
|
263
|
+
// Get the repository list from the modules
|
264
|
+
this.getRepositories();
|
265
|
+
this.reset();
|
266
|
+
}
|
267
|
+
|
268
|
+
reset() {
|
269
|
+
this.visible = false;
|
270
|
+
}
|
271
|
+
|
272
|
+
get isLocalHost() {
|
273
|
+
// Returns TRUE if first repository on the list is 'local host'
|
274
|
+
return this.repositories.length > 0 &&
|
275
|
+
this.repositories[0].name === 'local host';
|
276
|
+
}
|
277
|
+
|
278
|
+
getRepositories() {
|
279
|
+
// Gets the list of repository names from the server
|
280
|
+
this.repositories.length = 0;
|
281
|
+
// @@TO DO!!
|
282
|
+
}
|
283
|
+
|
284
|
+
repositoryByName(n) {
|
285
|
+
// Returns the repository having name `n` if already known, otherwise NULL
|
286
|
+
for(let i = 0; i < this.repositories.length; i++) {
|
287
|
+
if(this.repositories[i].name === n) {
|
288
|
+
return this.repositories[i];
|
289
|
+
}
|
290
|
+
}
|
291
|
+
return null;
|
292
|
+
}
|
293
|
+
|
294
|
+
asFileName(s) {
|
295
|
+
// NOTE: asFileName is implemented as function (see below) to permit
|
296
|
+
// its use prior to instantiation of the RepositoryBrowser
|
297
|
+
return stringToFileName(s);
|
298
|
+
}
|
299
|
+
|
300
|
+
}
|
301
|
+
|
302
|
+
function stringToFileName(s) {
|
303
|
+
// Returns string `s` with whitespace converted to a single dash, and
|
304
|
+
// special characters converted to underscores
|
305
|
+
return s.normalize('NFKD').trim()
|
306
|
+
.replace(/[\s\-]+/g, '-')
|
307
|
+
.replace(/[^A-Za-z0-9_\-]/g, '_')
|
308
|
+
.replace(/^[\-\_]+|[\-\_]+$/g, '');
|
309
|
+
}
|
310
|
+
|
250
311
|
// CLASS ConsoleFileManager allows loading and saving models and diagrams, and
|
251
312
|
// handles the interaction with the MILP solver via `exec` calls and files
|
252
313
|
// stored on the modeler's computer
|
253
314
|
class ConsoleFileManager {
|
254
315
|
|
316
|
+
anyOSpath(p) {
|
317
|
+
// Helper function that converts any path notation to platform notation
|
318
|
+
// based on the predominant separator
|
319
|
+
const
|
320
|
+
s_parts = p.split('/'),
|
321
|
+
bs_parts = p.split('\\'),
|
322
|
+
parts = (s_parts.length > bs_parts.length ? s_parts : bs_parts);
|
323
|
+
// On macOS machines, paths start with a slash, so first substring is empty
|
324
|
+
if(parts[0].endsWith(':') && path.sep === '\\') {
|
325
|
+
// On Windows machines, add a backslash after the disk (if specified)
|
326
|
+
parts[0] += path.sep;
|
327
|
+
}
|
328
|
+
// Reassemble path for the OS of this machine
|
329
|
+
return path.join(...parts);
|
330
|
+
}
|
331
|
+
|
255
332
|
getRemoteData(dataset, url) {
|
256
333
|
// Gets data from a URL, or from a file on the local host
|
257
334
|
if(url === '') return;
|
@@ -281,7 +358,13 @@ class ConsoleFileManager {
|
|
281
358
|
console.log('ERROR: Invalid URL', url);
|
282
359
|
}
|
283
360
|
} else {
|
284
|
-
|
361
|
+
let fp = this.anyOSpath(url);
|
362
|
+
if(!(fp.startsWith('/') || fp.startsWith('\\') || fp.indexOf(':\\') > 0)) {
|
363
|
+
// Relative path => add path to specified data path or to the
|
364
|
+
// default location user/data
|
365
|
+
fp = path.join(SETTINGS.data_path || WORKSPACE.data, fp);
|
366
|
+
console.log('Full path: ', fp);
|
367
|
+
}
|
285
368
|
fs.readFile(fp, 'utf8', (err, data) => {
|
286
369
|
if(err) {
|
287
370
|
console.log(err);
|
@@ -354,6 +437,17 @@ class ConsoleFileManager {
|
|
354
437
|
});
|
355
438
|
}
|
356
439
|
|
440
|
+
writeStringToFile(s, fp) {
|
441
|
+
// Write string `s` to path `fp`
|
442
|
+
try {
|
443
|
+
fs.writeFileSync(fp, s);
|
444
|
+
console.log(pluralS(s.length, 'character') + ' written to file ' + fp);
|
445
|
+
} catch(err) {
|
446
|
+
console.log(err);
|
447
|
+
console.log('ERROR: Failed to write data to file ' + fp);
|
448
|
+
}
|
449
|
+
}
|
450
|
+
|
357
451
|
} // END of class ConsoleFileManager
|
358
452
|
|
359
453
|
// CLASS ConsoleReceiver defines a listener/interpreter for channel commands
|
@@ -702,7 +796,9 @@ function commandLineSettings() {
|
|
702
796
|
const settings = {
|
703
797
|
cli_name: (PLATFORM.startsWith('win') ? 'Command Prompt' : 'Terminal'),
|
704
798
|
check: false,
|
799
|
+
data_path: '',
|
705
800
|
preferred_solver: '',
|
801
|
+
report: '',
|
706
802
|
run: false,
|
707
803
|
solver: '',
|
708
804
|
solver_path: '',
|
@@ -724,7 +820,7 @@ function commandLineSettings() {
|
|
724
820
|
const av = lca.split('=');
|
725
821
|
if(av.length === 1) av.push('');
|
726
822
|
if(av[0] === 'solver') {
|
727
|
-
if(av[1]
|
823
|
+
if(SUPPORTED_SOLVERS.indexOf(av[1]) < 0) {
|
728
824
|
console.log(`WARNING: Unknown solver "${av[1]}"`);
|
729
825
|
} else {
|
730
826
|
settings.preferred_solver = av[1];
|
@@ -759,6 +855,31 @@ function commandLineSettings() {
|
|
759
855
|
console.log(`ERROR: File "${av[1]}" not found`);
|
760
856
|
process.exit();
|
761
857
|
}
|
858
|
+
} else if(av[0] === 'data-dir') {
|
859
|
+
// Set path (if valid) to override default data directory
|
860
|
+
const dp = av[1];
|
861
|
+
try {
|
862
|
+
// See whether the directory already exists
|
863
|
+
try {
|
864
|
+
fs.accessSync(dp, fs.constants.R_OK | fs.constants.W_O);
|
865
|
+
} catch(err) {
|
866
|
+
// If not, try to create it
|
867
|
+
fs.mkdirSync(dp);
|
868
|
+
console.log('Created data directory:', dp);
|
869
|
+
}
|
870
|
+
settings.data_path = dp;
|
871
|
+
} catch(err) {
|
872
|
+
console.log(err.message);
|
873
|
+
console.log('ERROR: Failed to create data directory:', dp);
|
874
|
+
}
|
875
|
+
} else if(av[0] === 'report') {
|
876
|
+
// Set report file name (if valid)
|
877
|
+
const rfn = stringToFileName(av[1]);
|
878
|
+
if(/^[A-Za-z0-9]+/.test(rfn)) {
|
879
|
+
settings.report = path.join(settings.user_dir, 'reports', rfn);
|
880
|
+
} else {
|
881
|
+
console.log(`WARNING: Invalid report file name "{$rfn}"`);
|
882
|
+
}
|
762
883
|
} else if(av[0] === 'module') {
|
763
884
|
// Add default repository is none specified
|
764
885
|
if(av[1].indexOf('@') < 0) av[1] += '@local host';
|
@@ -802,6 +923,7 @@ function commandLineSettings() {
|
|
802
923
|
// Check whether MILP solver(s) and Inkscape have been installed
|
803
924
|
const path_list = process.env.PATH.split(path.delimiter);
|
804
925
|
let gurobi_path = '',
|
926
|
+
scip_path = '',
|
805
927
|
match,
|
806
928
|
max_v = -1;
|
807
929
|
for(let i = 0; i < path_list.length; i++) {
|
@@ -810,6 +932,8 @@ function commandLineSettings() {
|
|
810
932
|
gurobi_path = path_list[i];
|
811
933
|
max_v = parseInt(match[1]);
|
812
934
|
}
|
935
|
+
match = path_list[i].match(/[\/\\]scip[^\/\\]+[\/\\]bin/i);
|
936
|
+
if(match) scip_path = path_list[i];
|
813
937
|
match = path_list[i].match(/inkscape/i);
|
814
938
|
if(match) settings.inkscape = path_list[i];
|
815
939
|
}
|
@@ -840,6 +964,23 @@ function commandLineSettings() {
|
|
840
964
|
'WARNING: Failed to access the Gurobi command line application');
|
841
965
|
}
|
842
966
|
}
|
967
|
+
// Check if scip(.exe) exists in its directory
|
968
|
+
let sp = path.join(scip_path, 'scip' + (PLATFORM.startsWith('win') ? '.exe' : ''));
|
969
|
+
const need_scip = !settings.solver || settings.preferred_solver === 'scip';
|
970
|
+
try {
|
971
|
+
fs.accessSync(sp, fs.constants.X_OK);
|
972
|
+
console.log('Path to SCIP:', sp);
|
973
|
+
if(need_scip) {
|
974
|
+
settings.solver = 'scip';
|
975
|
+
settings.solver_path = sp;
|
976
|
+
}
|
977
|
+
} catch(err) {
|
978
|
+
// Only report error if SCIP is needed
|
979
|
+
if(need_scip) {
|
980
|
+
console.log(err.message);
|
981
|
+
console.log('WARNING: SCIP application not found in', sp);
|
982
|
+
}
|
983
|
+
}
|
843
984
|
// Check if lp_solve(.exe) exists in main directory
|
844
985
|
const
|
845
986
|
sp = path.join(WORKING_DIRECTORY,
|
@@ -882,10 +1023,13 @@ function createWorkspace() {
|
|
882
1023
|
}
|
883
1024
|
// Define the sub-directory paths
|
884
1025
|
const ws = {
|
1026
|
+
autosave: path.join(SETTINGS.user_dir, 'autosave'),
|
885
1027
|
channel: path.join(SETTINGS.user_dir, 'channel'),
|
886
1028
|
callback: path.join(SETTINGS.user_dir, 'callback'),
|
1029
|
+
data: path.join(SETTINGS.user_dir, 'data'),
|
887
1030
|
diagrams: path.join(SETTINGS.user_dir, 'diagrams'),
|
888
1031
|
modules: path.join(SETTINGS.user_dir, 'modules'),
|
1032
|
+
reports: path.join(SETTINGS.user_dir, 'reports'),
|
889
1033
|
solver_output: path.join(SETTINGS.user_dir, 'solver'),
|
890
1034
|
};
|
891
1035
|
// Create these sub-directories if not aready there
|
@@ -968,7 +1112,7 @@ PROMPTER.questionPrompt = (str) => {
|
|
968
1112
|
// Initialize the Linny-R console components as global variables
|
969
1113
|
global.UI = new Controller();
|
970
1114
|
global.VM = new VirtualMachine();
|
971
|
-
|
1115
|
+
global.REPOSITORY_BROWSER = new ConsoleRepositoryBrowser();
|
972
1116
|
global.FILE_MANAGER = new ConsoleFileManager();
|
973
1117
|
global.DATASET_MANAGER = new DatasetManager();
|
974
1118
|
global.CHART_MANAGER = new ChartManager();
|
@@ -990,8 +1134,19 @@ if(SETTINGS.model_path) {
|
|
990
1134
|
MONITOR.show_log = SETTINGS.verbose;
|
991
1135
|
VM.callback = () => {
|
992
1136
|
const od = model.outputData;
|
993
|
-
|
994
|
-
|
1137
|
+
// Output data is two-string list [time series, statistics]
|
1138
|
+
if(SETTINGS.report) {
|
1139
|
+
// Output time series
|
1140
|
+
FILE_MANAGER.writeStringToFile(od[0],
|
1141
|
+
SETTINGS.report + '-series.txt');
|
1142
|
+
// Output statistics
|
1143
|
+
FILE_MANAGER.writeStringToFile(od[1],
|
1144
|
+
SETTINGS.report + '-stats.txt');
|
1145
|
+
} else {
|
1146
|
+
// Output strings to console
|
1147
|
+
console.log(od[0]);
|
1148
|
+
console.log(od[1]);
|
1149
|
+
}
|
995
1150
|
VM.callback = null;
|
996
1151
|
};
|
997
1152
|
VM.solveModel();
|
package/package.json
CHANGED
package/server.js
CHANGED
@@ -122,6 +122,8 @@ function checkNodeModule(name) {
|
|
122
122
|
}
|
123
123
|
}
|
124
124
|
|
125
|
+
// Currently, these external solvers are supported:
|
126
|
+
const SUPPORTED_SOLVERS = ['gurobi', 'scip', 'lp_solve'];
|
125
127
|
|
126
128
|
// Load class MILPSolver
|
127
129
|
const MILPSolver = require('./static/scripts/linny-r-milp.js');
|
@@ -847,20 +849,19 @@ function repoDelete(res, name, file) {
|
|
847
849
|
// Dataset dialog
|
848
850
|
|
849
851
|
function anyOSpath(p) {
|
850
|
-
// Helper function that converts
|
851
|
-
//
|
852
|
-
|
853
|
-
|
852
|
+
// Helper function that converts any path notation to platform notation
|
853
|
+
// based on the predominant separator
|
854
|
+
const
|
855
|
+
s_parts = p.split('/'),
|
856
|
+
bs_parts = p.split('\\'),
|
857
|
+
parts = (s_parts.length > bs_parts.length ? s_parts : bs_parts);
|
854
858
|
// On macOS machines, paths start with a slash, so first substring is empty
|
855
|
-
if(
|
856
|
-
// In that case, add the leading slash
|
857
|
-
return '/' + path.join(...p);
|
858
|
-
} else if(p[0].endsWith(':') && path.sep === '\\') {
|
859
|
+
if(parts[0].endsWith(':') && path.sep === '\\') {
|
859
860
|
// On Windows machines, add a backslash after the disk (if specified)
|
860
|
-
|
861
|
+
parts[0] += path.sep;
|
861
862
|
}
|
862
863
|
// Reassemble path for the OS of this machine
|
863
|
-
return path.join(...
|
864
|
+
return path.join(...parts);
|
864
865
|
}
|
865
866
|
|
866
867
|
function loadData(res, url) {
|
@@ -881,7 +882,11 @@ function loadData(res, url) {
|
|
881
882
|
servePlainText(res, `ERROR: Invalid URL <tt>${url}</tt>`);
|
882
883
|
}
|
883
884
|
} else {
|
884
|
-
|
885
|
+
let fp = anyOSpath(url);
|
886
|
+
if(!(fp.startsWith('/') || fp.startsWith('\\') || fp.indexOf(':\\') > 0)) {
|
887
|
+
// Relative path => add path to user/data directory
|
888
|
+
fp = path.join(WORKSPACE.data, fp);
|
889
|
+
}
|
885
890
|
fs.readFile(fp, 'utf8', (err, data) => {
|
886
891
|
if(err) {
|
887
892
|
console.log(err);
|
@@ -1419,7 +1424,7 @@ function commandLineSettings() {
|
|
1419
1424
|
settings.port = n;
|
1420
1425
|
}
|
1421
1426
|
} else if(av[0] === 'solver') {
|
1422
|
-
if(av[1]
|
1427
|
+
if(SUPPORTED_SOLVERS.indexOf(av[1]) < 0) {
|
1423
1428
|
console.log(`WARNING: Unknown solver "${av[1]}"`);
|
1424
1429
|
} else {
|
1425
1430
|
settings.preferred_solver = av[1];
|
@@ -1452,6 +1457,7 @@ function commandLineSettings() {
|
|
1452
1457
|
// Check whether MILP solver(s) and Inkscape have been installed
|
1453
1458
|
const path_list = process.env.PATH.split(path.delimiter);
|
1454
1459
|
let gurobi_path = '',
|
1460
|
+
scip_path = '',
|
1455
1461
|
match,
|
1456
1462
|
max_v = -1;
|
1457
1463
|
for(let i = 0; i < path_list.length; i++) {
|
@@ -1460,6 +1466,8 @@ function commandLineSettings() {
|
|
1460
1466
|
gurobi_path = path_list[i];
|
1461
1467
|
max_v = parseInt(match[1]);
|
1462
1468
|
}
|
1469
|
+
match = path_list[i].match(/[\/\\]scip[^\/\\]+[\/\\]bin/i);
|
1470
|
+
if(match) scip_path = path_list[i];
|
1463
1471
|
match = path_list[i].match(/inkscape/i);
|
1464
1472
|
if(match) settings.inkscape = path_list[i];
|
1465
1473
|
}
|
@@ -1490,11 +1498,27 @@ function commandLineSettings() {
|
|
1490
1498
|
'WARNING: Failed to access the Gurobi command line application');
|
1491
1499
|
}
|
1492
1500
|
}
|
1501
|
+
// Check if scip(.exe) exists in its directory
|
1502
|
+
let sp = path.join(scip_path, 'scip' + (PLATFORM.startsWith('win') ? '.exe' : ''));
|
1503
|
+
const need_scip = !settings.solver || settings.preferred_solver === 'scip';
|
1504
|
+
try {
|
1505
|
+
fs.accessSync(sp, fs.constants.X_OK);
|
1506
|
+
console.log('Path to SCIP:', sp);
|
1507
|
+
if(need_scip) {
|
1508
|
+
settings.solver = 'scip';
|
1509
|
+
settings.solver_path = sp;
|
1510
|
+
}
|
1511
|
+
} catch(err) {
|
1512
|
+
// Only report error if SCIP is needed
|
1513
|
+
if(need_scip) {
|
1514
|
+
console.log(err.message);
|
1515
|
+
console.log('WARNING: SCIP application not found in', sp);
|
1516
|
+
}
|
1517
|
+
}
|
1493
1518
|
// Check if lp_solve(.exe) exists in working directory
|
1494
|
-
|
1495
|
-
|
1496
|
-
|
1497
|
-
need_lps = !settings.solver || settings.preferred_solver === 'lp_solve';
|
1519
|
+
sp = path.join(WORKING_DIRECTORY,
|
1520
|
+
'lp_solve' + (PLATFORM.startsWith('win') ? '.exe' : ''));
|
1521
|
+
const need_lps = !settings.solver || settings.preferred_solver === 'lp_solve';
|
1498
1522
|
try {
|
1499
1523
|
fs.accessSync(sp, fs.constants.X_OK);
|
1500
1524
|
console.log('Path to LP_solve:', sp);
|
@@ -1568,6 +1592,7 @@ function createWorkspace() {
|
|
1568
1592
|
autosave: path.join(SETTINGS.user_dir, 'autosave'),
|
1569
1593
|
channel: path.join(SETTINGS.user_dir, 'channel'),
|
1570
1594
|
callback: path.join(SETTINGS.user_dir, 'callback'),
|
1595
|
+
data: path.join(SETTINGS.user_dir, 'data'),
|
1571
1596
|
diagrams: path.join(SETTINGS.user_dir, 'diagrams'),
|
1572
1597
|
modules: path.join(SETTINGS.user_dir, 'modules'),
|
1573
1598
|
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
|
-
|
1708
|
-
|
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">
|
@@ -2801,7 +2804,65 @@ where X can be one or several of these letters: ABCDELPQ">
|
|
2801
2804
|
</div>
|
2802
2805
|
</div>
|
2803
2806
|
</div>
|
2804
|
-
|
2807
|
+
|
2808
|
+
<!-- the PASTE dialog prompts for a prefix and parameter bindings -->
|
2809
|
+
<div id="paste-modal" class="modal">
|
2810
|
+
<div id="paste-dlg" class="inp-dlg">
|
2811
|
+
<div class="dlg-title">
|
2812
|
+
Name conflict resolution strategy
|
2813
|
+
<img class="cancel-btn" src="images/cancel.png">
|
2814
|
+
<img class="ok-btn" src="images/ok.png">
|
2815
|
+
</div>
|
2816
|
+
<div id="paste-ftp" class="paste-option">
|
2817
|
+
<div id="paste-ftp-box" class="box checked"></div>
|
2818
|
+
<div class="paste-tactic">
|
2819
|
+
Change <span id="paste-from-prefix"></span>…
|
2820
|
+
to <span id="paste-to-prefix"></span>…
|
2821
|
+
</div>
|
2822
|
+
</div>
|
2823
|
+
<div id="paste-fta" class="paste-option">
|
2824
|
+
<div id="paste-fta-box" class="box checked"></div>
|
2825
|
+
<div class="paste-tactic">
|
2826
|
+
Change actor <span id="paste-from-actor"></span>
|
2827
|
+
to <span id="paste-to-actor"></span>
|
2828
|
+
</div>
|
2829
|
+
</div>
|
2830
|
+
<div class="paste-option">
|
2831
|
+
<div id="paste-a-box" class="box checked"></div>
|
2832
|
+
<div class="paste-tactic">
|
2833
|
+
<label>If no actor, add:</label>
|
2834
|
+
<input id="paste-actor" type="text"
|
2835
|
+
placeholder="(no actor)" autocomplete="off">
|
2836
|
+
</div>
|
2837
|
+
</div>
|
2838
|
+
<div class="paste-option">
|
2839
|
+
<div id="paste-p-box" class="box checked"></div>
|
2840
|
+
<div class="paste-tactic">
|
2841
|
+
<label>Add prefix:</label>
|
2842
|
+
<input id="paste-prefix" type="text"
|
2843
|
+
placeholder="(none)" autocomplete="off">
|
2844
|
+
</div>
|
2845
|
+
</div>
|
2846
|
+
<div class="paste-option">
|
2847
|
+
<div id="paste-inc-box" class="box checked"></div>
|
2848
|
+
<div class="paste-tactic">
|
2849
|
+
Auto-increment tail number
|
2850
|
+
</div>
|
2851
|
+
</div>
|
2852
|
+
<div class="paste-option">
|
2853
|
+
<div id="paste-near-box" class="box checked"></div>
|
2854
|
+
<div class="paste-tactic">
|
2855
|
+
Link to nearest eligible node
|
2856
|
+
</div>
|
2857
|
+
</div>
|
2858
|
+
<div style="font-weight: bold; margin:4px 2px 2px 2px">
|
2859
|
+
Mapping of nodes to link from/to:
|
2860
|
+
</div>
|
2861
|
+
<div id="paste-scroll-area">
|
2862
|
+
</div>
|
2863
|
+
</div>
|
2864
|
+
</div>
|
2865
|
+
|
2805
2866
|
<!--- rotating Linny-R logo icon -->
|
2806
2867
|
<div id="rotating-icon">
|
2807
2868
|
<div>
|
package/static/linny-r.css
CHANGED
@@ -2468,12 +2468,18 @@ td.equation-expression {
|
|
2468
2468
|
#series-remote {
|
2469
2469
|
position: absolute;
|
2470
2470
|
top: 134px;
|
2471
|
-
left:
|
2472
|
-
width: calc(100% -
|
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
|
|
@@ -4047,6 +4053,7 @@ span.sd-clear {
|
|
4047
4053
|
height: 111px;
|
4048
4054
|
}
|
4049
4055
|
|
4056
|
+
#paste-dlg,
|
4050
4057
|
#include-dlg {
|
4051
4058
|
width: 320px;
|
4052
4059
|
height: min-content;
|
@@ -4072,6 +4079,7 @@ span.sd-clear {
|
|
4072
4079
|
margin-left: 9px;
|
4073
4080
|
}
|
4074
4081
|
|
4082
|
+
#paste-scroll-area,
|
4075
4083
|
#include-scroll-area {
|
4076
4084
|
margin: 2px;
|
4077
4085
|
height: min-content;
|
@@ -4082,6 +4090,60 @@ span.sd-clear {
|
|
4082
4090
|
overflow-y: auto;
|
4083
4091
|
}
|
4084
4092
|
|
4093
|
+
div.paste-tactic {
|
4094
|
+
display: inline-block;
|
4095
|
+
vertical-align: top;
|
4096
|
+
padding-top: 3px;
|
4097
|
+
}
|
4098
|
+
|
4099
|
+
div.paste-select::before {
|
4100
|
+
content: ' \1F872';
|
4101
|
+
margin-right: 3px;
|
4102
|
+
}
|
4103
|
+
|
4104
|
+
div.paste-select {
|
4105
|
+
display: inline-block;
|
4106
|
+
font-size: 12px;
|
4107
|
+
max-width: calc(100% - 25px);
|
4108
|
+
margin: 2px;
|
4109
|
+
}
|
4110
|
+
|
4111
|
+
#paste-prefix,
|
4112
|
+
#paste-actor {
|
4113
|
+
width: 150px;
|
4114
|
+
font-size: 12px;
|
4115
|
+
height: 17px !important;
|
4116
|
+
margin-left: 1px;
|
4117
|
+
}
|
4118
|
+
|
4119
|
+
#paste-from-prefix,
|
4120
|
+
#paste-to-prefix,
|
4121
|
+
#paste-from-actor,
|
4122
|
+
#paste-to-actor {
|
4123
|
+
max-width: 220px;
|
4124
|
+
overflow: clip;
|
4125
|
+
white-space: nowrap;
|
4126
|
+
text-overflow: ellipsis;
|
4127
|
+
}
|
4128
|
+
|
4129
|
+
#paste-from-prefix {
|
4130
|
+
color: #6060c0;
|
4131
|
+
}
|
4132
|
+
|
4133
|
+
#paste-to-prefix {
|
4134
|
+
color: #0000c0;
|
4135
|
+
}
|
4136
|
+
|
4137
|
+
#paste-from-actor {
|
4138
|
+
font-style: italic;
|
4139
|
+
color: #8050a0;
|
4140
|
+
}
|
4141
|
+
|
4142
|
+
#paste-to-actor {
|
4143
|
+
font-style: italic;
|
4144
|
+
color: #600080;
|
4145
|
+
}
|
4146
|
+
|
4085
4147
|
tr.i-param {
|
4086
4148
|
font-style: italic;
|
4087
4149
|
border: 1px solid Silver;
|
@@ -136,7 +136,7 @@ class Controller {
|
|
136
136
|
ACTOR_PROPS: ['weight', 'comments'],
|
137
137
|
CLUSTER_PROPS: ['comments', 'collapsed', 'ignore'],
|
138
138
|
PROCESS_PROPS: ['comments', 'lower_bound', 'upper_bound', 'initial_level',
|
139
|
-
'
|
139
|
+
'pace_expression', 'equal_bounds', 'level_to_zero', 'integer_level', 'collapsed'],
|
140
140
|
PRODUCT_PROPS: ['comments', 'lower_bound', 'upper_bound', 'initial_level',
|
141
141
|
'scale_unit', 'equal_bounds', 'price', 'is_source', 'is_sink', 'is_buffer',
|
142
142
|
'is_data', 'integer_level', 'no_slack'],
|
@@ -316,6 +316,21 @@ class Controller {
|
|
316
316
|
return pan;
|
317
317
|
}
|
318
318
|
|
319
|
+
sharedPrefix(n1, n2) {
|
320
|
+
const
|
321
|
+
pan1 = this.prefixesAndName(n1),
|
322
|
+
pan2 = this.prefixesAndName(n2),
|
323
|
+
l = Math.min(pan1.length - 1, pan2.length - 1),
|
324
|
+
shared = [];
|
325
|
+
let i = 0;
|
326
|
+
while(i < l && ciCompare(pan1[i], pan2[i]) === 0) {
|
327
|
+
// NOTE: if identical except for case, prefer "Abc" over "aBc"
|
328
|
+
shared.push(pan1[i] < pan2[i] ? pan1[i] : pan2[i]);
|
329
|
+
i++;
|
330
|
+
}
|
331
|
+
return shared.join(this.PREFIXER);
|
332
|
+
}
|
333
|
+
|
319
334
|
nameToID(name) {
|
320
335
|
// Returns a name in lower case with link arrow replaced by three
|
321
336
|
// underscores, constraint link arrow by four underscores, and spaces
|