linny-r 1.4.2 → 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.
- package/README.md +162 -74
- package/package.json +1 -1
- package/server.js +145 -49
- package/static/images/check-off-not-same-changed.png +0 -0
- package/static/images/check-off-not-same-not-changed.png +0 -0
- package/static/images/check-off-same-changed.png +0 -0
- package/static/images/check-off-same-not-changed.png +0 -0
- package/static/images/check-on-not-same-changed.png +0 -0
- package/static/images/check-on-not-same-not-changed.png +0 -0
- package/static/images/check-on-same-changed.png +0 -0
- package/static/images/check-on-same-not-changed.png +0 -0
- package/static/images/eq-not-same-changed.png +0 -0
- package/static/images/eq-not-same-not-changed.png +0 -0
- package/static/images/eq-same-changed.png +0 -0
- package/static/images/eq-same-not-changed.png +0 -0
- package/static/images/ne-not-same-changed.png +0 -0
- package/static/images/ne-not-same-not-changed.png +0 -0
- package/static/images/ne-same-changed.png +0 -0
- package/static/images/ne-same-not-changed.png +0 -0
- package/static/images/octaeder.svg +993 -0
- package/static/images/sort-asc-lead.png +0 -0
- package/static/images/sort-asc.png +0 -0
- package/static/images/sort-desc-lead.png +0 -0
- package/static/images/sort-desc.png +0 -0
- package/static/images/sort-not.png +0 -0
- package/static/index.html +72 -647
- package/static/linny-r.css +199 -417
- package/static/scripts/linny-r-gui-actor-manager.js +340 -0
- package/static/scripts/linny-r-gui-chart-manager.js +944 -0
- package/static/scripts/linny-r-gui-constraint-editor.js +681 -0
- package/static/scripts/linny-r-gui-controller.js +4005 -0
- package/static/scripts/linny-r-gui-dataset-manager.js +1176 -0
- package/static/scripts/linny-r-gui-documentation-manager.js +739 -0
- package/static/scripts/linny-r-gui-equation-manager.js +307 -0
- package/static/scripts/linny-r-gui-experiment-manager.js +1944 -0
- package/static/scripts/linny-r-gui-expression-editor.js +449 -0
- package/static/scripts/linny-r-gui-file-manager.js +392 -0
- package/static/scripts/linny-r-gui-finder.js +727 -0
- package/static/scripts/linny-r-gui-model-autosaver.js +230 -0
- package/static/scripts/linny-r-gui-monitor.js +448 -0
- package/static/scripts/linny-r-gui-paper.js +2789 -0
- package/static/scripts/linny-r-gui-receiver.js +323 -0
- package/static/scripts/linny-r-gui-repository-browser.js +819 -0
- package/static/scripts/linny-r-gui-scale-unit-manager.js +244 -0
- package/static/scripts/linny-r-gui-sensitivity-analysis.js +778 -0
- package/static/scripts/linny-r-gui-undo-redo.js +560 -0
- package/static/scripts/linny-r-model.js +27 -11
- package/static/scripts/linny-r-utils.js +17 -2
- package/static/scripts/linny-r-vm.js +31 -12
- package/static/scripts/linny-r-gui.js +0 -16761
@@ -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' ? '' : ' ' + 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
|