linny-r 1.4.3 → 1.4.4

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.
Files changed (49) hide show
  1. package/README.md +102 -48
  2. package/package.json +1 -1
  3. package/server.js +31 -6
  4. package/static/images/check-off-not-same-changed.png +0 -0
  5. package/static/images/check-off-not-same-not-changed.png +0 -0
  6. package/static/images/check-off-same-changed.png +0 -0
  7. package/static/images/check-off-same-not-changed.png +0 -0
  8. package/static/images/check-on-not-same-changed.png +0 -0
  9. package/static/images/check-on-not-same-not-changed.png +0 -0
  10. package/static/images/check-on-same-changed.png +0 -0
  11. package/static/images/check-on-same-not-changed.png +0 -0
  12. package/static/images/eq-not-same-changed.png +0 -0
  13. package/static/images/eq-not-same-not-changed.png +0 -0
  14. package/static/images/eq-same-changed.png +0 -0
  15. package/static/images/eq-same-not-changed.png +0 -0
  16. package/static/images/ne-not-same-changed.png +0 -0
  17. package/static/images/ne-not-same-not-changed.png +0 -0
  18. package/static/images/ne-same-changed.png +0 -0
  19. package/static/images/ne-same-not-changed.png +0 -0
  20. package/static/images/sort-asc-lead.png +0 -0
  21. package/static/images/sort-asc.png +0 -0
  22. package/static/images/sort-desc-lead.png +0 -0
  23. package/static/images/sort-desc.png +0 -0
  24. package/static/images/sort-not.png +0 -0
  25. package/static/index.html +51 -35
  26. package/static/linny-r.css +167 -53
  27. package/static/scripts/linny-r-gui-actor-manager.js +340 -0
  28. package/static/scripts/linny-r-gui-chart-manager.js +944 -0
  29. package/static/scripts/linny-r-gui-constraint-editor.js +681 -0
  30. package/static/scripts/linny-r-gui-controller.js +4005 -0
  31. package/static/scripts/linny-r-gui-dataset-manager.js +1176 -0
  32. package/static/scripts/linny-r-gui-documentation-manager.js +739 -0
  33. package/static/scripts/linny-r-gui-equation-manager.js +307 -0
  34. package/static/scripts/linny-r-gui-experiment-manager.js +1944 -0
  35. package/static/scripts/linny-r-gui-expression-editor.js +449 -0
  36. package/static/scripts/linny-r-gui-file-manager.js +392 -0
  37. package/static/scripts/linny-r-gui-finder.js +727 -0
  38. package/static/scripts/linny-r-gui-model-autosaver.js +230 -0
  39. package/static/scripts/linny-r-gui-monitor.js +448 -0
  40. package/static/scripts/linny-r-gui-paper.js +2789 -0
  41. package/static/scripts/linny-r-gui-receiver.js +323 -0
  42. package/static/scripts/linny-r-gui-repository-browser.js +819 -0
  43. package/static/scripts/linny-r-gui-scale-unit-manager.js +244 -0
  44. package/static/scripts/linny-r-gui-sensitivity-analysis.js +778 -0
  45. package/static/scripts/linny-r-gui-undo-redo.js +560 -0
  46. package/static/scripts/linny-r-model.js +24 -11
  47. package/static/scripts/linny-r-utils.js +10 -0
  48. package/static/scripts/linny-r-vm.js +21 -12
  49. package/static/scripts/linny-r-gui.js +0 -16908
@@ -0,0 +1,1176 @@
1
+ /*
2
+ Linny-R is an executable graphical specification language for (mixed integer)
3
+ linear programming (MILP) problems, especially unit commitment problems (UCP).
4
+ The Linny-R language and tool have been developed by Pieter Bots at Delft
5
+ University of Technology, starting in 2009. The project to develop a browser-
6
+ based version started in 2017. See https://linny-r.org for more information.
7
+
8
+ This JavaScript file (linny-r-gui-datamgr.js) provides the GUI functionality
9
+ for the Linny-R Dataset Manager dialog.
10
+
11
+ */
12
+
13
+ /*
14
+ Copyright (c) 2017-2023 Delft University of Technology
15
+
16
+ Permission is hereby granted, free of charge, to any person obtaining a copy
17
+ of this software and associated documentation files (the "Software"), to deal
18
+ in the Software without restriction, including without limitation the rights to
19
+ use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
20
+ of the Software, and to permit persons to whom the Software is furnished to do
21
+ so, subject to the following conditions:
22
+
23
+ The above copyright notice and this permission notice shall be included in
24
+ all copies or substantial portions of the Software.
25
+
26
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
27
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
28
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
29
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
30
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
31
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
32
+ SOFTWARE.
33
+ */
34
+
35
+ // CLASS GUIDatasetManager provides the dataset dialog functionality
36
+ class GUIDatasetManager extends DatasetManager {
37
+ constructor() {
38
+ super();
39
+ this.dialog = UI.draggableDialog('dataset');
40
+ UI.resizableDialog('dataset', 'DATASET_MANAGER');
41
+ // Make toolbar buttons responsive
42
+ this.close_btn = document.getElementById('dataset-close-btn');
43
+ this.close_btn.addEventListener(
44
+ 'click', (event) => UI.toggleDialog(event));
45
+ document.getElementById('ds-new-btn').addEventListener(
46
+ // Shift-click on New button => add prefix of selected dataset
47
+ // (if any) to the name field of the dialog
48
+ 'click', () => DATASET_MANAGER.promptForDataset(event.shiftKey));
49
+ document.getElementById('ds-data-btn').addEventListener(
50
+ 'click', () => DATASET_MANAGER.editData());
51
+ document.getElementById('ds-rename-btn').addEventListener(
52
+ 'click', () => DATASET_MANAGER.promptForName());
53
+ document.getElementById('ds-clone-btn').addEventListener(
54
+ 'click', () => DATASET_MANAGER.cloneDataset());
55
+ document.getElementById('ds-delete-btn').addEventListener(
56
+ 'click', () => DATASET_MANAGER.deleteDataset());
57
+ document.getElementById('ds-filter-btn').addEventListener(
58
+ 'click', () => DATASET_MANAGER.toggleFilter());
59
+ // Update when filter input text changes
60
+ this.filter_text = document.getElementById('ds-filter-text');
61
+ this.filter_text.addEventListener(
62
+ 'input', () => DATASET_MANAGER.changeFilter());
63
+ this.dataset_table = document.getElementById('dataset-table');
64
+ // Data properties pane
65
+ this.properties = document.getElementById('dataset-properties');
66
+ // Toggle buttons at bottom of dialog
67
+ this.blackbox = document.getElementById('dataset-blackbox');
68
+ this.blackbox.addEventListener(
69
+ 'click', () => DATASET_MANAGER.toggleBlackBox());
70
+ this.outcome = document.getElementById('dataset-outcome');
71
+ this.outcome.addEventListener(
72
+ 'click', () => DATASET_MANAGER.toggleOutcome());
73
+ this.io_box = document.getElementById('dataset-io');
74
+ this.io_box.addEventListener(
75
+ 'click', () => DATASET_MANAGER.toggleImportExport());
76
+ // Modifier pane buttons
77
+ document.getElementById('ds-add-modif-btn').addEventListener(
78
+ 'click', () => DATASET_MANAGER.promptForSelector('new'));
79
+ document.getElementById('ds-rename-modif-btn').addEventListener(
80
+ 'click', () => DATASET_MANAGER.promptForSelector('rename'));
81
+ document.getElementById('ds-edit-modif-btn').addEventListener(
82
+ 'click', () => DATASET_MANAGER.editExpression());
83
+ document.getElementById('ds-delete-modif-btn').addEventListener(
84
+ 'click', () => DATASET_MANAGER.deleteModifier());
85
+ document.getElementById('ds-convert-modif-btn').addEventListener(
86
+ 'click', () => DATASET_MANAGER.promptToConvertModifiers());
87
+ // Modifier table
88
+ this.modifier_table = document.getElementById('dataset-modif-table');
89
+ // Modal dialogs
90
+ this.new_modal = new ModalDialog('new-dataset');
91
+ this.new_modal.ok.addEventListener(
92
+ 'click', () => DATASET_MANAGER.newDataset());
93
+ this.new_modal.cancel.addEventListener(
94
+ 'click', () => DATASET_MANAGER.new_modal.hide());
95
+ this.rename_modal = new ModalDialog('rename-dataset');
96
+ this.rename_modal.ok.addEventListener(
97
+ 'click', () => DATASET_MANAGER.renameDataset());
98
+ this.rename_modal.cancel.addEventListener(
99
+ 'click', () => DATASET_MANAGER.rename_modal.hide());
100
+ this.conversion_modal = new ModalDialog('convert-modifiers');
101
+ this.conversion_modal.ok.addEventListener(
102
+ 'click', () => DATASET_MANAGER.convertModifiers());
103
+ this.conversion_modal.cancel.addEventListener(
104
+ 'click', () => DATASET_MANAGER.conversion_modal.hide());
105
+ this.new_selector_modal = new ModalDialog('new-selector');
106
+ this.new_selector_modal.ok.addEventListener(
107
+ 'click', () => DATASET_MANAGER.newModifier());
108
+ this.new_selector_modal.cancel.addEventListener(
109
+ 'click', () => DATASET_MANAGER.new_selector_modal.hide());
110
+ this.rename_selector_modal = new ModalDialog('rename-selector');
111
+ this.rename_selector_modal.ok.addEventListener(
112
+ 'click', () => DATASET_MANAGER.renameModifier());
113
+ this.rename_selector_modal.cancel.addEventListener(
114
+ 'click', () => DATASET_MANAGER.rename_selector_modal.hide());
115
+ // The dataset time series dialog has more controls
116
+ this.series_modal = new ModalDialog('series');
117
+ this.series_modal.ok.addEventListener(
118
+ 'click', () => DATASET_MANAGER.saveSeriesData());
119
+ this.series_modal.cancel.addEventListener(
120
+ 'click', () => DATASET_MANAGER.series_modal.hide());
121
+ // Time-related controls must not be shown when array box is checked
122
+ // NOTE: use timeout to permit checkbox to update its status first
123
+ this.series_modal.element('array').addEventListener(
124
+ 'click', () => setTimeout(() => UI.toggle('series-no-time-msg'), 0));
125
+ // When URL is entered, data is fetched from this URL
126
+ this.series_modal.element('url').addEventListener(
127
+ 'blur', (event) => DATASET_MANAGER.getRemoteDataset(event.target.value));
128
+ // The series data text area must update its status line
129
+ this.series_data = this.series_modal.element('data');
130
+ this.series_data.addEventListener(
131
+ 'keyup', () => DATASET_MANAGER.updateLine());
132
+ this.series_data.addEventListener(
133
+ 'click', () => DATASET_MANAGER.updateLine());
134
+ this.reset();
135
+ }
136
+
137
+ reset() {
138
+ super.reset();
139
+ this.selected_prefix_row = null;
140
+ this.selected_modifier = null;
141
+ this.edited_expression = null;
142
+ this.filter_pattern = null;
143
+ this.clicked_object = null;
144
+ this.last_time_clicked = 0;
145
+ this.focal_table = null;
146
+ this.expanded_rows = [];
147
+ }
148
+
149
+ doubleClicked(obj) {
150
+ const
151
+ now = Date.now(),
152
+ dt = now - this.last_time_clicked;
153
+ this.last_time_clicked = now;
154
+ if(obj === this.clicked_object) {
155
+ // Consider click to be "double" if it occurred less than 300 ms ago
156
+ if(dt < 300) {
157
+ this.last_time_clicked = 0;
158
+ return true;
159
+ }
160
+ }
161
+ this.clicked_object = obj;
162
+ return false;
163
+ }
164
+
165
+ enterKey() {
166
+ // Open "edit" dialog for the selected dataset or modifier expression
167
+ const srl = this.focal_table.getElementsByClassName('sel-set');
168
+ if(srl.length > 0) {
169
+ const r = this.focal_table.rows[srl[0].rowIndex];
170
+ if(r) {
171
+ const e = new Event('click');
172
+ if(this.focal_table === this.dataset_table) {
173
+ // Emulate Alt-click in the table to open the time series dialog
174
+ e.altKey = true;
175
+ r.dispatchEvent(e);
176
+ } else if(this.focal_table === this.modifier_table) {
177
+ // Emulate a double-click on the second cell to edit the expression
178
+ this.last_time_clicked = Date.now();
179
+ r.cells[1].dispatchEvent(e);
180
+ }
181
+ }
182
+ }
183
+ }
184
+
185
+ upDownKey(dir) {
186
+ // Select row above or below the selected one (if possible)
187
+ const srl = this.focal_table.getElementsByClassName('sel-set');
188
+ if(srl.length > 0) {
189
+ let r = this.focal_table.rows[srl[0].rowIndex + dir];
190
+ while(r && r.style.display === 'none') {
191
+ r = (dir > 0 ? r.nextSibling : r.previousSibling);
192
+ }
193
+ if(r) {
194
+ UI.scrollIntoView(r);
195
+ // NOTE: cell, not row, listens for onclick event
196
+ if(this.focal_table === this.modifier_table) r = r.cells[1];
197
+ r.dispatchEvent(new Event('click'));
198
+ }
199
+ }
200
+ }
201
+
202
+ hideCollapsedRows() {
203
+ // Hides all rows except top level and immediate children of expanded
204
+ for(let i = 0; i < this.dataset_table.rows.length; i++) {
205
+ const
206
+ row = this.dataset_table.rows[i],
207
+ // Get the first DIV in the first TD of this row
208
+ first_div = row.firstChild.firstElementChild,
209
+ btn = first_div.dataset.prefix === 'x';
210
+ let p = row.dataset.prefix,
211
+ x = this.expanded_rows.indexOf(p) >= 0,
212
+ show = !p || x;
213
+ if(btn) {
214
+ const btn_div = row.getElementsByClassName('tree-btn')[0];
215
+ // Special expand/collapse row
216
+ if(show) {
217
+ // Set triangle to point down
218
+ btn_div.innerText = '\u25BC';
219
+ } else {
220
+ // Set triangle to point right
221
+ btn_div.innerText = '\u25BA';
222
+ // See whether "parent prefix" is expanded
223
+ p = p.split(UI.PREFIXER);
224
+ p.pop();
225
+ p = p.join(UI.PREFIXER);
226
+ // If so, then also show the row
227
+ show = (!p || this.expanded_rows.indexOf(p) >= 0);
228
+ }
229
+ }
230
+ row.style.display = (show ? 'block' : 'none');
231
+ }
232
+ }
233
+
234
+ togglePrefixRow(e) {
235
+ // Shows list items of the next prefix level
236
+ let r = e.target;
237
+ while(r.tagName !== 'TR') r = r.parentNode;
238
+ const
239
+ p = r.dataset.prefix,
240
+ i = this.expanded_rows.indexOf(p);
241
+ if(i >= 0) {
242
+ this.expanded_rows.splice(i, 1);
243
+ // Also remove all prefixes that have `p` as prefix
244
+ for(let j = this.expanded_rows.length - 1; j >= 0; j--) {
245
+ if(this.expanded_rows[j].startsWith(p + UI.PREFIXER)) {
246
+ this.expanded_rows.splice(j, 1);
247
+ }
248
+ }
249
+ } else {
250
+ addDistinct(p, this.expanded_rows);
251
+ }
252
+ this.hideCollapsedRows();
253
+ }
254
+
255
+ rowByPrefix(prefix) {
256
+ // Returns first table row with the specified prefix
257
+ if(!prefix) return null;
258
+ let lcp = prefix.toLowerCase(),
259
+ pl = lcp.split(': ');
260
+ // Remove trailing ': '
261
+ if(lcp.endsWith(': ')) {
262
+ pl.pop();
263
+ lcp = pl.join(': ');
264
+ }
265
+ while(pl.length > 0) {
266
+ addDistinct(pl.join(': '), this.expanded_rows);
267
+ pl.pop();
268
+ }
269
+ this.hideCollapsedRows();
270
+ for(let i = 0; i < this.dataset_table.rows.length; i++) {
271
+ const r = this.dataset_table.rows[i];
272
+ if(r.dataset.prefix === lcp) return r;
273
+ }
274
+ return null;
275
+ }
276
+
277
+ selectPrefixRow(e) {
278
+ // Selects expand/collapse prefix row
279
+ this.focal_table = this.dataset_table;
280
+ // NOTE: `e` can also be a string specifying the prefix to select
281
+ let r = e.target || this.rowByPrefix(e);
282
+ if(!r) return;
283
+ // Modeler may have clicked on the expand/collapse triangle;
284
+ const toggle = r.classList.contains('tree-btn');
285
+ while(r.tagName !== 'TR') r = r.parentNode;
286
+ this.selected_prefix_row = r;
287
+ const sel = this.dataset_table.getElementsByClassName('sel-set');
288
+ this.selected_dataset = null;
289
+ if(sel.length > 0) {
290
+ sel[0].classList.remove('sel-set');
291
+ this.updatePanes();
292
+ }
293
+ r.classList.add('sel-set');
294
+ if(!e.target) r.scrollIntoView({block: 'center'});
295
+ if(toggle || e.altKey || this.doubleClicked(r)) this.togglePrefixRow(e);
296
+ UI.enableButtons('ds-rename');
297
+ }
298
+
299
+ updateDialog() {
300
+ const
301
+ indent_px = 14,
302
+ dl = [],
303
+ dnl = [],
304
+ sd = this.selected_dataset,
305
+ ioclass = ['', 'import', 'export'];
306
+ for(let d in MODEL.datasets) if(MODEL.datasets.hasOwnProperty(d) &&
307
+ // NOTE: do not list "black-boxed" entities
308
+ !d.startsWith(UI.BLACK_BOX) &&
309
+ // NOTE: do not list the equations dataset
310
+ MODEL.datasets[d] !== MODEL.equations_dataset) {
311
+ if(!this.filter_pattern || this.filter_pattern.length === 0 ||
312
+ patternMatch(MODEL.datasets[d].displayName, this.filter_pattern)) {
313
+ dnl.push(d);
314
+ }
315
+ }
316
+ dnl.sort((a, b) => UI.compareFullNames(a, b, true));
317
+ // First determine indentation levels, prefixes and names
318
+ const
319
+ indent = [],
320
+ pref_ids = [],
321
+ names = [],
322
+ pref_names = {},
323
+ xids = [];
324
+ for(let i = 0; i < dnl.length; i++) {
325
+ const pref = UI.prefixesAndName(MODEL.datasets[dnl[i]].name);
326
+ // NOTE: only the name part (so no prefixes at all) will be shown
327
+ names.push(pref.pop());
328
+ indent.push(pref.length);
329
+ // NOTE: ignore case but join again with ": " because prefixes
330
+ // can contain any character; only the prefixer is "reserved"
331
+ const pref_id = pref.join(UI.PREFIXER).toLowerCase();
332
+ pref_ids.push(pref_id);
333
+ pref_names[pref_id] = pref;
334
+ }
335
+ let sdid = 'dstr',
336
+ prev_id = '',
337
+ ind_div = '';
338
+ for(let i = 0; i < dnl.length; i++) {
339
+ const
340
+ d = MODEL.datasets[dnl[i]],
341
+ pid = pref_ids[i];
342
+ if(indent[i]) {
343
+ ind_div = '<div class="ds-indent" style="width: ' +
344
+ indent[i] * indent_px + 'px">\u25B9</div>';
345
+ } else {
346
+ ind_div = '';
347
+ }
348
+ // NOTE: empty string should not add a collapse/expand row
349
+ if(pid && pid != prev_id && xids.indexOf(pid) < 0) {
350
+ // NOTE: XX: aa may be followed by XX: YY: ZZ: bb, which requires
351
+ // *two* collapsable lines: XX: YY and XX: YY: ZZ: before adding
352
+ // XX: YY: ZZ: bb
353
+ const
354
+ ps = pid.split(UI.PREFIXER),
355
+ pps = prev_id.split(UI.PREFIXER),
356
+ pn = pref_names[pid],
357
+ pns = pn.join(UI.PREFIXER),
358
+ lpl = [];
359
+ let lindent = 0;
360
+ // Ignore identical leading prefixes
361
+ while(ps.length > 0 && pps.length > 0 && ps[0] === pps[0]) {
362
+ lpl.push(ps.shift());
363
+ pps.shift();
364
+ pn.shift();
365
+ lindent++;
366
+ }
367
+ // Add a "collapse" row for each new prefix
368
+ while(ps.length > 0) {
369
+ lpl.push(ps.shift());
370
+ lindent++;
371
+ const lpid = lpl.join(UI.PREFIXER);
372
+ dl.push(['<tr data-prefix="', lpid,
373
+ '" data-prefix-name="', pns, '" class="dataset"',
374
+ 'onclick="DATASET_MANAGER.selectPrefixRow(event);"><td>',
375
+ // NOTE: data-prefix="x" signals that this is an extra row
376
+ (lindent > 0 ?
377
+ '<div data-prefix="x" style="width: ' + lindent * indent_px +
378
+ 'px"></div>' :
379
+ ''),
380
+ '<div data-prefix="x" class="tree-btn">',
381
+ (this.expanded_rows.indexOf(lpid) >= 0 ? '\u25BC' : '\u25BA'),
382
+ '</div>', pn.shift(), '</td></tr>'].join(''));
383
+ // Add to the list to prevent multiple c/x-rows for the same prefix
384
+ xids.push(lpid);
385
+ }
386
+ }
387
+ prev_id = pid;
388
+ let cls = ioclass[MODEL.ioType(d)];
389
+ if(d.outcome) {
390
+ cls += ' outcome';
391
+ } else if(d.array) {
392
+ cls += ' array';
393
+ } else if(d.data.length > 0) {
394
+ cls += ' series';
395
+ }
396
+ if(Object.keys(d.modifiers).length > 0) cls += ' modif';
397
+ if(d.black_box) cls += ' blackbox';
398
+ cls = cls.trim();
399
+ if(cls) cls = ' class="' + cls + '"';
400
+ if(d === sd) sdid += i;
401
+ dl.push(['<tr id="dstr', i, '" class="dataset',
402
+ (d === sd ? ' sel-set' : ''),
403
+ (d.default_selector ? ' def-sel' : ''),
404
+ '" data-prefix="', pid,
405
+ '" onclick="DATASET_MANAGER.selectDataset(event, \'',
406
+ dnl[i], '\');" onmouseover="DATASET_MANAGER.showInfo(\'', dnl[i],
407
+ '\', event.shiftKey);"><td>', ind_div, '<div', cls, '>',
408
+ names[i], '</td></tr>'].join(''));
409
+ }
410
+ this.dataset_table.innerHTML = dl.join('');
411
+ this.hideCollapsedRows();
412
+ const e = document.getElementById(sdid);
413
+ if(e) UI.scrollIntoView(e);
414
+ this.updatePanes();
415
+ }
416
+
417
+ updatePanes() {
418
+ const
419
+ sd = this.selected_dataset,
420
+ btns = 'ds-data ds-clone ds-delete ds-rename';
421
+ if(sd) {
422
+ this.properties.style.display = 'block';
423
+ document.getElementById('dataset-default').innerHTML =
424
+ VM.sig4Dig(sd.default_value) +
425
+ (sd.scale_unit === '1' ? '' : '&nbsp;' + sd.scale_unit);
426
+ document.getElementById('dataset-count').innerHTML = sd.data.length;
427
+ document.getElementById('dataset-special').innerHTML = sd.propertiesString;
428
+ if(sd.data.length > 0) {
429
+ document.getElementById('dataset-min').innerHTML = VM.sig4Dig(sd.min);
430
+ document.getElementById('dataset-max').innerHTML = VM.sig4Dig(sd.max);
431
+ document.getElementById('dataset-mean').innerHTML = VM.sig4Dig(sd.mean);
432
+ document.getElementById('dataset-stdev').innerHTML =
433
+ VM.sig4Dig(sd.standard_deviation);
434
+ document.getElementById('dataset-stats').style.display = 'block';
435
+ } else {
436
+ document.getElementById('dataset-stats').style.display = 'none';
437
+ }
438
+ if(sd.black_box) {
439
+ this.blackbox.classList.remove('off');
440
+ this.blackbox.classList.add('on');
441
+ } else {
442
+ this.blackbox.classList.remove('on');
443
+ this.blackbox.classList.add('off');
444
+ }
445
+ if(sd.outcome) {
446
+ this.outcome.classList.remove('not-selected');
447
+ } else {
448
+ this.outcome.classList.add('not-selected');
449
+ }
450
+ UI.setImportExportBox('dataset', MODEL.ioType(sd));
451
+ UI.enableButtons(btns);
452
+ } else {
453
+ this.properties.style.display = 'none';
454
+ UI.disableButtons(btns);
455
+ if(this.selected_prefix_row) UI.enableButtons('ds-rename');
456
+ }
457
+ this.updateModifiers();
458
+ }
459
+
460
+ updateModifiers() {
461
+ const
462
+ sd = this.selected_dataset,
463
+ hdr = document.getElementById('dataset-modif-header'),
464
+ name = document.getElementById('dataset-modif-ds-name'),
465
+ ttls = document.getElementById('dataset-modif-titles'),
466
+ mbtns = document.getElementById('dataset-modif-buttons'),
467
+ msa = document.getElementById('dataset-modif-scroll-area');
468
+ if(!sd) {
469
+ hdr.innerText = '(no dataset selected)';
470
+ name.style.display = 'none';
471
+ ttls.style.display = 'none';
472
+ msa.style.display = 'none';
473
+ mbtns.style.display = 'none';
474
+ return;
475
+ }
476
+ hdr.innerText = 'Modifiers of';
477
+ name.innerHTML = sd.displayName;
478
+ name.style.display = 'block';
479
+ const
480
+ ml = [],
481
+ msl = sd.selectorList,
482
+ sm = this.selected_modifier;
483
+ let smid = 'dsmtr';
484
+ for(let i = 0; i < msl.length; i++) {
485
+ const
486
+ m = sd.modifiers[UI.nameToID(msl[i])],
487
+ wild = m.hasWildcards,
488
+ defsel = (m.selector === sd.default_selector),
489
+ clk = '" onclick="DATASET_MANAGER.selectModifier(event, \'' +
490
+ m.selector + '\'';
491
+ if(m === sm) smid += i;
492
+ ml.push(['<tr id="dsmtr', i, '" class="dataset-modif',
493
+ (m === sm ? ' sel-set' : ''),
494
+ '"><td class="dataset-selector',
495
+ (wild ? ' wildcard' : ''),
496
+ '" title="Shift-click to ', (defsel ? 'clear' : 'set as'),
497
+ ' default modifier',
498
+ clk, ', false);">',
499
+ (defsel ? '<img src="images/solve.png" style="height: 14px;' +
500
+ ' width: 14px; margin: 0 1px -3px -1px;">' : ''),
501
+ (wild ? wildcardFormat(m.selector, true) : m.selector),
502
+ '</td><td class="dataset-expression',
503
+ clk, ');">', m.expression.text, '</td></tr>'].join(''));
504
+ }
505
+ this.modifier_table.innerHTML = ml.join('');
506
+ ttls.style.display = 'block';
507
+ msa.style.display = 'block';
508
+ mbtns.style.display = 'block';
509
+ if(sm) UI.scrollIntoView(document.getElementById(smid));
510
+ const btns = 'ds-rename-modif ds-edit-modif ds-delete-modif';
511
+ if(sm) {
512
+ UI.enableButtons(btns);
513
+ } else {
514
+ UI.disableButtons(btns);
515
+ }
516
+ // Check if dataset appears to "misuse" dataset modifiers
517
+ const
518
+ pml = sd.inferPrefixableModifiers,
519
+ e = document.getElementById('ds-convert-modif-btn');
520
+ if(pml.length > 0) {
521
+ e.style.display = 'inline-block';
522
+ e.title = 'Convert '+ pluralS(pml.length, 'modifier') +
523
+ ' to prefixed dataset(s)';
524
+ } else {
525
+ e.style.display = 'none';
526
+ }
527
+ }
528
+
529
+ showInfo(id, shift) {
530
+ // Display documentation for the dataset having identifier `id`
531
+ const d = MODEL.datasets[id];
532
+ if(d) DOCUMENTATION_MANAGER.update(d, shift);
533
+ }
534
+
535
+ toggleFilter() {
536
+ const
537
+ btn = document.getElementById('ds-filter-btn'),
538
+ bar = document.getElementById('ds-filter-bar'),
539
+ dsa = document.getElementById('dataset-scroll-area');
540
+ if(btn.classList.toggle('stay-activ')) {
541
+ bar.style.display = 'block';
542
+ dsa.style.top = '81px';
543
+ dsa.style.height = 'calc(100% - 141px)';
544
+ this.changeFilter();
545
+ } else {
546
+ bar.style.display = 'none';
547
+ dsa.style.top = '62px';
548
+ dsa.style.height = 'calc(100% - 122px)';
549
+ this.filter_pattern = null;
550
+ this.updateDialog();
551
+ }
552
+ }
553
+
554
+ changeFilter() {
555
+ this.filter_pattern = patternList(this.filter_text.value);
556
+ this.updateDialog();
557
+ }
558
+
559
+ selectDataset(event, id) {
560
+ // Select dataset, or edit it when Alt- or double-clicked
561
+ this.focal_table = this.dataset_table;
562
+ const
563
+ d = MODEL.datasets[id] || null,
564
+ edit = event.altKey || this.doubleClicked(d);
565
+ this.selected_dataset = d;
566
+ if(d && edit) {
567
+ this.last_time_clicked = 0;
568
+ this.editData();
569
+ return;
570
+ }
571
+ this.updateDialog();
572
+ }
573
+
574
+ selectModifier(event, id, x=true) {
575
+ // Select modifier, or when double-clicked, edit its expression or the
576
+ // name of the modifier
577
+ this.focal_table = this.modifier_table;
578
+ if(this.selected_dataset) {
579
+ const m = this.selected_dataset.modifiers[UI.nameToID(id)],
580
+ edit = event.altKey || this.doubleClicked(m);
581
+ if(event.shiftKey) {
582
+ // NOTE: prepare to update HTML class of selected dataset
583
+ const el = this.dataset_table.getElementsByClassName('sel-set')[0];
584
+ // Toggle dataset default selector
585
+ if(m.selector === this.selected_dataset.default_selector) {
586
+ this.selected_dataset.default_selector = '';
587
+ el.classList.remove('def-sel');
588
+ } else {
589
+ this.selected_dataset.default_selector = m.selector;
590
+ el.classList.add('def-sel');
591
+ }
592
+ }
593
+ this.selected_modifier = m;
594
+ if(edit) {
595
+ this.last_time_clicked = 0;
596
+ if(x) {
597
+ this.editExpression();
598
+ } else {
599
+ this.promptForSelector('rename');
600
+ }
601
+ return;
602
+ }
603
+ } else {
604
+ this.selected_modifier = null;
605
+ }
606
+ this.updateModifiers();
607
+ }
608
+
609
+ get selectedPrefix() {
610
+ // Returns the selected prefix (with its trailing colon-space)
611
+ const tr = this.selected_prefix_row;
612
+ if(tr && tr.dataset.prefixName) return tr.dataset.prefixName + UI.PREFIXER;
613
+ return '';
614
+ }
615
+
616
+ promptForDataset(shift=false) {
617
+ // Shift signifies: add prefix of selected dataset (if any) to
618
+ // the name field of the dialog
619
+ let prefix = '';
620
+ if(shift) {
621
+ if(this.selected_dataset) {
622
+ prefix = UI.completePrefix(this.selected_dataset.name);
623
+ } else if(this.selected_prefix) {
624
+ prefix = this.selectedPrefix;
625
+ }
626
+ }
627
+ this.new_modal.element('name').value = prefix;
628
+ this.new_modal.show('name');
629
+ }
630
+
631
+ newDataset() {
632
+ const n = this.new_modal.element('name').value.trim(),
633
+ d = MODEL.addDataset(n);
634
+ if(d) {
635
+ this.new_modal.hide();
636
+ this.selected_dataset = d;
637
+ this.focal_table = this.dataset_table;
638
+ this.updateDialog();
639
+ }
640
+ }
641
+
642
+ promptForName() {
643
+ // Prompts the modeler for a new name for the selected dataset (if any)
644
+ if(this.selected_dataset) {
645
+ this.rename_modal.element('title').innerText = 'Rename dataset';
646
+ this.rename_modal.element('name').value =
647
+ this.selected_dataset.displayName;
648
+ this.rename_modal.show('name');
649
+ } else if(this.selected_prefix_row) {
650
+ this.rename_modal.element('title').innerText = 'Rename datasets by prefix';
651
+ this.rename_modal.element('name').value = this.selectedPrefix.slice(0, -2);
652
+ this.rename_modal.show('name');
653
+ }
654
+ }
655
+
656
+ renameDataset() {
657
+ // Changes the name of the selected dataset
658
+ if(this.selected_dataset) {
659
+ const
660
+ inp = this.rename_modal.element('name'),
661
+ n = UI.cleanName(inp.value);
662
+ // Show modeler the "cleaned" new name
663
+ inp.value = n;
664
+ // Then try to rename -- this may generate a warning
665
+ if(this.selected_dataset.rename(n)) {
666
+ this.rename_modal.hide();
667
+ if(EXPERIMENT_MANAGER.selected_experiment) {
668
+ EXPERIMENT_MANAGER.selected_experiment.inferVariables();
669
+ }
670
+ UI.updateControllerDialogs('CDEFJX');
671
+ }
672
+ } else if(this.selected_prefix_row) {
673
+ // Create a list of datasets to be renamed
674
+ let e = this.rename_modal.element('name'),
675
+ prefix = e.value.trim();
676
+ e.focus();
677
+ // Trim trailing colon if user entered it
678
+ while(prefix.endsWith(':')) prefix = prefix.slice(0, -1);
679
+ // NOTE: prefix may be empty string, but otherwise should be a valid name
680
+ if(prefix && !UI.validName(prefix)) {
681
+ UI.warn('Invalid prefix');
682
+ return;
683
+ }
684
+ // Now add the colon-plus-space prefix separator
685
+ prefix += UI.PREFIXER;
686
+ const
687
+ oldpref = this.selectedPrefix,
688
+ key = oldpref.toLowerCase().split(UI.PREFIXER).join(':_'),
689
+ newkey = prefix.toLowerCase().split(UI.PREFIXER).join(':_'),
690
+ dsl = [];
691
+ // No change if new prefix is identical to old prefix
692
+ if(oldpref !== prefix) {
693
+ for(let k in MODEL.datasets) if(MODEL.datasets.hasOwnProperty(k)) {
694
+ if(k.startsWith(key)) dsl.push(k);
695
+ }
696
+ // NOTE: no check needed for mere upper/lower case changes
697
+ if(newkey !== key) {
698
+ let nc = 0;
699
+ for(let i = 0; i < dsl.length; i++) {
700
+ let nk = newkey + dsl[i].substring(key.length);
701
+ if(MODEL.datasets[nk]) nc++;
702
+ }
703
+ if(nc) {
704
+ UI.warn('Renaming ' + pluralS(dsl.length, 'dataset').toLowerCase() +
705
+ ' would cause ' + pluralS(nc, 'name conflict'));
706
+ return;
707
+ }
708
+ }
709
+ // Reset counts of effects of a rename operation
710
+ this.entity_count = 0;
711
+ this.expression_count = 0;
712
+ // Rename datasets one by one, suppressing notifications
713
+ for(let i = 0; i < dsl.length; i++) {
714
+ const d = MODEL.datasets[dsl[i]];
715
+ d.rename(d.displayName.replace(oldpref, prefix), false);
716
+ }
717
+ let msg = 'Renamed ' + pluralS(dsl.length, 'dataset').toLowerCase();
718
+ if(MODEL.variable_count) msg += ', and updated ' +
719
+ pluralS(MODEL.variable_count, 'variable') + ' in ' +
720
+ pluralS(MODEL.expression_count, 'expression');
721
+ UI.notify(msg);
722
+ if(EXPERIMENT_MANAGER.selected_experiment) {
723
+ EXPERIMENT_MANAGER.selected_experiment.inferVariables();
724
+ }
725
+ UI.updateControllerDialogs('CDEFJX');
726
+ this.selectPrefixRow(prefix);
727
+ }
728
+ }
729
+ this.rename_modal.hide();
730
+ }
731
+
732
+ cloneDataset() {
733
+ // Create a new dataset that is identical to the current one
734
+ if(this.selected_dataset) {
735
+ const d = this.selected_dataset;
736
+ let nn = d.name + '-copy';
737
+ while(MODEL.objectByName(nn)) {
738
+ nn += '-copy';
739
+ }
740
+ const nd = MODEL.addDataset(nn);
741
+ // Copy properties of d to nd
742
+ nd.comments = `${d.comments}`;
743
+ nd.default_value = d.default_value;
744
+ nd.scale_unit = d.scale_unit;
745
+ nd.time_scale = d.time_scale;
746
+ nd.time_unit = d.time_unit;
747
+ nd.method = d.method;
748
+ nd.periodic = d.periodic;
749
+ nd.outcome = d.outcome;
750
+ nd.array = d.array;
751
+ nd.url = d.url;
752
+ nd.data = d.data.slice();
753
+ for(let s in d.modifiers) if(d.modifiers.hasOwnProperty(s)) {
754
+ const
755
+ m = d.modifiers[s],
756
+ nm = nd.addModifier(m.selector);
757
+ nm.expression = new Expression(nd, s, m.expression.text);
758
+ }
759
+ nd.resetExpressions();
760
+ nd.computeStatistics();
761
+ this.selected_dataset = nd;
762
+ this.updateDialog();
763
+ }
764
+ }
765
+
766
+ deleteDataset() {
767
+ const d = this.selected_dataset;
768
+ // Double-check, just in case...
769
+ if(d && d !== MODEL.equations_dataset) {
770
+ MODEL.removeImport(d);
771
+ MODEL.removeExport(d);
772
+ delete MODEL.datasets[d.identifier];
773
+ this.selected_dataset = null;
774
+ this.updateDialog();
775
+ MODEL.updateDimensions();
776
+ }
777
+ }
778
+
779
+ toggleBlackBox() {
780
+ const d = this.selected_dataset;
781
+ if(d) {
782
+ d.black_box = !d.black_box;
783
+ this.updateDialog();
784
+ }
785
+ }
786
+
787
+ toggleOutcome() {
788
+ const d = this.selected_dataset;
789
+ if(d) {
790
+ // NOTE: arrays cannot be outcomes
791
+ if(d.array) {
792
+ d.outcome = false;
793
+ } else {
794
+ d.outcome = !d.outcome;
795
+ }
796
+ this.updateDialog();
797
+ if(!UI.hidden('experiment-dlg')) EXPERIMENT_MANAGER.updateDialog();
798
+ }
799
+ }
800
+
801
+ toggleImportExport() {
802
+ const d = this.selected_dataset;
803
+ if(d) {
804
+ MODEL.ioUpdate(d, (MODEL.ioType(d) + 1) % 3);
805
+ this.updateDialog();
806
+ }
807
+ }
808
+
809
+ promptForSelector(dlg) {
810
+ let ms = '',
811
+ md = this.new_selector_modal;
812
+ if(dlg === 'rename') {
813
+ if(this.selected_modifier) ms = this.selected_modifier.selector;
814
+ md = this.rename_selector_modal;
815
+ }
816
+ md.element('name').value = ms;
817
+ md.show('name');
818
+ }
819
+
820
+ newModifier() {
821
+ const
822
+ sel = this.new_selector_modal.element('name').value,
823
+ m = this.selected_dataset.addModifier(sel);
824
+ if(m) {
825
+ this.selected_modifier = m;
826
+ // NOTE: update dimensions only if dataset now has 2 or more modifiers
827
+ // (ignoring those with wildcards)
828
+ const sl = this.selected_dataset.plainSelectors;
829
+ if(sl.length > 1) MODEL.expandDimension(sl);
830
+ this.new_selector_modal.hide();
831
+ this.updateModifiers();
832
+ }
833
+ }
834
+
835
+ renameModifier() {
836
+ if(!this.selected_modifier) return;
837
+ const
838
+ wild = this.selected_modifier.hasWildcards,
839
+ sel = this.rename_selector_modal.element('name').value,
840
+ // NOTE: normal dataset selector, so remove all invalid characters
841
+ clean_sel = sel.replace(/[^a-zA-z0-9\%\+\-\?\*]/g, ''),
842
+ // Keep track of old name
843
+ oldm = this.selected_modifier,
844
+ // NOTE: addModifier returns existing one if selector not changed
845
+ m = this.selected_dataset.addModifier(clean_sel);
846
+ // NULL can result when new name is invalid
847
+ if(!m) return;
848
+ // If selected modifier was the dataset default selector, update it
849
+ if(oldm.selector === this.selected_dataset.default_selector) {
850
+ this.selected_dataset.default_selector = m.selector;
851
+ }
852
+ MODEL.renameSelectorInExperiments(oldm.selector, clean_sel);
853
+ // If only case has changed, just update the selector
854
+ if(m === oldm) {
855
+ m.selector = clean_sel;
856
+ this.updateDialog();
857
+ this.rename_selector_modal.hide();
858
+ return;
859
+ }
860
+ // Rest is needed only when a new modifier has been added
861
+ m.expression = oldm.expression;
862
+ if(wild) {
863
+ // Wildcard selector means: recompile the modifier expression
864
+ m.expression.attribute = m.selector;
865
+ m.expression.compile();
866
+ }
867
+ this.deleteModifier();
868
+ this.selected_modifier = m;
869
+ // Update all chartvariables referencing this dataset + old selector
870
+ const vl = MODEL.datasetVariables;
871
+ let cv_cnt = 0;
872
+ for(let i = 0; i < vl.length; i++) {
873
+ if(v.object === this.selected_dataset && v.attribute === oldm.selector) {
874
+ v.attribute = m.selector;
875
+ cv_cnt++;
876
+ }
877
+ }
878
+ // Also replace old selector in all expressions (count these as well)
879
+ const xr_cnt = MODEL.replaceAttributeInExpressions(
880
+ oldm.dataset.name + '|' + oldm.selector, m.selector);
881
+ // Notify modeler of changes (if any)
882
+ const msg = [];
883
+ if(cv_cnt) msg.push(pluralS(cv_cnt, ' chart variable'));
884
+ if(xr_cnt) msg.push(pluralS(xr_cnt, ' expression variable'));
885
+ if(msg.length) {
886
+ UI.notify('Updated ' + msg.join(' and '));
887
+ // Also update these stay-on-top dialogs, as they may display a
888
+ // variable name for this dataset + modifier
889
+ UI.updateControllerDialogs('CDEFJX');
890
+ }
891
+ // NOTE: update dimensions only if dataset now has 2 or more modifiers
892
+ // (ignoring those with wildcards)
893
+ const sl = this.selected_dataset.plainSelectors;
894
+ if(sl.length > 1) MODEL.expandDimension(sl);
895
+ this.rename_selector_modal.hide();
896
+ this.updateModifiers();
897
+ }
898
+
899
+ editExpression() {
900
+ const m = this.selected_modifier;
901
+ if(m) {
902
+ this.edited_expression = m.expression;
903
+ const md = UI.modals.expression;
904
+ md.element('property').innerHTML = this.selected_dataset.displayName +
905
+ UI.OA_SEPARATOR + m.selector;
906
+ md.element('text').value = m.expression.text;
907
+ document.getElementById('variable-obj').value = 0;
908
+ X_EDIT.updateVariableBar();
909
+ X_EDIT.clearStatusBar();
910
+ md.show('text');
911
+ }
912
+ }
913
+
914
+ modifyExpression(x) {
915
+ // Update and compile expression only if it has been changed
916
+ if (x != this.edited_expression.text) {
917
+ this.edited_expression.text = x;
918
+ this.edited_expression.compile();
919
+ }
920
+ this.edited_expression.reset();
921
+ this.edited_expression = null;
922
+ this.updateModifiers();
923
+ }
924
+
925
+ deleteModifier() {
926
+ // Delete modifier from selected dataset
927
+ const m = this.selected_modifier;
928
+ if(m) {
929
+ // If it was the dataset default modifier, clear the default
930
+ if(m.selector === this.selected_dataset.default_selector) {
931
+ this.selected_dataset.default_selector = '';
932
+ }
933
+ // Then simply remove the object
934
+ delete this.selected_dataset.modifiers[UI.nameToID(m.selector)];
935
+ this.selected_modifier = null;
936
+ this.updateModifiers();
937
+ MODEL.updateDimensions();
938
+ }
939
+ }
940
+
941
+ promptToConvertModifiers() {
942
+ // Convert modifiers of selected dataset to new prefixed datasets
943
+ const
944
+ ds = this.selected_dataset,
945
+ md = this.conversion_modal;
946
+ if(ds) {
947
+ md.element('prefix').value = ds.displayName;
948
+ md.show('prefix');
949
+ }
950
+ }
951
+
952
+ convertModifiers() {
953
+ // Convert modifiers of selected dataset to new prefixed datasets
954
+ if(!this.selected_dataset) return;
955
+ const
956
+ ds = this.selected_dataset,
957
+ md = this.conversion_modal,
958
+ e = md.element('prefix');
959
+ let prefix = e.value.trim(),
960
+ vcount = 0;
961
+ e.focus();
962
+ while(prefix.endsWith(':')) prefix = prefix.slice(0, -1);
963
+ // NOTE: prefix may be empty string, but otherwise should be a valid name
964
+ if(!UI.validName(prefix)) {
965
+ UI.warn('Invalid prefix');
966
+ return;
967
+ }
968
+ prefix += UI.PREFIXER;
969
+ const
970
+ dsn = ds.displayName,
971
+ pml = ds.inferPrefixableModifiers,
972
+ xl = MODEL.allExpressions,
973
+ vl = MODEL.datasetVariables,
974
+ nl = MODEL.notesWithTags;
975
+ for(let i = 0; i < pml.length; i++) {
976
+ // Create prefixed dataset with correct default value
977
+ const
978
+ m = pml[i],
979
+ sel = m.selector,
980
+ newds = MODEL.addDataset(prefix + sel);
981
+ if(newds) {
982
+ // Retain properties of the "parent" dataset
983
+ newds.scale_unit = ds.scale_unit;
984
+ newds.time_scale = ds.time_scale;
985
+ newds.time_unit = ds.time_unit;
986
+ // Set modifier's expression result as default value
987
+ newds.default_value = m.expression.result(1);
988
+ // Remove the modifier from the dataset
989
+ delete ds.modifiers[UI.nameToID(sel)];
990
+ // If it was the dataset default modifier, clear this default
991
+ if(sel === ds.default_selector) ds.default_selector = '';
992
+ // Rename variable in charts
993
+ const
994
+ from = dsn + UI.OA_SEPARATOR + sel,
995
+ to = newds.displayName;
996
+ for(let j = 0; j < vl.length; j++) {
997
+ const v = vl[j];
998
+ // NOTE: variable should match original dataset + selector
999
+ if(v.displayName === from) {
1000
+ // Change to new dataset WITHOUT selector
1001
+ v.object = newds;
1002
+ v.attribute = '';
1003
+ vcount++;
1004
+ }
1005
+ }
1006
+ // Rename variable in the Sensitivity Analysis
1007
+ for(let j = 0; j < MODEL.sensitivity_parameters.length; j++) {
1008
+ if(MODEL.sensitivity_parameters[j] === from) {
1009
+ MODEL.sensitivity_parameters[j] = to;
1010
+ vcount++;
1011
+ }
1012
+ }
1013
+ for(let j = 0; j < MODEL.sensitivity_outcomes.length; j++) {
1014
+ if(MODEL.sensitivity_outcomes[j] === from) {
1015
+ MODEL.sensitivity_outcomes[j] = to;
1016
+ vcount++;
1017
+ }
1018
+ }
1019
+ // Rename variable in expressions and notes
1020
+ const re = new RegExp(
1021
+ // Handle multiple spaces between words
1022
+ '\\[\\s*' + escapeRegex(from).replace(/\s+/g, '\\s+')
1023
+ // Handle spaces around the separator |
1024
+ .replace('\\|', '\\s*\\|\\s*') +
1025
+ // Pattern ends at any character that is invalid for a
1026
+ // dataset modifier selector (unlike equation names)
1027
+ '\\s*[^a-zA-Z0-9\\+\\-\\%\\_]', 'gi');
1028
+ for(let j = 0; j < xl.length; j++) {
1029
+ const
1030
+ x = xl[j],
1031
+ matches = x.text.match(re);
1032
+ if(matches) {
1033
+ for(let k = 0; k < matches.length; k++) {
1034
+ // NOTE: each match will start with the opening bracket,
1035
+ // but end with the first "non-selector" character, which
1036
+ // will typically be ']', but may also be '@' (and now that
1037
+ // units can be converted, also the '>' of the arrow '->')
1038
+ x.text = x.text.replace(matches[k], '[' + to + matches[k].slice(-1));
1039
+ vcount ++;
1040
+ }
1041
+ // Force recompilation
1042
+ x.code = null;
1043
+ }
1044
+ }
1045
+ for(let j = 0; j < nl.length; j++) {
1046
+ const
1047
+ n = nl[j],
1048
+ matches = n.contents.match(re);
1049
+ if(matches) {
1050
+ for(let k = 0; k < matches.length; k++) {
1051
+ // See NOTE above for the use of `slice` here
1052
+ n.contents = n.contents.replace(matches[k], '[' + to + matches[k].slice(-1));
1053
+ vcount ++;
1054
+ }
1055
+ // Note fields must be parsed again
1056
+ n.parsed = false;
1057
+ }
1058
+ }
1059
+ }
1060
+ }
1061
+ if(vcount) UI.notify('Renamed ' + pluralS(vcount, 'variable') +
1062
+ ' throughout the model');
1063
+ // Delete the original dataset unless it has series data
1064
+ if(ds.data.length === 0) this.deleteDataset();
1065
+ MODEL.updateDimensions();
1066
+ this.selected_dataset = null;
1067
+ this.selected_prefix_row = null;
1068
+ this.updateDialog();
1069
+ md.hide();
1070
+ this.selectPrefixRow(prefix);
1071
+ }
1072
+
1073
+ updateLine() {
1074
+ const
1075
+ ln = document.getElementById('series-line-number'),
1076
+ lc = document.getElementById('series-line-count');
1077
+ ln.innerHTML = this.series_data.value.substring(0,
1078
+ this.series_data.selectionStart).split('\n').length;
1079
+ lc.innerHTML = this.series_data.value.split('\n').length;
1080
+ }
1081
+
1082
+ editData() {
1083
+ // Show the Edit time series dialog
1084
+ const
1085
+ ds = this.selected_dataset,
1086
+ md = this.series_modal,
1087
+ cover = md.element('no-time-msg');
1088
+ if(ds) {
1089
+ md.element('default').value = ds.default_value;
1090
+ md.element('unit').value = ds.scale_unit;
1091
+ cover.style.display = (ds.array ? 'block' : 'none');
1092
+ md.element('time-scale').value = VM.sig4Dig(ds.time_scale);
1093
+ // Add options for time unit selector
1094
+ const ol = [];
1095
+ for(let u in VM.time_unit_shorthand) {
1096
+ if(VM.time_unit_shorthand.hasOwnProperty(u)) {
1097
+ ol.push(['<option value="', u,
1098
+ (u === ds.time_unit ? '" selected="selected' : ''),
1099
+ '">', VM.time_unit_shorthand[u], '</option>'].join(''));
1100
+ }
1101
+ }
1102
+ md.element('time-unit').innerHTML = ol.join('');
1103
+ // Add options for(dis)aggregation method selector
1104
+ ol.length = 0;
1105
+ for(let i = 0; i < this.methods.length; i++) {
1106
+ ol.push(['<option value="', this.methods[i],
1107
+ (this.methods[i] === ds.method ? '" selected="selected' : ''),
1108
+ '">', this.method_names[i], '</option>'].join(''));
1109
+ }
1110
+ md.element('method').innerHTML = ol.join('');
1111
+ // Update the "periodic" box
1112
+ UI.setBox('series-periodic', ds.periodic);
1113
+ // Update the "array" box
1114
+ UI.setBox('series-array', ds.array);
1115
+ md.element('url').value = ds.url;
1116
+ // Show data as decimal numbers (JS default notation) on separate lines
1117
+ this.series_data.value = ds.data.join('\n');
1118
+ md.show('default');
1119
+ }
1120
+ }
1121
+
1122
+ saveSeriesData() {
1123
+ const ds = this.selected_dataset;
1124
+ if(!ds) return false;
1125
+ const dv = UI.validNumericInput('series-default', 'default value');
1126
+ if(dv === false) return false;
1127
+ const ts = UI.validNumericInput('series-time-scale', 'time scale');
1128
+ if(ts === false) return false;
1129
+ // NOTE: Trim textarea value as it typically has trailing newlines
1130
+ let lines = this.series_data.value.trim();
1131
+ if(lines) {
1132
+ lines = lines.split('\n');
1133
+ } else {
1134
+ lines = [];
1135
+ }
1136
+ let n,
1137
+ data = [];
1138
+ for(let i = 0; i < lines.length; i++) {
1139
+ // consider comma's to denote the decimal period
1140
+ const txt = lines[i].trim().replace(',', '.');
1141
+ // consider blank lines as "no data" => replace by default value
1142
+ if(txt === '') {
1143
+ n = dv;
1144
+ } else {
1145
+ n = parseFloat(txt);
1146
+ if(isNaN(n) || '0123456789'.indexOf(txt[txt.length - 1]) < 0) {
1147
+ UI.warn(`Invalid number "${txt}" at line ${i + 1}`);
1148
+ return false;
1149
+ }
1150
+ }
1151
+ data.push(n);
1152
+ }
1153
+ // Save the data
1154
+ ds.default_value = dv;
1155
+ ds.changeScaleUnit(this.series_modal.element('unit').value);
1156
+ ds.time_scale = ts;
1157
+ ds.time_unit = this.series_modal.element('time-unit').value;
1158
+ ds.method = this.series_modal.element('method').value;
1159
+ ds.periodic = UI.boxChecked('series-periodic');
1160
+ ds.array = UI.boxChecked('series-array');
1161
+ if(ds.array) ds.outcome = false;
1162
+ ds.url = this.series_modal.element('url').value;
1163
+ ds.data = data;
1164
+ ds.computeVector();
1165
+ ds.computeStatistics();
1166
+ if(ds.data.length === 0 && !ds.array &&
1167
+ Object.keys(ds.modifiers).length > 0 &&
1168
+ ds.timeStepDuration !== MODEL.timeStepDuration) {
1169
+ UI.notify('Dataset time scale only affects time series data; ' +
1170
+ 'modifier expressions evaluate at model time scale');
1171
+ }
1172
+ this.series_modal.hide();
1173
+ this.updateDialog();
1174
+ }
1175
+
1176
+ } // END of class GUIDatasetManager