linny-r 1.6.0 → 1.6.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/package.json +1 -1
- package/server.js +27 -6
- package/static/scripts/linny-r-gui-controller.js +47 -35
- package/static/scripts/linny-r-gui-documentation-manager.js +36 -32
- package/static/scripts/linny-r-gui-equation-manager.js +10 -7
- package/static/scripts/linny-r-gui-experiment-manager.js +14 -3
- package/static/scripts/linny-r-gui-finder.js +1 -0
- package/static/scripts/linny-r-gui-paper.js +25 -23
- package/static/scripts/linny-r-milp.js +18 -15
- package/static/scripts/linny-r-model.js +143 -91
- package/static/scripts/linny-r-utils.js +33 -8
- package/static/scripts/linny-r-vm.js +250 -60
package/package.json
CHANGED
package/server.js
CHANGED
@@ -1521,7 +1521,7 @@ function connectionErrorText(msg) {
|
|
1521
1521
|
//
|
1522
1522
|
|
1523
1523
|
function commandLineSettings() {
|
1524
|
-
// Sets default settings, and then checks the command line arguments
|
1524
|
+
// Sets default settings, and then checks the command line arguments.
|
1525
1525
|
const settings = {
|
1526
1526
|
cli_name: (PLATFORM.startsWith('win') ? 'Command Prompt' : 'Terminal'),
|
1527
1527
|
inkscape: '',
|
@@ -1533,6 +1533,22 @@ function commandLineSettings() {
|
|
1533
1533
|
solver_path: '',
|
1534
1534
|
user_dir: path.join(WORKING_DIRECTORY, 'user')
|
1535
1535
|
};
|
1536
|
+
const
|
1537
|
+
cmd = process.argv[0],
|
1538
|
+
app = (cmd.endsWith('node.exe') ? 'node' : 'linny-r'),
|
1539
|
+
usage = `Usage: ${app} server [options]
|
1540
|
+
|
1541
|
+
Possible options are:
|
1542
|
+
dpi=[number] will make InkScape render SVGs in the specified resolution
|
1543
|
+
help will display these command line options
|
1544
|
+
launch will open the Linny-R GUI in a browser window
|
1545
|
+
port=[number] will listen at the specified port number
|
1546
|
+
(default is 5050; number must be unique for each server)
|
1547
|
+
solver=[name] will select solver [name], or warn if not found
|
1548
|
+
(name choices: Gurobi, CPLEX, SCIP or LP_solve)
|
1549
|
+
verbose will output solver messages to the console
|
1550
|
+
workspace=[path] will create workspace in [path] instead of (Linny-R)/user
|
1551
|
+
`;
|
1536
1552
|
for(let i = 2; i < process.argv.length; i++) {
|
1537
1553
|
const lca = process.argv[i].toLowerCase();
|
1538
1554
|
if(lca === 'launch') {
|
@@ -1541,7 +1557,7 @@ function commandLineSettings() {
|
|
1541
1557
|
const av = lca.split('=');
|
1542
1558
|
if(av.length === 1) av.push('');
|
1543
1559
|
if(av[0] === 'port') {
|
1544
|
-
// Accept any number greater than or equal to 1024
|
1560
|
+
// Accept any number greater than or equal to 1024.
|
1545
1561
|
const n = parseInt(av[1]);
|
1546
1562
|
if(isNaN(n) || n < 1024) {
|
1547
1563
|
console.log(`WARNING: Invalid port number ${av[1]}`);
|
@@ -1555,7 +1571,7 @@ function commandLineSettings() {
|
|
1555
1571
|
settings.preferred_solver = av[1];
|
1556
1572
|
}
|
1557
1573
|
} else if(av[0] === 'dpi') {
|
1558
|
-
// Accept any number greater than or equal to 1024
|
1574
|
+
// Accept any number greater than or equal to 1024.
|
1559
1575
|
const n = parseInt(av[1]);
|
1560
1576
|
if(isNaN(n) || n > 1200) {
|
1561
1577
|
console.log(`WARNING: Invalid resolution ${av[1]} (max. 1200 dpi)`);
|
@@ -1563,7 +1579,7 @@ function commandLineSettings() {
|
|
1563
1579
|
settings.dpi = n;
|
1564
1580
|
}
|
1565
1581
|
} else if(av[0] === 'workspace') {
|
1566
|
-
// User directory must be READ/WRITE-accessible
|
1582
|
+
// User directory must be READ/WRITE-accessible.
|
1567
1583
|
try {
|
1568
1584
|
fs.accessSync(av[1], fs.constants.R_OK | fs.constants.W_O);
|
1569
1585
|
} catch(err) {
|
@@ -1571,10 +1587,15 @@ function commandLineSettings() {
|
|
1571
1587
|
process.exit();
|
1572
1588
|
}
|
1573
1589
|
settings.user_dir = av[1];
|
1590
|
+
} else if(av[0] === 'help') {
|
1591
|
+
// Print command line options.
|
1592
|
+
console.log(usage);
|
1593
|
+
process.exit();
|
1574
1594
|
} else {
|
1575
|
-
// Terminate script
|
1595
|
+
// Terminate script.
|
1576
1596
|
console.log(
|
1577
|
-
`ERROR: Invalid command line argument "${process.argv[i]}"`);
|
1597
|
+
`ERROR: Invalid command line argument "${process.argv[i]}"\n`);
|
1598
|
+
console.log(usage);
|
1578
1599
|
process.exit();
|
1579
1600
|
}
|
1580
1601
|
}
|
@@ -2935,8 +2935,8 @@ class GUIController extends Controller {
|
|
2935
2935
|
}
|
2936
2936
|
const err = MODEL.cloneSelection(prefix, actor_name, renumber);
|
2937
2937
|
if(err) {
|
2938
|
-
// Something went wrong, so do not hide the modal, but focus on
|
2939
|
-
// 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.
|
2940
2940
|
const el = md.element(err);
|
2941
2941
|
if(el) {
|
2942
2942
|
el.focus();
|
@@ -2956,7 +2956,7 @@ class GUIController extends Controller {
|
|
2956
2956
|
}
|
2957
2957
|
|
2958
2958
|
copySelection() {
|
2959
|
-
// Save selection as XML in local storage of the browser
|
2959
|
+
// Save selection as XML in local storage of the browser.
|
2960
2960
|
const xml = MODEL.selectionAsXML;
|
2961
2961
|
if(xml) {
|
2962
2962
|
window.localStorage.setItem('Linny-R-selection-XML', xml);
|
@@ -2967,20 +2967,22 @@ class GUIController extends Controller {
|
|
2967
2967
|
}
|
2968
2968
|
|
2969
2969
|
get canPaste() {
|
2970
|
+
// Return TRUE if the browser has a recent selection-as-XML object
|
2971
|
+
// in its local storage.
|
2970
2972
|
const xml = window.localStorage.getItem('Linny-R-selection-XML');
|
2971
2973
|
if(xml) {
|
2972
2974
|
const timestamp = xml.match(/<copy timestamp="(\d+)"/);
|
2973
2975
|
if(timestamp) {
|
2974
2976
|
if(Date.now() - parseInt(timestamp[1]) < 8*3600000) return true;
|
2975
2977
|
}
|
2976
|
-
// Remove XML from local storage if older than 8 hours
|
2978
|
+
// Remove XML from local storage if older than 8 hours.
|
2977
2979
|
window.localStorage.removeItem('Linny-R-selection-XML');
|
2978
2980
|
}
|
2979
2981
|
return false;
|
2980
2982
|
}
|
2981
2983
|
|
2982
2984
|
promptForMapping(mapping) {
|
2983
|
-
// Prompt user to specify name conflict resolution strategy
|
2985
|
+
// Prompt user to specify name conflict resolution strategy.
|
2984
2986
|
const md = this.paste_modal;
|
2985
2987
|
md.mapping = mapping;
|
2986
2988
|
md.element('from-prefix').innerText = mapping.from_prefix || '';
|
@@ -3000,7 +3002,8 @@ class GUIController extends Controller {
|
|
3000
3002
|
if(tc.length) {
|
3001
3003
|
sl.push('<div style="font-weight: bold; margin:4px 2px 2px 2px">',
|
3002
3004
|
'Names for top-level clusters:</div>');
|
3003
|
-
|
3005
|
+
const sll = sl.length;
|
3006
|
+
// Add text inputs for selected cluster nodes.
|
3004
3007
|
for(let i = 0; i < tc.length; i++) {
|
3005
3008
|
const
|
3006
3009
|
ti = mapping.top_clusters[tc[i]],
|
@@ -3012,11 +3015,14 @@ class GUIController extends Controller {
|
|
3012
3015
|
'" type="text" style="', state, 'font-size: 12px" value="',
|
3013
3016
|
ti, '"></div></div>');
|
3014
3017
|
}
|
3018
|
+
// Remove header when no items were added.
|
3019
|
+
if(sl.length === sll) sl.pop();
|
3015
3020
|
}
|
3016
3021
|
if(ft.length) {
|
3017
3022
|
sl.push('<div style="font-weight: bold; margin:4px 2px 2px 2px">',
|
3018
3023
|
'Mapping of nodes to link from/to:</div>');
|
3019
|
-
|
3024
|
+
const sll = sl.length;
|
3025
|
+
// Add selectors for unresolved FROM/TO nodes.
|
3020
3026
|
for(let i = 0; i < ft.length; i++) {
|
3021
3027
|
const ti = mapping.from_to[ft[i]];
|
3022
3028
|
if(ft[i] === ti) {
|
@@ -3036,15 +3042,17 @@ class GUIController extends Controller {
|
|
3036
3042
|
sl.push('</div>');
|
3037
3043
|
}
|
3038
3044
|
}
|
3045
|
+
// Remove header when no items were added.
|
3046
|
+
if(sl.length === sll) sl.pop();
|
3039
3047
|
}
|
3040
3048
|
md.element('scroll-area').innerHTML = sl.join('');
|
3041
|
-
// Open dialog, which will call pasteSelection(...) on OK
|
3049
|
+
// Open dialog, which will call pasteSelection(...) on OK.
|
3042
3050
|
this.paste_modal.show();
|
3043
3051
|
}
|
3044
3052
|
|
3045
3053
|
setPasteMapping() {
|
3046
|
-
//
|
3047
|
-
//
|
3054
|
+
// Update the paste mapping as specified by the modeler and then
|
3055
|
+
// proceed to paste.
|
3048
3056
|
const
|
3049
3057
|
md = this.paste_modal,
|
3050
3058
|
mapping = Object.assign(md.mapping, {}),
|
@@ -3071,7 +3079,7 @@ class GUIController extends Controller {
|
|
3071
3079
|
pasteSelection(mapping={}) {
|
3072
3080
|
// If selection has been saved as XML in local storage, test to
|
3073
3081
|
// see whether PASTE would result in name conflicts, and if so,
|
3074
|
-
// open the name conflict resolution window
|
3082
|
+
// open the name conflict resolution window.
|
3075
3083
|
let xml = window.localStorage.getItem('Linny-R-selection-XML');
|
3076
3084
|
try {
|
3077
3085
|
xml = parseXML(xml);
|
@@ -3095,7 +3103,7 @@ class GUIController extends Controller {
|
|
3095
3103
|
// AUXILIARY FUNCTIONS
|
3096
3104
|
|
3097
3105
|
function fullName(node) {
|
3098
|
-
//
|
3106
|
+
// Return full entity name inferred from XML node data.
|
3099
3107
|
if(node.nodeName === 'from-to' || node.nodeName === 'selc') {
|
3100
3108
|
const
|
3101
3109
|
n = xmlDecoded(nodeParameterValue(node, 'name')),
|
@@ -3134,12 +3142,12 @@ class GUIController extends Controller {
|
|
3134
3142
|
}
|
3135
3143
|
|
3136
3144
|
function nameAndActor(name) {
|
3137
|
-
//
|
3138
|
-
//
|
3145
|
+
// Return tuple [entity name, actor name] if `name` ends with a
|
3146
|
+
// parenthesized string that identifies an actor in the selection.
|
3139
3147
|
const ai = name.lastIndexOf(' (');
|
3140
3148
|
if(ai < 0) return [name, ''];
|
3141
3149
|
let actor = name.slice(ai + 2, -1);
|
3142
|
-
// Test whether parenthesized string denotes an actor
|
3150
|
+
// Test whether parenthesized string denotes an actor.
|
3143
3151
|
if(actor_names.indexOf(actor) >= 0 || actor === mapping.actor ||
|
3144
3152
|
actor === mapping.from_actor || actor === mapping.to_actor) {
|
3145
3153
|
name = name.substring(0, ai);
|
@@ -3150,8 +3158,8 @@ class GUIController extends Controller {
|
|
3150
3158
|
}
|
3151
3159
|
|
3152
3160
|
function mappedName(n) {
|
3153
|
-
// Returns full name `n` modified according to the mapping
|
3154
|
-
// NOTE:
|
3161
|
+
// Returns full name `n` modified according to the mapping.
|
3162
|
+
// NOTE: Links and constraints require two mappings (recursion!).
|
3155
3163
|
if(n.indexOf(UI.LINK_ARROW) > 0) {
|
3156
3164
|
const ft = n.split(UI.LINK_ARROW);
|
3157
3165
|
return mappedName(ft[0]) + UI.LINK_ARROW + mappedName(ft[1]);
|
@@ -3174,7 +3182,7 @@ class GUIController extends Controller {
|
|
3174
3182
|
const ai = n.lastIndexOf(mapping.from_actor);
|
3175
3183
|
if(ai > 0) return n.substring(0, ai) + mapping.to_actor;
|
3176
3184
|
}
|
3177
|
-
// NOTE: specified actor cannot override existing actor
|
3185
|
+
// NOTE: specified actor cannot override existing actor.
|
3178
3186
|
if(mapping.actor && !nameAndActor(n)[1]) {
|
3179
3187
|
return `${n} (${mapping.actor})`;
|
3180
3188
|
}
|
@@ -3191,23 +3199,27 @@ class GUIController extends Controller {
|
|
3191
3199
|
if(mapping.from_to && mapping.from_to[n]) {
|
3192
3200
|
return mapping.from_to[n];
|
3193
3201
|
}
|
3194
|
-
// No mapping => return original name
|
3202
|
+
// No mapping => return original name.
|
3195
3203
|
return n;
|
3196
3204
|
}
|
3197
3205
|
|
3198
3206
|
function nameConflicts(node) {
|
3199
3207
|
// Maps names of entities defined by the child nodes of `node`
|
3200
|
-
// while detecting name conflicts
|
3208
|
+
// while detecting name conflicts.
|
3201
3209
|
for(let i = 0; i < node.childNodes.length; i++) {
|
3202
3210
|
const c = node.childNodes[i];
|
3203
3211
|
if(c.nodeName !== 'link' && c.nodeName !== 'constraint') {
|
3204
3212
|
const
|
3205
3213
|
fn = fullName(c),
|
3206
|
-
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);
|
3207
3219
|
// Name conflict occurs when the mapped name is already in use
|
3208
3220
|
// in the target model, or when the original name is mapped onto
|
3209
|
-
// different names (this might occur due to modeler input)
|
3210
|
-
if(
|
3221
|
+
// different names (this might occur due to modeler input).
|
3222
|
+
if((obj && !add_pp) || (name_map[fn] && name_map[fn] !== mn)) {
|
3211
3223
|
addDistinct(fn, name_conflicts);
|
3212
3224
|
} else {
|
3213
3225
|
name_map[fn] = mn;
|
@@ -3217,10 +3229,10 @@ class GUIController extends Controller {
|
|
3217
3229
|
}
|
3218
3230
|
|
3219
3231
|
function addEntityFromNode(node) {
|
3220
|
-
// Adds entity to model based on XML node data and mapping
|
3221
|
-
// NOTE:
|
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
|
3222
3234
|
// already exists; name conflicts accross entity types may occur
|
3223
|
-
// and result in error messages
|
3235
|
+
// and result in error messages.
|
3224
3236
|
const
|
3225
3237
|
et = node.nodeName,
|
3226
3238
|
fn = fullName(node),
|
@@ -3282,17 +3294,17 @@ class GUIController extends Controller {
|
|
3282
3294
|
sp = this.sharedPrefix(cn, fcn),
|
3283
3295
|
fpn = (cn === UI.TOP_CLUSTER_NAME ? '' : cn.replace(sp, '')),
|
3284
3296
|
tpn = (fcn === UI.TOP_CLUSTER_NAME ? '' : fcn.replace(sp, ''));
|
3285
|
-
// Infer mapping from XML data and focal cluster name & actor name
|
3297
|
+
// Infer mapping from XML data and focal cluster name & actor name.
|
3286
3298
|
mapping.shared_prefix = sp;
|
3287
3299
|
mapping.from_prefix = (fpn ? sp + fpn + UI.PREFIXER : sp);
|
3288
3300
|
mapping.to_prefix = (tpn ? sp + tpn + UI.PREFIXER : sp);
|
3289
3301
|
mapping.from_actor = (ca === UI.NO_ACTOR ? '' : ca);
|
3290
3302
|
mapping.to_actor = (fca === UI.NO_ACTOR ? '' : fca);
|
3291
|
-
// Prompt for mapping when pasting to the same model and cluster
|
3303
|
+
// Prompt for mapping when pasting to the same model and cluster.
|
3292
3304
|
if(parseInt(mts) === MODEL.time_created.getTime() &&
|
3293
3305
|
ca === fca && mapping.from_prefix === mapping.to_prefix &&
|
3294
3306
|
!(mapping.prefix || mapping.actor || mapping.increment)) {
|
3295
|
-
// Prompt for names of selected cluster nodes
|
3307
|
+
// Prompt for names of selected cluster nodes.
|
3296
3308
|
if(selc_node.childNodes.length && !mapping.prefix) {
|
3297
3309
|
mapping.top_clusters = {};
|
3298
3310
|
for(let i = 0; i < selc_node.childNodes.length; i++) {
|
@@ -3307,7 +3319,7 @@ class GUIController extends Controller {
|
|
3307
3319
|
return;
|
3308
3320
|
}
|
3309
3321
|
// Also prompt if FROM and/or TO nodes are not selected, and map to
|
3310
|
-
// existing entities
|
3322
|
+
// existing entities.
|
3311
3323
|
if(from_tos_node.childNodes.length && !mapping.from_to) {
|
3312
3324
|
const
|
3313
3325
|
ft_map = {},
|
@@ -3323,7 +3335,7 @@ class GUIController extends Controller {
|
|
3323
3335
|
'Data' : nodeParameterValue(c, 'type'));
|
3324
3336
|
}
|
3325
3337
|
}
|
3326
|
-
// Prompt only for FROM/TO nodes that map to existing nodes
|
3338
|
+
// Prompt only for FROM/TO nodes that map to existing nodes.
|
3327
3339
|
if(Object.keys(ft_map).length) {
|
3328
3340
|
mapping.from_to = ft_map;
|
3329
3341
|
mapping.from_to_type = ft_type;
|
@@ -3334,7 +3346,7 @@ class GUIController extends Controller {
|
|
3334
3346
|
|
3335
3347
|
// Only check for selected entities; from-to's and extra's should be
|
3336
3348
|
// used if they exist, or should be created when copying to a different
|
3337
|
-
// model
|
3349
|
+
// model.
|
3338
3350
|
name_map.length = 0;
|
3339
3351
|
nameConflicts(entities_node);
|
3340
3352
|
if(name_conflicts.length) {
|
@@ -3353,20 +3365,20 @@ console.log('HERE name conflicts', name_conflicts, mapping);
|
|
3353
3365
|
for(let i = 0; i < entities_node.childNodes.length; i++) {
|
3354
3366
|
addEntityFromNode(entities_node.childNodes[i]);
|
3355
3367
|
}
|
3356
|
-
// Update diagram, showing newly added nodes as selection
|
3368
|
+
// Update diagram, showing newly added nodes as selection.
|
3357
3369
|
MODEL.clearSelection();
|
3358
3370
|
for(let i = 0; i < selection_node.childNodes.length; i++) {
|
3359
3371
|
const
|
3360
3372
|
n = xmlDecoded(nodeContent(selection_node.childNodes[i])),
|
3361
3373
|
obj = MODEL.objectByName(mappedName(n));
|
3362
3374
|
if(obj) {
|
3363
|
-
// NOTE:
|
3375
|
+
// NOTE: Selected products must be positioned.
|
3364
3376
|
if(obj instanceof Product) MODEL.focal_cluster.addProductPosition(obj);
|
3365
3377
|
MODEL.select(obj);
|
3366
3378
|
}
|
3367
3379
|
}
|
3368
3380
|
// Force redrawing the selection to ensure that links to positioned
|
3369
|
-
// products are displayed as arrows instead of block arrows
|
3381
|
+
// products are displayed as arrows instead of block arrows.
|
3370
3382
|
fc.clearAllProcesses();
|
3371
3383
|
UI.drawDiagram(MODEL);
|
3372
3384
|
this.paste_modal.hide();
|
@@ -338,7 +338,7 @@ class DocumentationManager {
|
|
338
338
|
this.viewer.innerHTML = this.markdown;
|
339
339
|
this.edit_btn.classList.remove('disab');
|
340
340
|
this.edit_btn.classList.add('enab');
|
341
|
-
// NOTE:
|
341
|
+
// NOTE: Permit documentation of the model by raising the dialog.
|
342
342
|
if(this.entity === MODEL) this.dialog.style.zIndex = 101;
|
343
343
|
} else if(e instanceof DatasetModifier) {
|
344
344
|
this.title.innerHTML = e.selector;
|
@@ -347,17 +347,17 @@ class DocumentationManager {
|
|
347
347
|
if(e.expression.eligible_prefixes) {
|
348
348
|
const el = Object.keys(e.expression.eligible_prefixes)
|
349
349
|
.sort(compareSelectors);
|
350
|
-
if(el.length > 0) this.viewer.innerHTML =
|
351
|
-
e.selector
|
352
|
-
pluralS(el.length, 'prefixed entity group')
|
353
|
-
':<ul><li>'
|
350
|
+
if(el.length > 0) this.viewer.innerHTML = [
|
351
|
+
'Method <tt>', e.selector, '</tt> applies to ',
|
352
|
+
pluralS(el.length, 'prefixed entity group'),
|
353
|
+
':<ul><li>', el.join('</li><li>'), '</li></ul>'].join('');
|
354
354
|
}
|
355
355
|
}
|
356
356
|
}
|
357
357
|
}
|
358
358
|
|
359
359
|
rewrite(str) {
|
360
|
-
// Apply all the rewriting rules to `str
|
360
|
+
// Apply all the rewriting rules to `str`.
|
361
361
|
str = '\n' + str + '\n';
|
362
362
|
this.rules.forEach(
|
363
363
|
(rule) => { str = str.replace(rule.pattern, rule.rewrite); });
|
@@ -365,28 +365,29 @@ class DocumentationManager {
|
|
365
365
|
}
|
366
366
|
|
367
367
|
makeList(par, isp, type) {
|
368
|
-
// Split on the *global multi-line* item separator pattern
|
368
|
+
// Split on the *global multi-line* item separator pattern.
|
369
369
|
const splitter = new RegExp(isp, 'gm'),
|
370
370
|
list = par.split(splitter);
|
371
371
|
if(list.length < 2) return false;
|
372
|
-
// Now we know that the paragraph contains at least one list item line
|
372
|
+
// Now we know that the paragraph contains at least one list item line.
|
373
373
|
let start = 0;
|
374
|
-
// Paragraph may start with plain text, so check using the original
|
374
|
+
// Paragraph may start with plain text, so check using the original
|
375
|
+
// pattern.
|
375
376
|
if(!par.match(isp)) {
|
376
377
|
// If so, retain this first part as a separate paragraph...
|
377
378
|
start = 1;
|
378
|
-
// NOTE:
|
379
|
+
// NOTE: Add it only if it contains text.
|
379
380
|
par = (list[0].trim() ? `<p>${this.rewrite(list[0])}</p>` : '');
|
380
|
-
// ... and clear it as list item
|
381
|
+
// ... and clear it as list item.
|
381
382
|
list[0] = '';
|
382
383
|
} else {
|
383
384
|
par = '';
|
384
385
|
}
|
385
|
-
// Rewrite each list item fragment that contains text
|
386
|
+
// Rewrite each list item fragment that contains text.
|
386
387
|
for(let j = start; j < list.length; j++) {
|
387
388
|
list[j] = (list[j].trim() ? `<li>${this.rewrite(list[j])}</li>` : '');
|
388
389
|
}
|
389
|
-
// Return assembled parts
|
390
|
+
// Return assembled parts.
|
390
391
|
return [par, '<', type, 'l>', list.join(''), '</', type, 'l>'].join('');
|
391
392
|
}
|
392
393
|
|
@@ -395,16 +396,16 @@ class DocumentationManager {
|
|
395
396
|
const html = this.markup.split(/\n{2,}/);
|
396
397
|
let list;
|
397
398
|
for(let i = 0; i < html.length; i++) {
|
398
|
-
// Paragraph with only dashes and spaces becomes a horizontal rule
|
399
|
+
// Paragraph with only dashes and spaces becomes a horizontal rule.
|
399
400
|
if(html[i].match(/^( *-)+$/)) {
|
400
401
|
html[i] = '<hr>';
|
401
|
-
// Paragraph may contain a bulleted list
|
402
|
+
// Paragraph may contain a bulleted list.
|
402
403
|
} else if ((list = this.makeList(html[i], /^ *- +/, 'u')) !== false) {
|
403
404
|
html[i] = list;
|
404
|
-
// Paragraph may contain a numbered list
|
405
|
+
// Paragraph may contain a numbered list.
|
405
406
|
} else if ((list = this.makeList(html[i], /^ *\d+. +/, 'o')) !== false) {
|
406
407
|
html[i] = list;
|
407
|
-
// Otherwise: default HTML paragraph
|
408
|
+
// Otherwise: default HTML paragraph.
|
408
409
|
} else {
|
409
410
|
html[i] = `<p>${this.rewrite(html[i])}</p>`;
|
410
411
|
}
|
@@ -431,7 +432,7 @@ class DocumentationManager {
|
|
431
432
|
}
|
432
433
|
|
433
434
|
insertSymbol(sym) {
|
434
|
-
// Insert symbol (clicked item in list below text area) into text area
|
435
|
+
// Insert symbol (clicked item in list below text area) into text area.
|
435
436
|
this.editor.focus();
|
436
437
|
let p = this.editor.selectionStart;
|
437
438
|
const
|
@@ -454,7 +455,7 @@ class DocumentationManager {
|
|
454
455
|
} else if(this.entity instanceof Constraint) {
|
455
456
|
UI.paper.drawConstraint(this.entity);
|
456
457
|
} else if (typeof this.entity.draw === 'function') {
|
457
|
-
// Only draw if the entity responds to that method
|
458
|
+
// Only draw if the entity responds to that method.
|
458
459
|
this.entity.draw();
|
459
460
|
}
|
460
461
|
}
|
@@ -501,14 +502,14 @@ class DocumentationManager {
|
|
501
502
|
}
|
502
503
|
|
503
504
|
addMessage(msg) {
|
504
|
-
// Append message to the info messages list
|
505
|
+
// Append message to the info messages list.
|
505
506
|
if(msg) this.info_messages.push(msg);
|
506
|
-
// Update dialog only when it is showing
|
507
|
+
// Update dialog only when it is showing.
|
507
508
|
if(!UI.hidden(this.dialog.id)) this.showInfoMessages(true);
|
508
509
|
}
|
509
510
|
|
510
511
|
showInfoMessages(shift) {
|
511
|
-
// Show all messages that have appeared on the status line
|
512
|
+
// Show all messages that have appeared on the status line.
|
512
513
|
const
|
513
514
|
n = this.info_messages.length,
|
514
515
|
title = pluralS(n, 'message') + ' since the current model was loaded';
|
@@ -524,21 +525,21 @@ class DocumentationManager {
|
|
524
525
|
'<div class="', m.status, first, '-msg">', m.text, '</div></div>');
|
525
526
|
}
|
526
527
|
this.viewer.innerHTML = divs.join('');
|
527
|
-
// Set the dialog title
|
528
|
+
// Set the dialog title.
|
528
529
|
this.title.innerHTML = title;
|
529
530
|
}
|
530
531
|
}
|
531
532
|
|
532
533
|
showArrowLinks(arrow) {
|
533
|
-
// Show list of links represented by a composite arrow
|
534
|
+
// Show list of links represented by a composite arrow.
|
534
535
|
const
|
535
536
|
n = arrow.links.length,
|
536
537
|
msg = 'Arrow represents ' + pluralS(n, 'link');
|
537
538
|
UI.setMessage(msg);
|
538
539
|
if(this.visible && !this.editing) {
|
539
|
-
// Set the dialog title
|
540
|
+
// Set the dialog title.
|
540
541
|
this.title.innerHTML = msg;
|
541
|
-
// Show list
|
542
|
+
// Show list.
|
542
543
|
const lis = [];
|
543
544
|
let l, dn, c, af;
|
544
545
|
for(let i = 0; i < n; i++) {
|
@@ -570,7 +571,8 @@ class DocumentationManager {
|
|
570
571
|
}
|
571
572
|
|
572
573
|
showHiddenIO(node, arrow) {
|
573
|
-
// Show list of products or processes linked to node by an invisible
|
574
|
+
// Show list of products or processes linked to node by an invisible
|
575
|
+
// arrow (i.e., links represented by a block arrow).
|
574
576
|
let msg, iol;
|
575
577
|
if(arrow === UI.BLOCK_IN) {
|
576
578
|
iol = node.hidden_inputs;
|
@@ -586,9 +588,9 @@ class DocumentationManager {
|
|
586
588
|
UI.on_block_arrow = true;
|
587
589
|
UI.setMessage(msg);
|
588
590
|
if(this.visible && !this.editing) {
|
589
|
-
// Set the dialog title
|
591
|
+
// Set the dialog title.
|
590
592
|
this.title.innerHTML = msg;
|
591
|
-
// Show list
|
593
|
+
// Show list.
|
592
594
|
const lis = [];
|
593
595
|
for(let i = 0; i < iol.length; i++) {
|
594
596
|
lis.push(`<li>${iol[i].displayName}</li>`);
|
@@ -599,17 +601,19 @@ class DocumentationManager {
|
|
599
601
|
}
|
600
602
|
|
601
603
|
showAllDocumentation() {
|
604
|
+
// Show (as HTML) all model entities (categorized by type) with their
|
605
|
+
// associated comments (if added by the modeler).
|
602
606
|
const
|
603
607
|
html = [],
|
604
608
|
sl = MODEL.listOfAllComments;
|
605
609
|
for(let i = 0; i < sl.length; i++) {
|
606
610
|
if(sl[i].startsWith('_____')) {
|
607
|
-
// 5-underscore leader indicates: start of new category
|
611
|
+
// 5-underscore leader indicates: start of new category.
|
608
612
|
html.push('<h2>', sl[i].substring(5), '</h2>');
|
609
613
|
} else {
|
610
614
|
// Expect model element name...
|
611
615
|
html.push('<p><tt>', sl[i], '</tt><br><small>');
|
612
|
-
// ... immediately followed by its associated marked-up comments
|
616
|
+
// ... immediately followed by its associated marked-up comments.
|
613
617
|
i++;
|
614
618
|
this.markup = sl[i];
|
615
619
|
html.push(this.markdown, '</small></p>');
|
@@ -617,7 +621,7 @@ class DocumentationManager {
|
|
617
621
|
}
|
618
622
|
this.title.innerHTML = 'Complete model documentation';
|
619
623
|
this.viewer.innerHTML = html.join('');
|
620
|
-
// Deselect entity and disable editing
|
624
|
+
// Deselect entity and disable editing.
|
621
625
|
this.entity = null;
|
622
626
|
this.edit_btn.classList.remove('enab');
|
623
627
|
this.edit_btn.classList.add('disab');
|
@@ -151,7 +151,7 @@ class EquationManager {
|
|
151
151
|
(m === sm ? ' sel-set' : ''),
|
152
152
|
'"><td class="equation-selector',
|
153
153
|
(method ? ' method' : ''),
|
154
|
-
// Display in gray when method cannot be applied
|
154
|
+
// Display in gray when method cannot be applied.
|
155
155
|
(m.expression.noMethodObject ? ' no-object' : ''),
|
156
156
|
(m.expression.isStatic ? '' : ' it'), issue,
|
157
157
|
(wild ? ' wildcard' : ''), clk, ', false);"', mover, '>',
|
@@ -268,20 +268,23 @@ class EquationManager {
|
|
268
268
|
if(!this.selected_modifier) return;
|
269
269
|
const
|
270
270
|
sel = this.rename_modal.element('name').value,
|
271
|
-
// Keep track of old name
|
271
|
+
// Keep track of old name.
|
272
272
|
oldm = this.selected_modifier,
|
273
273
|
olds = oldm.selector,
|
274
|
-
// NOTE: addModifier returns existing one if selector not changed
|
274
|
+
// NOTE: addModifier returns existing one if selector not changed.
|
275
275
|
m = MODEL.equations_dataset.addModifier(sel);
|
276
|
-
// NULL indicates invalid name
|
276
|
+
// NULL indicates invalid name.
|
277
277
|
if(!m) return;
|
278
|
-
// If only case has changed, update the selector
|
279
|
-
// NOTE:
|
278
|
+
// If only case has changed, update the selector.
|
279
|
+
// NOTE: Equation names may contain spaces; if so, reduce to single space.
|
280
280
|
if(m === oldm) {
|
281
281
|
m.selector = sel.trim().replace(/\s+/g, ' ');
|
282
282
|
} else {
|
283
|
-
// When a new modifier has been added, more actions are needed
|
283
|
+
// When a new modifier has been added, more actions are needed.
|
284
284
|
m.expression = oldm.expression;
|
285
|
+
// NOTE: The `attribute` property of the expression must be updated
|
286
|
+
// because it identifies the "owner" of the expression.
|
287
|
+
m.expression.attribute = m.selector;
|
285
288
|
this.deleteEquation();
|
286
289
|
this.selected_modifier = m;
|
287
290
|
}
|
@@ -356,7 +356,11 @@ class GUIExperimentManager extends ExperimentManager {
|
|
356
356
|
dim_count.innerHTML = pluralS(x.available_dimensions.length,
|
357
357
|
'more dimension');
|
358
358
|
x.inferActualDimensions();
|
359
|
+
for(let i = 0; i < x.actual_dimensions.length; i++) {
|
360
|
+
x.actual_dimensions[i].sort(compareSelectors);
|
361
|
+
}
|
359
362
|
x.inferCombinations();
|
363
|
+
//x.combinations.sort(compareCombinations);
|
360
364
|
combi_count.innerHTML = pluralS(x.combinations.length, 'combination');
|
361
365
|
if(x.combinations.length === 0) canview = false;
|
362
366
|
header.innerHTML = x.title;
|
@@ -850,6 +854,9 @@ class GUIExperimentManager extends ExperimentManager {
|
|
850
854
|
}
|
851
855
|
// Get the selected statistic for each run so as to get an array of numbers
|
852
856
|
const data = [];
|
857
|
+
// Set reference column indices so that values for the reference|
|
858
|
+
// configuration can be displayed in orange.
|
859
|
+
const ref_conf_indices = [];
|
853
860
|
for(let i = 0; i < x.runs.length; i++) {
|
854
861
|
const
|
855
862
|
r = x.runs[i],
|
@@ -878,7 +885,7 @@ class GUIExperimentManager extends ExperimentManager {
|
|
878
885
|
data.push(rr.last);
|
879
886
|
}
|
880
887
|
}
|
881
|
-
// Scale data as selected
|
888
|
+
// Scale data as selected.
|
882
889
|
const scaled = data.slice();
|
883
890
|
// NOTE: scale only after the experiment has been completed AND
|
884
891
|
// configurations have been defined (otherwise comparison is pointless)
|
@@ -896,7 +903,9 @@ class GUIExperimentManager extends ExperimentManager {
|
|
896
903
|
}
|
897
904
|
// Set difference for reference configuration itself to 0
|
898
905
|
for(let i = 0; i < n; i++) {
|
899
|
-
|
906
|
+
const index = rc * n + i;
|
907
|
+
scaled[index] = 0;
|
908
|
+
ref_conf_indices.push(index);
|
900
909
|
}
|
901
910
|
} else if(x.selected_scale === 'reg') {
|
902
911
|
// Compute regret: current config - high value config in same scenario
|
@@ -934,13 +943,15 @@ class GUIExperimentManager extends ExperimentManager {
|
|
934
943
|
formatted.push(VM.sig4Dig(scaled[i]));
|
935
944
|
}
|
936
945
|
uniformDecimals(formatted);
|
937
|
-
// Display formatted data in cells
|
946
|
+
// Display formatted data in cells.
|
938
947
|
for(let i = 0; i < x.combinations.length; i++) {
|
939
948
|
const cell = document.getElementById('xr' + i);
|
940
949
|
if(i < x.runs.length) {
|
941
950
|
cell.innerHTML = formatted[i];
|
942
951
|
cell.classList.remove('not-run');
|
943
952
|
cell.style.backgroundColor = this.color_scale.rgb(normalized[i]);
|
953
|
+
cell.style.color = (ref_conf_indices.indexOf(i) >= 0 ?
|
954
|
+
'orange' : 'black');
|
944
955
|
const
|
945
956
|
r = x.runs[i],
|
946
957
|
rr = r.results[rri],
|
@@ -441,6 +441,7 @@ class Finder {
|
|
441
441
|
}
|
442
442
|
}
|
443
443
|
document.getElementById('finder-item-header').innerHTML = hdr;
|
444
|
+
occ.sort(compareSelectors);
|
444
445
|
for(let i = 0; i < occ.length; i++) {
|
445
446
|
const e = MODEL.objectByID(occ[i]);
|
446
447
|
el.push(['<tr id="eotr', i, '" class="dataset" onclick="FINDER.reveal(\'',
|