linny-r 1.1.12 → 1.1.14
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
package/static/linny-r.css
CHANGED
@@ -2168,8 +2168,8 @@ td.equation-expression {
|
|
2168
2168
|
|
2169
2169
|
/* SERIES modal dialog */
|
2170
2170
|
#series-dlg {
|
2171
|
-
width:
|
2172
|
-
height:
|
2171
|
+
width: 165px;
|
2172
|
+
height: 320px;
|
2173
2173
|
}
|
2174
2174
|
|
2175
2175
|
#series-default-lbl {
|
@@ -2182,7 +2182,7 @@ td.equation-expression {
|
|
2182
2182
|
position: absolute;
|
2183
2183
|
top: 24px;
|
2184
2184
|
left: 79px;
|
2185
|
-
width:
|
2185
|
+
width: calc(100% - 83px);
|
2186
2186
|
font-size: 12px;
|
2187
2187
|
margin-bottom: 2px;
|
2188
2188
|
}
|
@@ -2202,13 +2202,13 @@ td.equation-expression {
|
|
2202
2202
|
#series-array {
|
2203
2203
|
position: absolute;
|
2204
2204
|
top: 45px;
|
2205
|
-
left:
|
2205
|
+
left: 77px;
|
2206
2206
|
}
|
2207
2207
|
|
2208
2208
|
#series-array-lbl {
|
2209
2209
|
position: absolute;
|
2210
2210
|
top: 47px;
|
2211
|
-
left:
|
2211
|
+
left: 99px;
|
2212
2212
|
}
|
2213
2213
|
|
2214
2214
|
#series-no-time-msg {
|
@@ -2216,7 +2216,7 @@ td.equation-expression {
|
|
2216
2216
|
top: 66px;
|
2217
2217
|
left: 1px;
|
2218
2218
|
width: calc(100% - 2px);
|
2219
|
-
height:
|
2219
|
+
height: 30px;
|
2220
2220
|
z-index: 1;
|
2221
2221
|
background-color: inherit;
|
2222
2222
|
font-style: italic;
|
@@ -2235,7 +2235,7 @@ td.equation-expression {
|
|
2235
2235
|
position: absolute;
|
2236
2236
|
top: 67px;
|
2237
2237
|
left: 60px;
|
2238
|
-
width:
|
2238
|
+
width: 50px;
|
2239
2239
|
font-size: 12px;
|
2240
2240
|
margin-bottom: 2px;
|
2241
2241
|
}
|
@@ -2243,7 +2243,7 @@ td.equation-expression {
|
|
2243
2243
|
#series-time-unit {
|
2244
2244
|
position: absolute;
|
2245
2245
|
top: 67px;
|
2246
|
-
left:
|
2246
|
+
left: 116px;
|
2247
2247
|
height: 19px;
|
2248
2248
|
width: 45px;
|
2249
2249
|
font-size: 12px;
|
@@ -2251,40 +2251,43 @@ td.equation-expression {
|
|
2251
2251
|
|
2252
2252
|
#series-method-lbl {
|
2253
2253
|
position: absolute;
|
2254
|
-
top:
|
2254
|
+
top: 91px;
|
2255
2255
|
left: 2px;
|
2256
2256
|
}
|
2257
2257
|
|
2258
2258
|
#series-method {
|
2259
2259
|
position: absolute;
|
2260
|
-
top:
|
2261
|
-
left:
|
2262
|
-
height:
|
2263
|
-
width:
|
2264
|
-
font-size:
|
2260
|
+
top: 89px;
|
2261
|
+
left: 50px;
|
2262
|
+
height: 21px;
|
2263
|
+
width: 111px;
|
2264
|
+
font-size: 12px;
|
2265
2265
|
}
|
2266
2266
|
|
2267
2267
|
#series-remote {
|
2268
2268
|
position: absolute;
|
2269
|
-
top:
|
2270
|
-
left:
|
2269
|
+
top: 113px;
|
2270
|
+
left: 3px;
|
2271
|
+
width: calc(100% - 8px);
|
2271
2272
|
}
|
2272
2273
|
|
2273
2274
|
#series-url {
|
2274
|
-
width:
|
2275
|
+
width: 100%;
|
2275
2276
|
font-size: 12px;
|
2276
2277
|
}
|
2277
2278
|
|
2278
2279
|
#series-data-lbl {
|
2279
2280
|
position: absolute;
|
2280
|
-
top:
|
2281
|
+
top: 133px;
|
2281
2282
|
left: 2px;
|
2282
2283
|
}
|
2283
2284
|
|
2284
2285
|
#series-data {
|
2285
|
-
|
2286
|
-
|
2287
|
-
|
2286
|
+
position: absolute;
|
2287
|
+
bottom: 13px;
|
2288
|
+
width: calc(100% - 6px);
|
2289
|
+
height: calc(100% - 165px);
|
2290
|
+
margin: 3px;
|
2288
2291
|
}
|
2289
2292
|
|
2290
2293
|
#series-line {
|
@@ -140,7 +140,7 @@ class Controller {
|
|
140
140
|
'scale_unit', 'equal_bounds', 'price', 'is_source', 'is_sink', 'is_buffer',
|
141
141
|
'is_data', 'integer_level', 'no_slack'],
|
142
142
|
DATASET_PROPS: ['comments', 'default_value', 'time_scale', 'time_unit',
|
143
|
-
'method', 'periodic', 'array', 'url'],
|
143
|
+
'method', 'periodic', 'array', 'url', 'default_selector'],
|
144
144
|
LINK_PROPS: ['comments', 'multiplier', 'relative_rate', 'share_of_cost',
|
145
145
|
'flow_delay'],
|
146
146
|
CONSTRAINT_PROPS: ['comments', 'no_slack', 'share_of_cost'],
|
@@ -1471,7 +1471,7 @@ class Paper {
|
|
1471
1471
|
// achieved by multiplying the "gap" being (lengths - heights)/2 by
|
1472
1472
|
// (1 - |dy/l|). NOTE: we re-use the values of `th` and `tw`
|
1473
1473
|
// computed in the previous block!
|
1474
|
-
shift += th/2;
|
1474
|
+
shift += th / 2;
|
1475
1475
|
s = VM.sig4Dig(luc.share_of_cost * 100) + '%';
|
1476
1476
|
bb = this.numberSize(s, 7);
|
1477
1477
|
const sgap = (tw + bb.width + 3 - th - bb.height) / 2;
|
@@ -1631,7 +1631,8 @@ class Paper {
|
|
1631
1631
|
if(cp <= VM.MINUS_INFINITY || cp >= VM.PLUS_INFINITY) {
|
1632
1632
|
s = VM.sig4Dig(cp);
|
1633
1633
|
} else if(Math.abs(cp) <= VM.SIG_DIF_FROM_ZERO) {
|
1634
|
-
|
1634
|
+
// DO not display CP when it is "propagated" NO_COST
|
1635
|
+
s = (cp === VM.NO_COST ? '' : '0');
|
1635
1636
|
} else {
|
1636
1637
|
// NOTE: use the absolute value of the flow, as cost is not affected by direction
|
1637
1638
|
s = VM.sig4Dig(Math.abs(af) * soc * cp);
|
@@ -8832,7 +8833,7 @@ class GUIDatasetManager extends DatasetManager {
|
|
8832
8833
|
if(d === sd) sdid += i;
|
8833
8834
|
dl.push(['<tr id="dstr', i, '" class="dataset',
|
8834
8835
|
(d === sd ? ' sel-set' : ''),
|
8835
|
-
'" onclick="DATASET_MANAGER.selectDataset(\'',
|
8836
|
+
'" onclick="DATASET_MANAGER.selectDataset(event, \'',
|
8836
8837
|
dnl[i], '\');" onmouseover="DATASET_MANAGER.showInfo(\'', dnl[i],
|
8837
8838
|
'\', event.shiftKey);"><td', cls, '>', d.displayName,
|
8838
8839
|
'</td></tr>'].join(''));
|
@@ -8906,14 +8907,19 @@ class GUIDatasetManager extends DatasetManager {
|
|
8906
8907
|
for(let i = 0; i < msl.length; i++) {
|
8907
8908
|
const
|
8908
8909
|
m = sd.modifiers[UI.nameToID(msl[i])],
|
8909
|
-
|
8910
|
+
defsel = (m.selector === sd.default_selector),
|
8911
|
+
clk = '" onclick="DATASET_MANAGER.selectModifier(event, \'' +
|
8910
8912
|
m.selector + '\'';
|
8911
8913
|
if(m === sm) smid += i;
|
8912
8914
|
ml.push(['<tr id="dsmtr', i, '" class="dataset-modif',
|
8913
8915
|
(m === sm ? ' sel-set' : ''),
|
8914
8916
|
'"><td class="dataset-selector',
|
8915
8917
|
(m.hasWildcards ? ' wildcard' : ''),
|
8918
|
+
'" title="Shift-click to ', (defsel ? 'clear' : 'set as'),
|
8919
|
+
' default modifier',
|
8916
8920
|
clk, ', false);">',
|
8921
|
+
(defsel ? '<img src="images/solve.png" style="height: 14px;' +
|
8922
|
+
' width: 14px; margin: 0 1px -3px -1px;">' : ''),
|
8917
8923
|
m.selector, '</td><td class="dataset-expression',
|
8918
8924
|
clk, ');">', m.expression.text, '</td></tr>'].join(''));
|
8919
8925
|
}
|
@@ -8960,46 +8966,54 @@ class GUIDatasetManager extends DatasetManager {
|
|
8960
8966
|
this.updateDialog();
|
8961
8967
|
}
|
8962
8968
|
|
8963
|
-
selectDataset(id) {
|
8964
|
-
// Select dataset, or edit it when double-clicked
|
8969
|
+
selectDataset(event, id) {
|
8970
|
+
// Select dataset, or edit it when Alt- or double-clicked
|
8965
8971
|
const
|
8966
|
-
d = MODEL.datasets[id],
|
8972
|
+
d = MODEL.datasets[id] || null,
|
8967
8973
|
now = Date.now(),
|
8968
|
-
dt = now - this.last_time_selected
|
8974
|
+
dt = now - this.last_time_selected,
|
8975
|
+
// Consider click to be "double" if it occurred less than 300 ms ago
|
8976
|
+
edit = event.altKey || (d === this.selected_dataset && dt < 300);
|
8977
|
+
this.selected_dataset = d;
|
8969
8978
|
this.last_time_selected = now;
|
8970
|
-
if(d
|
8971
|
-
|
8972
|
-
|
8973
|
-
|
8974
|
-
this.editData();
|
8975
|
-
return;
|
8976
|
-
}
|
8979
|
+
if(d && edit) {
|
8980
|
+
this.last_time_selected = 0;
|
8981
|
+
this.editData();
|
8982
|
+
return;
|
8977
8983
|
}
|
8978
|
-
this.selected_dataset = MODEL.datasets[id];
|
8979
8984
|
this.updateDialog();
|
8980
8985
|
}
|
8981
8986
|
|
8982
|
-
selectModifier(id, x=true) {
|
8987
|
+
selectModifier(event, id, x=true) {
|
8983
8988
|
// Select modifier, or when double-clicked, edit its expression or the
|
8984
8989
|
// name of the modifier
|
8985
8990
|
if(this.selected_dataset) {
|
8986
8991
|
const m = this.selected_dataset.modifiers[UI.nameToID(id)],
|
8987
8992
|
now = Date.now(),
|
8988
|
-
dt = now - this.last_time_selected
|
8993
|
+
dt = now - this.last_time_selected,
|
8994
|
+
// NOTE: Alt-click and double-click indicate: edit
|
8995
|
+
// Consider click to be "double" if the same modifier was clicked
|
8996
|
+
// less than 300 ms ago
|
8997
|
+
edit = event.altKey || (m === this.selected_modifier && dt < 300);
|
8989
8998
|
this.last_time_selected = now;
|
8990
|
-
if(
|
8991
|
-
//
|
8992
|
-
if(
|
8993
|
-
this.
|
8994
|
-
|
8995
|
-
|
8996
|
-
} else {
|
8997
|
-
this.promptForSelector('rename');
|
8998
|
-
}
|
8999
|
-
return;
|
8999
|
+
if(event.shiftKey) {
|
9000
|
+
// Toggle dataset default selector
|
9001
|
+
if(m.selector === this.selected_dataset.default_selector) {
|
9002
|
+
this.selected_dataset.default_selector = '';
|
9003
|
+
} else {
|
9004
|
+
this.selected_dataset.default_selector = m.selector;
|
9000
9005
|
}
|
9001
9006
|
}
|
9002
9007
|
this.selected_modifier = m;
|
9008
|
+
if(edit) {
|
9009
|
+
this.last_time_selected = 0;
|
9010
|
+
if(x) {
|
9011
|
+
this.editExpression();
|
9012
|
+
} else {
|
9013
|
+
this.promptForSelector('rename');
|
9014
|
+
}
|
9015
|
+
return;
|
9016
|
+
}
|
9003
9017
|
} else {
|
9004
9018
|
this.selected_modifier = null;
|
9005
9019
|
}
|
@@ -9166,6 +9180,10 @@ class GUIDatasetManager extends DatasetManager {
|
|
9166
9180
|
m = this.selected_dataset.addModifier(sel);
|
9167
9181
|
// NULL can result when new name is invalid
|
9168
9182
|
if(!m) return;
|
9183
|
+
// If selected modifier was the dataset default selector, update it
|
9184
|
+
if(oldm.selector === this.selected_dataset.default_selector) {
|
9185
|
+
this.selected_dataset.default_selector = m.selector;
|
9186
|
+
}
|
9169
9187
|
// If only case has changed, just update the selector
|
9170
9188
|
// NOTE: normal dataset selector, so remove all invalid characters
|
9171
9189
|
if(m === oldm) {
|
@@ -9247,8 +9265,14 @@ class GUIDatasetManager extends DatasetManager {
|
|
9247
9265
|
}
|
9248
9266
|
|
9249
9267
|
deleteModifier() {
|
9268
|
+
// Delete modifier from selected dataset
|
9250
9269
|
const m = this.selected_modifier;
|
9251
9270
|
if(m) {
|
9271
|
+
// If it was the dataset default modifier, clear the default
|
9272
|
+
if(m.selector === this.selected_dataset.default_selector) {
|
9273
|
+
this.selected_dataset.default_selector = '';
|
9274
|
+
}
|
9275
|
+
// Then simply remove the object
|
9252
9276
|
delete this.selected_dataset.modifiers[UI.nameToID(m.selector)];
|
9253
9277
|
this.selected_modifier = null;
|
9254
9278
|
this.updateModifiers();
|
@@ -9417,7 +9441,7 @@ class EquationManager {
|
|
9417
9441
|
const
|
9418
9442
|
m = ed.modifiers[UI.nameToID(msl[i])],
|
9419
9443
|
mp = (m.parameters ? '\\' + m.parameters.join('\\') : ''),
|
9420
|
-
clk = '" onclick="EQUATION_MANAGER.selectModifier(\'' +
|
9444
|
+
clk = '" onclick="EQUATION_MANAGER.selectModifier(event, \'' +
|
9421
9445
|
m.selector + '\'';
|
9422
9446
|
if(m === sm) smid += i;
|
9423
9447
|
ml.push(['<tr id="eqmtr', i, '" class="dataset-modif',
|
@@ -9443,28 +9467,27 @@ class EquationManager {
|
|
9443
9467
|
// @@TO DO: Display documentation for the equation => extra comments field?
|
9444
9468
|
}
|
9445
9469
|
|
9446
|
-
selectModifier(id, x=true) {
|
9447
|
-
// Select modifier, or when double-clicked, edit its expression
|
9448
|
-
// name of the modifier
|
9470
|
+
selectModifier(event, id, x=true) {
|
9471
|
+
// Select modifier, or when Alt- or double-clicked, edit its expression
|
9472
|
+
// or the equation name (= name of the modifier)
|
9449
9473
|
if(MODEL.equations_dataset) {
|
9450
9474
|
const
|
9451
|
-
m = MODEL.equations_dataset.modifiers[UI.nameToID(id)],
|
9475
|
+
m = MODEL.equations_dataset.modifiers[UI.nameToID(id)] || null,
|
9452
9476
|
now = Date.now(),
|
9453
|
-
dt = now - this.last_time_selected
|
9477
|
+
dt = now - this.last_time_selected,
|
9478
|
+
// Consider click to be "double" if it occurred less than 300 ms ago
|
9479
|
+
edit = event.altKey || (m === this.selected_modifier && dt < 300);
|
9454
9480
|
this.last_time_selected = now;
|
9455
|
-
|
9456
|
-
|
9457
|
-
|
9458
|
-
|
9459
|
-
|
9460
|
-
|
9461
|
-
|
9462
|
-
this.promptForName();
|
9463
|
-
}
|
9464
|
-
return;
|
9481
|
+
this.selected_modifier = m;
|
9482
|
+
if(m && edit) {
|
9483
|
+
this.last_time_selected = 0;
|
9484
|
+
if(x) {
|
9485
|
+
this.editEquation();
|
9486
|
+
} else {
|
9487
|
+
this.promptForName();
|
9465
9488
|
}
|
9489
|
+
return;
|
9466
9490
|
}
|
9467
|
-
this.selected_modifier = m;
|
9468
9491
|
} else {
|
9469
9492
|
this.selected_modifier = null;
|
9470
9493
|
}
|
@@ -168,15 +168,6 @@ class LinnyRModel {
|
|
168
168
|
return olist;
|
169
169
|
}
|
170
170
|
|
171
|
-
get legacyVersion() {
|
172
|
-
// Return TRUE if the model as it has been loaded was not saved by
|
173
|
-
// JavaScript Linny-R
|
174
|
-
const
|
175
|
-
vnl = this.version.split('.'),
|
176
|
-
legacy = vnl[0] === '0' || (vnl[0] === '1' && vnl.length > 3);
|
177
|
-
return legacy;
|
178
|
-
}
|
179
|
-
|
180
171
|
get newProcessCode() {
|
181
172
|
// Return the next unused process code
|
182
173
|
const n = this.next_process_number;
|
@@ -1994,6 +1985,15 @@ class LinnyRModel {
|
|
1994
1985
|
// Initialize a model from the XML tree with `node` as root
|
1995
1986
|
// NOTE: do NOT reset and initialize basic model properties when *including*
|
1996
1987
|
// a module into the current model
|
1988
|
+
// NOTE: obsolete XML nodes indicate: legacy Linny-R model
|
1989
|
+
const legacy_model = (nodeParameterValue(node, 'view-options') +
|
1990
|
+
nodeParameterValue(node, 'autosave') +
|
1991
|
+
nodeParameterValue(node, 'look-ahead') +
|
1992
|
+
nodeParameterValue(node, 'save-series') +
|
1993
|
+
nodeParameterValue(node, 'show-lp') +
|
1994
|
+
nodeParameterValue(node, 'optional-slack')).length > 0;
|
1995
|
+
// Flag to set when legacy time series data are added
|
1996
|
+
this.legacy_datasets = false;
|
1997
1997
|
if(!IO_CONTEXT) {
|
1998
1998
|
this.reset();
|
1999
1999
|
this.next_process_number = safeStrToInt(
|
@@ -2019,8 +2019,8 @@ class LinnyRModel {
|
|
2019
2019
|
this.timeout_period = Math.max(0,
|
2020
2020
|
safeStrToInt(nodeContentByTag(node, 'timeout-period')));
|
2021
2021
|
// Legacy models have tag "optimization-period" instead of "block-length"
|
2022
|
-
const bl_tag = (
|
2023
|
-
'optimization-period'
|
2022
|
+
const bl_tag = nodeContentByTag(node, 'block-length') ||
|
2023
|
+
nodeContentByTag(node, 'optimization-period');
|
2024
2024
|
this.block_length = Math.max(1,
|
2025
2025
|
safeStrToInt(nodeContentByTag(node, bl_tag)));
|
2026
2026
|
this.start_period = Math.max(1,
|
@@ -2177,7 +2177,7 @@ class LinnyRModel {
|
|
2177
2177
|
}
|
2178
2178
|
// Clear the default (empty) equations dataset, or it will block adding it
|
2179
2179
|
if(!IO_CONTEXT) {
|
2180
|
-
this.datasets = {};
|
2180
|
+
if(!this.legacy_datasets) this.datasets = {};
|
2181
2181
|
this.equations_dataset = null;
|
2182
2182
|
}
|
2183
2183
|
// NOTE: keep track of datasets that load from URL or file
|
@@ -2322,7 +2322,7 @@ class LinnyRModel {
|
|
2322
2322
|
// NOTE: links in legacy Linny-R models by default have 100% share-of-cost;
|
2323
2323
|
// to minimize conversion effort, set SoC for SINGLE links OUT of processes
|
2324
2324
|
// to 100%
|
2325
|
-
if(
|
2325
|
+
if(legacy_model) {
|
2326
2326
|
for(let l in this.links) if(this.links.hasOwnProperty(l)) {
|
2327
2327
|
l = this.links[l];
|
2328
2328
|
// NOTE: preserve non-zero SoC values, as these have been specified
|
@@ -2707,6 +2707,36 @@ class LinnyRModel {
|
|
2707
2707
|
links = [],
|
2708
2708
|
constraints = [],
|
2709
2709
|
can_calculate = true;
|
2710
|
+
const
|
2711
|
+
// NOTE: define local functions as constants
|
2712
|
+
costAffectingConstraints = (p) => {
|
2713
|
+
// Returns number of relevant contraints (see below) that
|
2714
|
+
// can affect the cost price of product or process `p`
|
2715
|
+
let n = 0;
|
2716
|
+
for(let i = 0; i < constraints.length; i++) {
|
2717
|
+
const c = constraints[i];
|
2718
|
+
if((c.to_node === p && c.soc_direction === VM.SOC_X_Y) ||
|
2719
|
+
(c.from_node === p && c.soc_direction === VM.SOC_Y_X)) n++;
|
2720
|
+
}
|
2721
|
+
return n;
|
2722
|
+
},
|
2723
|
+
inputsFromProcesses = (p, t) => {
|
2724
|
+
// Returns a tuple {n, nosoc, nz} where n is the number of input links
|
2725
|
+
// from processes, nosoc the number of these that carry no cost,
|
2726
|
+
// and nz the number of links having actual flow > 0
|
2727
|
+
let tuple = {n: 0, nosoc: 0, nz: 0};
|
2728
|
+
for(let i = 0; i < p.inputs.length; i++) {
|
2729
|
+
const l = p.inputs[i];
|
2730
|
+
// NOTE: only process --> product links can carry cost
|
2731
|
+
if(l.from_node instanceof Process) {
|
2732
|
+
tuple.n++;
|
2733
|
+
if(l.share_of_cost === 0) tuple.nosoc++;
|
2734
|
+
if(l.actualFlow(t) > VM.NEAR_ZERO) tuple.nz++;
|
2735
|
+
}
|
2736
|
+
}
|
2737
|
+
return tuple;
|
2738
|
+
};
|
2739
|
+
|
2710
2740
|
// First scan constraints X --> Y: these must have SoC > 0 and moreover
|
2711
2741
|
// the level of both X and Y must be non-zero, or they transfer no cost
|
2712
2742
|
for(let k in this.constraints) if(this.constraints.hasOwnProperty(k) &&
|
@@ -2741,12 +2771,7 @@ class LinnyRModel {
|
|
2741
2771
|
break;
|
2742
2772
|
}
|
2743
2773
|
// Count constraints that affect CP of this process
|
2744
|
-
let n =
|
2745
|
-
for(let i = 0; i < constraints.length; i++) {
|
2746
|
-
const c = constraints[i];
|
2747
|
-
if((c.to_node === p && c.soc_direction === VM.SOC_X_Y) ||
|
2748
|
-
(c.from_node === p && c.soc_direction === VM.SOC_Y_X)) n++;
|
2749
|
-
}
|
2774
|
+
let n = costAffectingConstraints(p);
|
2750
2775
|
if(n || p.inputs.length) {
|
2751
2776
|
// All inputs can affect the CP of a process
|
2752
2777
|
p.cost_price[t] = VM.UNDEFINED;
|
@@ -2765,65 +2790,38 @@ class LinnyRModel {
|
|
2765
2790
|
if(pr < 0) negpr -= pr * l.relative_rate.result(dt);
|
2766
2791
|
}
|
2767
2792
|
p.cost_price[t] = negpr;
|
2793
|
+
// Done, so not add to `processes` list
|
2768
2794
|
}
|
2769
2795
|
}
|
2770
2796
|
// Then scan the products
|
2771
2797
|
for(let k in this.products) if(this.products.hasOwnProperty(k) &&
|
2772
2798
|
!MODEL.ignored_entities[k]) {
|
2773
2799
|
const p = this.products[k];
|
2774
|
-
let
|
2775
|
-
nc =
|
2776
|
-
|
2777
|
-
|
2778
|
-
|
2779
|
-
const l = p.inputs[i];
|
2780
|
-
// NOTE: only process --> product links can carry cost
|
2781
|
-
if(l.share_of_cost > 0 && l.from_node instanceof Process) n++;
|
2782
|
-
}
|
2783
|
-
if(p.is_buffer) {
|
2784
|
-
// Stocks often introduce cycles; those having only zero-flow
|
2785
|
-
// links in/out have stockprice of t-1
|
2800
|
+
let ifp = inputsFromProcesses(p, t),
|
2801
|
+
nc = costAffectingConstraints(p);
|
2802
|
+
if(p.is_buffer && !ifp.nz) {
|
2803
|
+
// Stocks for which all INput links have flow = 0 have the same
|
2804
|
+
// stock price as in t-1
|
2786
2805
|
// NOTE: it is not good to check for zero stock, as that may be
|
2787
2806
|
// the net result of in/outflows
|
2788
|
-
|
2789
|
-
|
2790
|
-
|
2791
|
-
|
2792
|
-
|
2793
|
-
|
2794
|
-
|
2795
|
-
|
2796
|
-
|
2797
|
-
|
2798
|
-
}
|
2799
|
-
} else if(n > 1) {
|
2800
|
-
// NOTE: products having no storage, and *multiple* cost-carrying
|
2801
|
-
// input links that all are zero-flow have CP=0
|
2802
|
-
let nz = 0;
|
2803
|
-
for(let i = 0; i < p.inputs.length && !nz; i++) {
|
2804
|
-
if(p.inputs[i].actualFlow(t) > VM.NEAR_ZERO) nz++;
|
2805
|
-
}
|
2806
|
-
if(!nz) n = 0;
|
2807
|
-
}
|
2808
|
-
// Add number of cost-transferring constraints
|
2809
|
-
for(let i = 0; i < constraints.length; i++) {
|
2810
|
-
const c = constraints[i];
|
2811
|
-
if(c.to_node === p && c.soc_direction === VM.SOC_X_Y ||
|
2812
|
-
(c.from_node === p && c.soc_direction === VM.SOC_Y_X)) nc++;
|
2813
|
-
}
|
2814
|
-
if(n + nc) {
|
2807
|
+
p.cost_price[t] = p.stockPrice(t - 1);
|
2808
|
+
p.stock_price[t] = p.cost_price[t];
|
2809
|
+
} else if(!nc && (ifp.n === ifp.nosoc || (!ifp.nz && ifp.n > ifp.nosoc + 1))) {
|
2810
|
+
// For products having only input links that carry no cost,
|
2811
|
+
// CP = 0 but coded as NO_COST so that this can propagate.
|
2812
|
+
// Furthermore, for products having no storage and *multiple*
|
2813
|
+
// cost-carrying input links that all are zero-flow, the cost
|
2814
|
+
// price cannot be inferred unambiguously => set to 0
|
2815
|
+
p.cost_price[t] = (ifp.n && ifp.n === ifp.nosoc ? VM.NO_COST : 0);
|
2816
|
+
} else {
|
2815
2817
|
// Cost price must be calculated
|
2816
2818
|
p.cost_price[t] = VM.UNDEFINED;
|
2817
|
-
p.stock_price[t] = VM.UNDEFINED;
|
2818
2819
|
products.push(p);
|
2819
|
-
} else {
|
2820
|
-
// Cost price is zero (for stocks: CP[t-1])
|
2821
|
-
p.cost_price[t] = cp;
|
2822
|
-
p.stock_price[t] = cp;
|
2823
2820
|
}
|
2821
|
+
p.cost_price[t] = p.cost_price[t];
|
2824
2822
|
}
|
2825
|
-
// Finally, scan all links, and
|
2826
|
-
//
|
2823
|
+
// Finally, scan all links, and retain only those for which the CP
|
2824
|
+
// can not already be inferred from their FROM node
|
2827
2825
|
for(let k in this.links) if(this.links.hasOwnProperty(k) &&
|
2828
2826
|
!MODEL.ignored_entities[k]) {
|
2829
2827
|
const
|
@@ -2831,15 +2829,8 @@ class LinnyRModel {
|
|
2831
2829
|
ld = l.actualDelay(t),
|
2832
2830
|
fn = l.from_node,
|
2833
2831
|
fncp = fn.costPrice(t - ld),
|
2834
|
-
tn = l.to_node
|
2835
|
-
|
2836
|
-
if(fncp !== VM.UNDEFINED && fncp !== VM.NOT_COMPUTED) {
|
2837
|
-
// Links that are output of a node having CP defined have UCP = CP
|
2838
|
-
l.unit_cost_price = fncp;
|
2839
|
-
} else if(tncp !== VM.UNDEFINED && tncp !== VM.NOT_COMPUTED) {
|
2840
|
-
// Links that are input of a node having CP defined have UCP = CP
|
2841
|
-
l.unit_cost_price = 0;
|
2842
|
-
} else if(fn instanceof Product && fn.price.defined) {
|
2832
|
+
tn = l.to_node;
|
2833
|
+
if(fn instanceof Product && fn.price.defined) {
|
2843
2834
|
// Links from products having a market price have this price
|
2844
2835
|
// multiplied by their relative rate as unit CP
|
2845
2836
|
l.unit_cost_price = fn.price.result(t) * l.relative_rate.result(t);
|
@@ -2848,6 +2839,9 @@ class LinnyRModel {
|
|
2848
2839
|
// Process output links that do not carry cost and product-to-
|
2849
2840
|
// product links have unit CP = 0
|
2850
2841
|
l.unit_cost_price = 0;
|
2842
|
+
} else if(fncp !== VM.UNDEFINED && fncp !== VM.NOT_COMPUTED) {
|
2843
|
+
// Links that are output of a node having CP defined have UCP = CP
|
2844
|
+
l.unit_cost_price = fncp * l.relative_rate.result(t);
|
2851
2845
|
} else {
|
2852
2846
|
l.unit_cost_price = VM.UNDEFINED;
|
2853
2847
|
// Do not push links related to processes having level < 0
|
@@ -3001,14 +2995,15 @@ class LinnyRModel {
|
|
3001
2995
|
cp_sccp = VM.COMPUTING;
|
3002
2996
|
for(let j = 0; j < p.inputs.length; j++) {
|
3003
2997
|
const l = p.inputs[j];
|
3004
|
-
if(l.from_node instanceof Process
|
2998
|
+
if(l.from_node instanceof Process) {
|
3005
2999
|
cp = l.from_node.costPrice(t - l.actualDelay(t));
|
3006
|
-
if(cp === VM.UNDEFINED) {
|
3000
|
+
if(cp === VM.UNDEFINED && l.share_of_cost > 0) {
|
3001
|
+
// Contibuting CP still unknown => break from FOR loop
|
3007
3002
|
break;
|
3008
3003
|
} else {
|
3009
3004
|
if(cp_sccp === VM.COMPUTING) {
|
3010
3005
|
// First CC process having a defined CP => use this CP
|
3011
|
-
cp_sccp = cp;
|
3006
|
+
cp_sccp = cp * l.share_of_cost;
|
3012
3007
|
} else {
|
3013
3008
|
// Multiple CC processes => set CP to 0
|
3014
3009
|
cp_sccp = 0;
|
@@ -3034,7 +3029,7 @@ class LinnyRModel {
|
|
3034
3029
|
if(cp === VM.UNDEFINED) continue;
|
3035
3030
|
// CP of product is 0 if no new production UNLESS it has only
|
3036
3031
|
// one cost-carrying production input, as then its CP equals
|
3037
|
-
// the CP of the producing process;
|
3032
|
+
// the CP of the producing process times the link SoC;
|
3038
3033
|
// if new production > 0 then CP = cost / quantity
|
3039
3034
|
if(cp_sccp !== VM.COMPUTING) {
|
3040
3035
|
cp = (qnp > 0 ? cnp / qnp : cp_sccp);
|
@@ -6306,6 +6301,37 @@ class Node extends NodeBox {
|
|
6306
6301
|
}
|
6307
6302
|
return cac;
|
6308
6303
|
}
|
6304
|
+
|
6305
|
+
convertLegacyBoundData(lb_data, ub_data) {
|
6306
|
+
// Convert time series data for LB and UB in legacy models to datasets,
|
6307
|
+
// and replace attribute expressions by references to these datasets
|
6308
|
+
if(!lb_data && !ub_data) return;
|
6309
|
+
const same = lb_data === ub_data;
|
6310
|
+
if(lb_data) {
|
6311
|
+
const
|
6312
|
+
dsn = this.displayName + (same ? '' : ' LOWER') + ' BOUND DATA',
|
6313
|
+
ds = MODEL.addDataset(dsn);
|
6314
|
+
// Use the LB attribute as default value for the dataset
|
6315
|
+
ds.default_value = parseFloat(this.lower_bound.text);
|
6316
|
+
ds.data = stringToFloatArray(lb_data);
|
6317
|
+
ds.computeVector();
|
6318
|
+
ds.computeStatistics();
|
6319
|
+
this.lower_bound.text = `[${dsn}]`;
|
6320
|
+
if(same) this.equal_bounds = true;
|
6321
|
+
MODEL.legacy_datasets = true;
|
6322
|
+
}
|
6323
|
+
if(ub_data && !same) {
|
6324
|
+
const
|
6325
|
+
dsn = this.displayName + ' UPPER BOUND DATA',
|
6326
|
+
ds = MODEL.addDataset(dsn);
|
6327
|
+
ds.default_value = parseFloat(this.upper_bound.text);
|
6328
|
+
ds.data = stringToFloatArray(ub_data);
|
6329
|
+
ds.computeVector();
|
6330
|
+
ds.computeStatistics();
|
6331
|
+
this.upper_bound.text = `[${dsn}]`;
|
6332
|
+
MODEL.legacy_datasets = true;
|
6333
|
+
}
|
6334
|
+
}
|
6309
6335
|
|
6310
6336
|
actualLevel(t) {
|
6311
6337
|
// Returns the production level c.q. stock level for this node in time step t
|
@@ -6464,11 +6490,21 @@ class Process extends Node {
|
|
6464
6490
|
this.comments = xmlDecoded(nodeContentByTag(node, 'notes'));
|
6465
6491
|
this.lower_bound.text = xmlDecoded(nodeContentByTag(node, 'lower-bound'));
|
6466
6492
|
this.upper_bound.text = xmlDecoded(nodeContentByTag(node, 'upper-bound'));
|
6467
|
-
|
6468
|
-
|
6469
|
-
|
6470
|
-
|
6493
|
+
// legacy models can have LB and UB hexadecimal data strings
|
6494
|
+
this.convertLegacyBoundData(nodeContentByTag(node, 'lower-bound-data'),
|
6495
|
+
nodeContentByTag(node, 'upper-bound-data'));
|
6496
|
+
if(nodeParameterValue(node, 'reversible') === '1') {
|
6497
|
+
// For legacy "reversible" processes, the LB is set to -UB
|
6498
|
+
this.lower_bound.text = '-' + this.upper_bound.text;
|
6499
|
+
}
|
6500
|
+
// NOTE: legacy models have no initial level field => default to 0
|
6501
|
+
const ilt = xmlDecoded(nodeContentByTag(node, 'initial-level'));
|
6502
|
+
this.initial_level.text = ilt || '0';
|
6503
|
+
// NOTE: until version 1.0.16, pace was stored as a node parameter;
|
6504
|
+
const pace_text = nodeParameterValue(node, 'pace') +
|
6471
6505
|
xmlDecoded(nodeContentByTag(node, 'pace'));
|
6506
|
+
// NOTE: legacy models have no pace field => default to 1
|
6507
|
+
this.pace_expression.text = pace_text || '1';
|
6472
6508
|
// NOTE: immediately evaluate pace expression as integer
|
6473
6509
|
this.pace = Math.max(1, Math.floor(this.pace_expression.result(1)));
|
6474
6510
|
this.x = safeStrToInt(nodeContentByTag(node, 'x-coord'));
|
@@ -6873,18 +6909,25 @@ class Product extends Node {
|
|
6873
6909
|
this.equal_bounds = nodeParameterValue(node, 'equal-bounds') === '1';
|
6874
6910
|
this.integer_level = nodeParameterValue(node, 'integer-level') === '1';
|
6875
6911
|
this.no_slack = nodeParameterValue(node, 'no-slack') === '1';
|
6876
|
-
//
|
6877
|
-
|
6878
|
-
|
6912
|
+
// Legacy models have tag "hidden" instead of "no-links"
|
6913
|
+
this.no_links = (nodeParameterValue(node, 'no-links') ||
|
6914
|
+
nodeParameterValue(node, 'hidden')) === '1';
|
6879
6915
|
this.scale_unit = MODEL.addScaleUnit(
|
6880
6916
|
xmlDecoded(nodeContentByTag(node, 'unit')));
|
6881
|
-
//
|
6882
|
-
|
6883
|
-
|
6917
|
+
// Legacy models have tag "profit" instead of "price"
|
6918
|
+
let pp = nodeContentByTag(node, 'price');
|
6919
|
+
if(!pp) pp = nodeContentByTag(node, 'profit');
|
6920
|
+
this.price.text = xmlDecoded(pp);
|
6921
|
+
// Legacy models can have price time series data as hexadecimal string
|
6922
|
+
this.convertLegacyPriceData(nodeContentByTag(node, 'profit-data'));
|
6884
6923
|
this.lower_bound.text = xmlDecoded(nodeContentByTag(node, 'lower-bound'));
|
6885
6924
|
this.upper_bound.text = xmlDecoded(nodeContentByTag(node, 'upper-bound'));
|
6886
|
-
|
6887
|
-
|
6925
|
+
// legacy models can have LB and UB hexadecimal data strings
|
6926
|
+
this.convertLegacyBoundData(nodeContentByTag(node, 'lower-bound-data'),
|
6927
|
+
nodeContentByTag(node, 'upper-bound-data'));
|
6928
|
+
// NOTE: legacy models have no initial level field => default to 0
|
6929
|
+
const ilt = xmlDecoded(nodeContentByTag(node, 'initial-level'));
|
6930
|
+
this.initial_level.text = ilt || '0';
|
6888
6931
|
this.comments = xmlDecoded(nodeContentByTag(node, 'notes'));
|
6889
6932
|
this.x = safeStrToInt(nodeContentByTag(node, 'x-coord'));
|
6890
6933
|
this.y = safeStrToInt(nodeContentByTag(node, 'y-coord'));
|
@@ -6900,6 +6943,23 @@ class Product extends Node {
|
|
6900
6943
|
this.resize();
|
6901
6944
|
}
|
6902
6945
|
|
6946
|
+
convertLegacyPriceData(data) {
|
6947
|
+
// Convert time series data for prices in legacy models to a dataset,
|
6948
|
+
// and replace the price expression by a reference to this dataset
|
6949
|
+
if(data) {
|
6950
|
+
const
|
6951
|
+
dsn = this.displayName + ' PRICE DATA',
|
6952
|
+
ds = MODEL.addDataset(dsn);
|
6953
|
+
// Use the price attribute as default value for the dataset
|
6954
|
+
ds.default_value = parseFloat(this.price.text);
|
6955
|
+
ds.data = stringToFloatArray(data);
|
6956
|
+
ds.computeVector();
|
6957
|
+
ds.computeStatistics();
|
6958
|
+
this.price.text = `[${dsn}]`;
|
6959
|
+
MODEL.legacy_datasets = true;
|
6960
|
+
}
|
6961
|
+
}
|
6962
|
+
|
6903
6963
|
get defaultAttribute() {
|
6904
6964
|
return 'L';
|
6905
6965
|
}
|
@@ -7190,13 +7250,14 @@ class Link {
|
|
7190
7250
|
this.is_feedback = nodeParameterValue(node, 'is-feedback') === '1';
|
7191
7251
|
this.relative_rate.text = xmlDecoded(
|
7192
7252
|
nodeContentByTag(node, 'relative-rate'));
|
7193
|
-
|
7253
|
+
// NOTE: legacy models have no flow delay field => default to 0
|
7254
|
+
const fd_text = xmlDecoded(nodeContentByTag(node, 'delay'));
|
7255
|
+
this.flow_delay.text = fd_text || '0';
|
7194
7256
|
this.share_of_cost = safeStrToFloat(
|
7195
7257
|
nodeContentByTag(node, 'share-of-cost'), 0);
|
7196
|
-
if(
|
7258
|
+
if(!fd_text) {
|
7197
7259
|
// NOTE: default share-of-cost for links in legacy Linny-R was 100%;
|
7198
7260
|
// this is dysfunctional in JS Linny-R => set to 0 if equal to 1
|
7199
|
-
this.flow_delay.text = '0';
|
7200
7261
|
if(this.share_of_cost == 1) this.share_of_cost = 0;
|
7201
7262
|
}
|
7202
7263
|
this.comments = xmlDecoded(nodeContentByTag(node, 'notes'));
|
@@ -7368,6 +7429,8 @@ class Dataset {
|
|
7368
7429
|
// *model* time step t = 0
|
7369
7430
|
this.vector = [];
|
7370
7431
|
this.modifiers = {};
|
7432
|
+
// Selector to be used when model is run normally, i.e., no experiment
|
7433
|
+
this.default_selector = '';
|
7371
7434
|
}
|
7372
7435
|
|
7373
7436
|
get type() {
|
@@ -7753,7 +7816,8 @@ class Dataset {
|
|
7753
7816
|
'</method><url>', xmlEncoded(this.url),
|
7754
7817
|
'</url><data>', xmlEncoded(this.dataString),
|
7755
7818
|
'</data><modifiers>', ml.join(''),
|
7756
|
-
'</modifiers
|
7819
|
+
'</modifiers><default-selector>', xmlEncoded(this.default_selector),
|
7820
|
+
'</default-selector></dataset>'].join('');
|
7757
7821
|
return xml;
|
7758
7822
|
}
|
7759
7823
|
|
@@ -7784,6 +7848,12 @@ class Dataset {
|
|
7784
7848
|
}
|
7785
7849
|
}
|
7786
7850
|
}
|
7851
|
+
const ds = xmlDecoded(nodeContentByTag(node, 'default-selector'));
|
7852
|
+
if(ds && !this.modifiers[ds]) {
|
7853
|
+
UI.warn(`Dataset <tt>${this.name}</tt> has no selector <tt>${ds}</tt>`);
|
7854
|
+
} else {
|
7855
|
+
this.default_selector = ds;
|
7856
|
+
}
|
7787
7857
|
}
|
7788
7858
|
|
7789
7859
|
rename(name) {
|
@@ -554,6 +554,34 @@ function nameToLines(name, actor_name = '') {
|
|
554
554
|
return lines.join('\n');
|
555
555
|
}
|
556
556
|
|
557
|
+
//
|
558
|
+
// Linny-R legacy model conversion functions
|
559
|
+
//
|
560
|
+
|
561
|
+
function hexToFloat(s) {
|
562
|
+
const n = parseInt('0x' + s, 16);
|
563
|
+
if(isNaN(n)) return 0;
|
564
|
+
const
|
565
|
+
sign = (n >> 31 ? -1 : 1),
|
566
|
+
exp = Math.pow(2, ((n >> 23) & 0xFF) - 127);
|
567
|
+
f = sign * (n & 0x7fffff | 0x800000) * 1.0 / Math.pow(2, 23) * exp;
|
568
|
+
// NOTE: must consider precision of 32-bit floating point numbers!
|
569
|
+
return parseFloat(f.toPrecision(7));
|
570
|
+
}
|
571
|
+
|
572
|
+
function stringToFloatArray(s) {
|
573
|
+
let i = 8,
|
574
|
+
a = [];
|
575
|
+
while(i <= s.length) {
|
576
|
+
const
|
577
|
+
h = s.substr(i - 8, 8),
|
578
|
+
r = h.substr(6, 2) + h.substr(4, 2) + h.substr(2, 2) + h.substr(0, 2);
|
579
|
+
a.push(hexToFloat(r));
|
580
|
+
i += 8;
|
581
|
+
}
|
582
|
+
return a;
|
583
|
+
}
|
584
|
+
|
557
585
|
//
|
558
586
|
// Encryption-related functions
|
559
587
|
//
|
@@ -714,8 +714,11 @@ class ExpressionParser {
|
|
714
714
|
return [x, anchor1, offset1, anchor2, offset2];
|
715
715
|
}
|
716
716
|
}
|
717
|
+
|
718
|
+
//
|
717
719
|
// NOTE: for experiment results, the method will ALWAYS have returned
|
718
720
|
// a result, so what follows does not apply to experiment results
|
721
|
+
//
|
719
722
|
|
720
723
|
// Attribute name (optional) follows object-attribute separator
|
721
724
|
s = name.split(UI.OA_SEPARATOR);
|
@@ -725,7 +728,7 @@ class ExpressionParser {
|
|
725
728
|
// ... so restore name if itself contains other vertical bars
|
726
729
|
name = s.join(UI.OA_SEPARATOR).trim();
|
727
730
|
if(!attr) {
|
728
|
-
// Explicit empty attribute, e.g., [name|]
|
731
|
+
// Explicit *empty* attribute, e.g., [name|]
|
729
732
|
// NOTE: this matters for datasets having specifiers: the vertical
|
730
733
|
// bar indicates "do not infer a modifier from a running experiment,
|
731
734
|
// but use the data"
|
@@ -817,8 +820,12 @@ class ExpressionParser {
|
|
817
820
|
(attr ? ' and have attribute ' + attr : '');
|
818
821
|
return false;
|
819
822
|
}
|
823
|
+
|
824
|
+
//
|
820
825
|
// NOTE: for statistics, the method will ALWAYS have returned a result,
|
821
826
|
// so what follows does not apply to statistics results
|
827
|
+
//
|
828
|
+
|
822
829
|
let by_reference = false;
|
823
830
|
if(name === '.') {
|
824
831
|
// NOTE: when name is a single dot, it refers to the data of a dataset
|
@@ -1407,6 +1414,10 @@ class VirtualMachine {
|
|
1407
1414
|
// NOTE: below the "near zero" limit, a number is considered zero
|
1408
1415
|
// (this is to timely detect division-by-zero errors)
|
1409
1416
|
this.NEAR_ZERO = 1e-10;
|
1417
|
+
// Use a specific constant smaller than near-zero to denote "no cost"
|
1418
|
+
// to differentiate "no cost" form cost prices that really are 0
|
1419
|
+
this.NO_COST = 0.987654321e-10;
|
1420
|
+
|
1410
1421
|
// NOTE: allow for an accuracy margin: stocks may differ 0.1% from their
|
1411
1422
|
// target without displaying them in red or blue to signal shortage or surplus
|
1412
1423
|
this.SIG_DIF_LIMIT = 0.001;
|
@@ -1714,6 +1725,7 @@ class VirtualMachine {
|
|
1714
1725
|
if(n >= this.NOT_COMPUTED) return [true, '\u2297']; // Circled X
|
1715
1726
|
if(n >= this.UNDEFINED) return [true, '\u2047']; // Double question mark ??
|
1716
1727
|
if(n >= this.PLUS_INFINITY) return [true, '\u221E'];
|
1728
|
+
if(n === this.NO_COST) return [true, '\u00A2']; // c-slash (cent symbol)
|
1717
1729
|
return [false, n];
|
1718
1730
|
}
|
1719
1731
|
|
@@ -5222,15 +5234,26 @@ function VMI_push_dataset_modifier(x, args) {
|
|
5222
5234
|
MODEL.end_period - MODEL.start_period + MODEL.look_ahead + 1, t));
|
5223
5235
|
}
|
5224
5236
|
if(ms) {
|
5225
|
-
// If modifier selector is specified, use the associated expression
|
5237
|
+
// If modifier selector is specified, use the associated expression
|
5226
5238
|
obj = mx;
|
5227
|
-
|
5228
|
-
|
5229
|
-
|
5230
|
-
|
5231
|
-
|
5232
|
-
|
5233
|
-
|
5239
|
+
} else if(!ud) {
|
5240
|
+
if(MODEL.running_experiment) {
|
5241
|
+
// If an experiment is running, check if dataset modifiers match the
|
5242
|
+
// combination of selectors for the active run
|
5243
|
+
const mm = ds.matchingModifiers(MODEL.running_experiment.activeCombination);
|
5244
|
+
// If so, use the first match
|
5245
|
+
if(mm.length > 0) obj = mm[0].expression;
|
5246
|
+
} else if(ds.default_selector) {
|
5247
|
+
// If no expriment (so "normal" run), use default selector if specified
|
5248
|
+
const dm = ds.modifiers[ds.default_selector];
|
5249
|
+
if(dm) {
|
5250
|
+
obj = dm.expression;
|
5251
|
+
} else {
|
5252
|
+
// Exception should never occur, but check anyway and log it
|
5253
|
+
console.log('WARNING: Dataset "' + ds.name +
|
5254
|
+
`" has no default selector "${ds.default_selector}"`);
|
5255
|
+
}
|
5256
|
+
}
|
5234
5257
|
}
|
5235
5258
|
// By default, use the dataset default value
|
5236
5259
|
let v = ds.defaultValue,
|