linny-r 1.4.3 → 1.4.5
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 +102 -48
- package/package.json +1 -1
- package/server.js +31 -6
- 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/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 +51 -35
- package/static/linny-r.css +167 -53
- 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 +450 -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 +34 -15
- package/static/scripts/linny-r-utils.js +11 -1
- package/static/scripts/linny-r-vm.js +21 -12
- package/static/scripts/linny-r-gui.js +0 -16908
@@ -0,0 +1,560 @@
|
|
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-undo.js) provides the GUI undo/redo
|
9
|
+
functionality for the Linny-R model editor.
|
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 UndoEdit
|
36
|
+
class UndoEdit {
|
37
|
+
constructor(action) {
|
38
|
+
this.action = action;
|
39
|
+
// NOTE: store present focal cluster, as modeler may move to other clusters
|
40
|
+
// after an edit
|
41
|
+
this.cluster = MODEL.focal_cluster;
|
42
|
+
this.object_id = null;
|
43
|
+
// NOTE: the properties stored for an edit may differ, depending on the action
|
44
|
+
this.properties = [];
|
45
|
+
// Undo may involve restoring the `selected` property of selected items
|
46
|
+
this.selection = [];
|
47
|
+
this.xml = '';
|
48
|
+
}
|
49
|
+
|
50
|
+
get fullAction() {
|
51
|
+
// Returns a string that reflects this edit action
|
52
|
+
// If the identifier is set, return the action followed by the class name
|
53
|
+
// of the object. NOTE: `obj` should then not be NULL, but check anyway
|
54
|
+
if(this.action === 'drop' || this.action == 'lift') {
|
55
|
+
return `Move ${pluralS(this.properties.length, 'node')} to cluster ` +
|
56
|
+
MODEL.objectByID(this.object_id).displayName;
|
57
|
+
} else if(this.action === 'replace') {
|
58
|
+
return `Replace ${this.properties.g ? '' : '(locally) '}product \u2018` +
|
59
|
+
this.properties.p + '\u2019 by product \u2018' +
|
60
|
+
this.properties.r + '\u2019';
|
61
|
+
} else if(this.object_id) {
|
62
|
+
const
|
63
|
+
obj = MODEL.objectByID(this.object_id),
|
64
|
+
obt = (obj ? obj.type.toLowerCase() : 'UNKOWN ' + this.object_id);
|
65
|
+
return this.action + ' ' + obt;
|
66
|
+
// A REDO of "add" has as properties [class name, identifier] of the added object
|
67
|
+
} else if(this.action === 'add' && this.properties.length === 2) {
|
68
|
+
return 'add ' + this.properties[0].toLowerCase();
|
69
|
+
}
|
70
|
+
// By default, return the action without further specification
|
71
|
+
return this.action;
|
72
|
+
}
|
73
|
+
|
74
|
+
setSelection() {
|
75
|
+
// Compile the list of IDs of selected entities
|
76
|
+
this.selection.length = 0;
|
77
|
+
for(let i = 0; i < MODEL.selection.length; i++) {
|
78
|
+
this.selection.push(MODEL.selection[i].identifier);
|
79
|
+
}
|
80
|
+
}
|
81
|
+
|
82
|
+
get getSelection() {
|
83
|
+
// Return the list of entities that were selected at the time of the action
|
84
|
+
const ol = [];
|
85
|
+
for(let i = 0; i < this.selection.length; i++) {
|
86
|
+
const obj = MODEL.objectByID(this.selection[i]);
|
87
|
+
// Guard against pushing NULL pointers in case object is not found
|
88
|
+
if(obj) ol.push(obj);
|
89
|
+
}
|
90
|
+
return ol;
|
91
|
+
}
|
92
|
+
} // END of class UndoEdit
|
93
|
+
|
94
|
+
|
95
|
+
// CLASS UndoStack
|
96
|
+
// NOTE: this object actually comprises TWO stacks -- one with undoable actions
|
97
|
+
// and one with redoable actions
|
98
|
+
class UndoStack {
|
99
|
+
constructor() {
|
100
|
+
this.undoables = [];
|
101
|
+
this.redoables = [];
|
102
|
+
this.clear();
|
103
|
+
}
|
104
|
+
|
105
|
+
clear() {
|
106
|
+
this.undoables.length = 0;
|
107
|
+
this.redoables.length = 0;
|
108
|
+
}
|
109
|
+
|
110
|
+
get topUndo() {
|
111
|
+
// Return the short name of the top undoable action (if any)
|
112
|
+
const i = this.undoables.length;
|
113
|
+
if(i > 0) return this.undoables[i - 1].action;
|
114
|
+
return false;
|
115
|
+
}
|
116
|
+
|
117
|
+
get canUndo() {
|
118
|
+
// Return the "display name" of the top undoable action (if any)
|
119
|
+
const i = this.undoables.length;
|
120
|
+
if(i > 0) return `Undo "${this.undoables[i - 1].fullAction}"`;
|
121
|
+
return false;
|
122
|
+
}
|
123
|
+
|
124
|
+
get topRedo() {
|
125
|
+
// Return the short name of the top undoable action (if any)
|
126
|
+
const i = this.redoables.length;
|
127
|
+
if(i > 0) return this.redoables[i - 1].action;
|
128
|
+
return false;
|
129
|
+
}
|
130
|
+
|
131
|
+
get canRedo() {
|
132
|
+
// Return the "display name" of the top redoable action (if any)
|
133
|
+
const i = this.redoables.length;
|
134
|
+
if(i > 0) return `Redo "${this.redoables[i - 1].fullAction}"`;
|
135
|
+
return false;
|
136
|
+
}
|
137
|
+
|
138
|
+
addXML(xml) {
|
139
|
+
// Insert xml at the start (!) of any XML added previously to the UndoEdit
|
140
|
+
// at the top of the UNDO stack
|
141
|
+
const i = this.undoables.length;
|
142
|
+
if(i === 0) return false;
|
143
|
+
this.undoables[i-1].xml = xml + this.undoables[i-1].xml;
|
144
|
+
}
|
145
|
+
|
146
|
+
addOffset(dx, dy) {
|
147
|
+
// Add (dx, dy) to the offset of the "move" UndoEdit that should be at the
|
148
|
+
// top of the UNDO stack
|
149
|
+
let i = this.undoables.length;
|
150
|
+
if(i === 0) return false;
|
151
|
+
this.undoables[i-1].properties[3] += dx;
|
152
|
+
this.undoables[i-1].properties[4] += dy;
|
153
|
+
}
|
154
|
+
|
155
|
+
push(action, args=null, tentative=false) {
|
156
|
+
// Add an UndoEdit to the undo stack, labeled with edit action that is
|
157
|
+
// about to be performed. NOTE: the IDs of objects are stored, rather than
|
158
|
+
// the objects themselves, because deleted objects will have different
|
159
|
+
// memory addresses when restored by an UNDO
|
160
|
+
|
161
|
+
// Any action except "move" is likely to invalidate the solver result
|
162
|
+
if(action !== 'move' && !(
|
163
|
+
// Exceptions:
|
164
|
+
// (1) adding/modifying notes
|
165
|
+
(args instanceof Note)
|
166
|
+
)) VM.reset();
|
167
|
+
|
168
|
+
// If this edit is new (i.e., not a redo) then remove all "redoable" edits
|
169
|
+
if(!tentative) this.redoables.length = 0;
|
170
|
+
// If the undo stack is full then discard its bottom edit
|
171
|
+
if(this.undoables.length == CONFIGURATION.undo_stack_size) this.undoables.splice(0, 1);
|
172
|
+
const ue = new UndoEdit(action);
|
173
|
+
// For specific actions, store the IDs of the selected entities
|
174
|
+
if(['move', 'delete', 'drop', 'lift'].indexOf(action) >= 0) {
|
175
|
+
ue.setSelection();
|
176
|
+
}
|
177
|
+
// Set the properties of this undoable, depending on the type of action
|
178
|
+
if(action === 'move') {
|
179
|
+
// `args` holds the dragged node => store its ID and position
|
180
|
+
// NOTE: for products, use their ProductPosition in the focal cluster
|
181
|
+
const obj = (args instanceof Product ?
|
182
|
+
args.positionInFocalCluster : args);
|
183
|
+
ue.properties = [args.identifier, obj.x, obj.y, 0, 0];
|
184
|
+
// NOTE: object_id is NOT set, as dragged selection may contain
|
185
|
+
// multiple entities
|
186
|
+
} else if(action === 'add') {
|
187
|
+
// `args` holds the added entity => store its ID
|
188
|
+
ue.object_id = args.identifier;
|
189
|
+
} else if(action === 'drop' || action === 'lift') {
|
190
|
+
// Store ID of target cluster
|
191
|
+
ue.object_id = args.identifier;
|
192
|
+
ue.properties = MODEL.getSelectionPositions;
|
193
|
+
} else if(action === 'replace') {
|
194
|
+
// Replace passes its undo information as an object
|
195
|
+
ue.properties = args;
|
196
|
+
}
|
197
|
+
|
198
|
+
// NOTE: for a DELETE action, no properties are stored; the XML needed to
|
199
|
+
// restore deleted entities will be added by the respective delete methods
|
200
|
+
|
201
|
+
// Push the new edit onto the UNDO stack
|
202
|
+
this.undoables.push(ue);
|
203
|
+
// Update the GUI buttons
|
204
|
+
UI.updateButtons();
|
205
|
+
// NOTE: update the Finder only if needed, and with a delay because
|
206
|
+
// the "prepare for undo" is performed before the actual change
|
207
|
+
if(action !== 'move') setTimeout(() => { FINDER.updateDialog(); }, 5);
|
208
|
+
//console.log('push ' + action);
|
209
|
+
//console.log(UNDO_STACK);
|
210
|
+
}
|
211
|
+
|
212
|
+
pop(action='') {
|
213
|
+
// Remove the top edit (if any) from the stack if it has the specified action
|
214
|
+
// NOTE: pop does NOT undo the action (the model is not modified)
|
215
|
+
let i = this.undoables.length - 1;
|
216
|
+
if(i >= 0 && (action === '' || this.undoables[i].action === action)) {
|
217
|
+
this.undoables.pop();
|
218
|
+
UI.updateButtons();
|
219
|
+
}
|
220
|
+
//console.log('pop ' + action);
|
221
|
+
//console.log(UNDO_STACK);
|
222
|
+
}
|
223
|
+
|
224
|
+
doMove(ue) {
|
225
|
+
// This method implements shared code for UNDO and REDO of "move" actions
|
226
|
+
// First get the dragged node
|
227
|
+
let obj = MODEL.objectByID(ue.properties[0]);
|
228
|
+
if(obj) {
|
229
|
+
// For products, use the x and y of the ProductPosition
|
230
|
+
if(obj instanceof Product) obj = obj.positionInFocalCluster;
|
231
|
+
// Calculate the relative move (dx, dy)
|
232
|
+
const
|
233
|
+
dx = ue.properties[1] - obj.x,
|
234
|
+
dy = ue.properties[2] - obj.y,
|
235
|
+
tdx = -ue.properties[3],
|
236
|
+
tdy = -ue.properties[4];
|
237
|
+
// Update the undo edit's x and y properties so that it can be pushed onto
|
238
|
+
// the other stack (as the dragged node ID and the selection stays the same)
|
239
|
+
ue.properties[1] = obj.x;
|
240
|
+
ue.properties[2] = obj.y;
|
241
|
+
// Prepare to translate back (NOTE: this will also prepare for REDO)
|
242
|
+
ue.properties[3] = tdx;
|
243
|
+
ue.properties[4] = tdy;
|
244
|
+
// Translate the entire graph (NOTE: this does nothing if dx and dy both equal 0)
|
245
|
+
MODEL.translateGraph(tdx, tdy);
|
246
|
+
// Restore the selection as it was at the time of the "move" action
|
247
|
+
MODEL.selectList(ue.getSelection);
|
248
|
+
// Move the selection back to its original position
|
249
|
+
MODEL.moveSelection(dx - tdx, dy - tdy);
|
250
|
+
}
|
251
|
+
}
|
252
|
+
|
253
|
+
restoreFromXML(xml) {
|
254
|
+
// Restore deleted objects from XML and add them to the UndoEdit's selection
|
255
|
+
// (so that they can be RE-deleted)
|
256
|
+
// NOTES:
|
257
|
+
// (1) Store focal cluster, because this may change while initializing a
|
258
|
+
// cluster from XML
|
259
|
+
// (2) Set "selected" attribute of objects to FALSE, as the selection will
|
260
|
+
// be restored from UndoEdit
|
261
|
+
const n = parseXML(MODEL.xml_header + `<edits>${xml}</edits>`);
|
262
|
+
if(n && n.childNodes) {
|
263
|
+
let c, li = [], ppi = [], ci = [];
|
264
|
+
for(let i = 0; i < n.childNodes.length; i++) {
|
265
|
+
c = n.childNodes[i];
|
266
|
+
// Immediately restore "independent" entities ...
|
267
|
+
if(c.nodeName === 'dataset') {
|
268
|
+
MODEL.addDataset(xmlDecoded(nodeContentByTag(c, 'name')), c);
|
269
|
+
} else if(c.nodeName === 'actor') {
|
270
|
+
MODEL.addActor(xmlDecoded(nodeContentByTag(c, 'name')), c);
|
271
|
+
} else if(c.nodeName === 'note') {
|
272
|
+
const obj = MODEL.addNote(c);
|
273
|
+
obj.selected = false;
|
274
|
+
} else if(c.nodeName === 'process') {
|
275
|
+
const obj = MODEL.addProcess(xmlDecoded(nodeContentByTag(c, 'name')),
|
276
|
+
xmlDecoded(nodeContentByTag(c, 'owner')), c);
|
277
|
+
obj.selected = false;
|
278
|
+
} else if(c.nodeName === 'product') {
|
279
|
+
const obj = MODEL.addProduct(
|
280
|
+
xmlDecoded(nodeContentByTag(c, 'name')), c);
|
281
|
+
obj.selected = false;
|
282
|
+
} else if(c.nodeName === 'chart') {
|
283
|
+
MODEL.addChart(xmlDecoded(nodeContentByTag(c, 'title')), c);
|
284
|
+
// ... but merely collect indices of other entities
|
285
|
+
} else if(c.nodeName === 'link' || c.nodeName === 'constraint') {
|
286
|
+
li.push(i);
|
287
|
+
} else if(c.nodeName === 'product-position') {
|
288
|
+
ppi.push(i);
|
289
|
+
} else if(c.nodeName === 'cluster') {
|
290
|
+
ci.push(i);
|
291
|
+
}
|
292
|
+
}
|
293
|
+
// NOTE: collecting the indices of links, product positions and clusters
|
294
|
+
// saves the effort to iterate over ALL childnodes again
|
295
|
+
// First restore links and constraints
|
296
|
+
for(let i = 0; i < li.length; i++) {
|
297
|
+
c = n.childNodes[li[i]];
|
298
|
+
// Double-check that this node defines a link or a constraint
|
299
|
+
if(c.nodeName === 'link' || c.nodeName === 'constraint') {
|
300
|
+
let name = xmlDecoded(nodeContentByTag(c, 'from-name'));
|
301
|
+
let actor = xmlDecoded(nodeContentByTag(c, 'from-owner'));
|
302
|
+
if(actor != UI.NO_ACTOR) name += ` (${actor})`;
|
303
|
+
let fn = MODEL.nodeBoxByID(UI.nameToID(name));
|
304
|
+
if(fn) {
|
305
|
+
name = xmlDecoded(nodeContentByTag(c, 'to-name'));
|
306
|
+
actor = xmlDecoded(nodeContentByTag(c, 'to-owner'));
|
307
|
+
if(actor != UI.NO_ACTOR) name += ` (${actor})`;
|
308
|
+
let tn = MODEL.nodeBoxByID(UI.nameToID(name));
|
309
|
+
if(tn) {
|
310
|
+
if(c.nodeName === 'link') {
|
311
|
+
MODEL.addLink(fn, tn, c).selected = false;
|
312
|
+
} else {
|
313
|
+
MODEL.addConstraint(fn, tn, c).selected = false;
|
314
|
+
}
|
315
|
+
}
|
316
|
+
}
|
317
|
+
}
|
318
|
+
}
|
319
|
+
// Then restore product positions.
|
320
|
+
// NOTE: These correspond to the products that were part of the
|
321
|
+
// selection; all other product positions are restored as part of their
|
322
|
+
// containing clusters
|
323
|
+
for(let i = 0; i < ppi.length; i++) {
|
324
|
+
c = n.childNodes[ppi[i]];
|
325
|
+
// Double-check that this node defines a product position
|
326
|
+
if(c.nodeName === 'product-position') {
|
327
|
+
const obj = MODEL.nodeBoxByID(UI.nameToID(
|
328
|
+
xmlDecoded(nodeContentByTag(c, 'product-name'))));
|
329
|
+
if(obj) {
|
330
|
+
obj.selected = false;
|
331
|
+
MODEL.focal_cluster.addProductPosition(obj).initFromXML(c);
|
332
|
+
}
|
333
|
+
}
|
334
|
+
}
|
335
|
+
// Lastly, restore clusters.
|
336
|
+
// NOTE: Store focal cluster, because this may change while initializing
|
337
|
+
// a cluster from XML
|
338
|
+
const fc = MODEL.focal_cluster;
|
339
|
+
for(let i = 0; i < ci.length; i++) {
|
340
|
+
c = n.childNodes[ci[i]];
|
341
|
+
if(c.nodeName === 'cluster') {
|
342
|
+
const obj = MODEL.addCluster(xmlDecoded(nodeContentByTag(c, 'name')),
|
343
|
+
xmlDecoded(nodeContentByTag(c, 'owner')), c);
|
344
|
+
obj.selected = false;
|
345
|
+
|
346
|
+
// TEMPORARY trace (remove when done testing)
|
347
|
+
if (MODEL.focal_cluster === fc) {
|
348
|
+
console.log('NO refocus needed');
|
349
|
+
} else {
|
350
|
+
console.log('Refocusing from ... to ... : ', MODEL.focal_cluster, fc);
|
351
|
+
}
|
352
|
+
// Restore original focal cluster because addCluster may shift focus
|
353
|
+
// to a sub-cluster
|
354
|
+
MODEL.focal_cluster = fc;
|
355
|
+
}
|
356
|
+
}
|
357
|
+
}
|
358
|
+
MODEL.clearSelection();
|
359
|
+
}
|
360
|
+
|
361
|
+
undo() {
|
362
|
+
// Undo the most recent "undoable" action
|
363
|
+
let ue;
|
364
|
+
if(this.undoables.length > 0) {
|
365
|
+
UI.reset();
|
366
|
+
// Get the action to be undone
|
367
|
+
ue = this.undoables.pop();
|
368
|
+
// Focus on the cluster that was focal at the time of action
|
369
|
+
// NOTE: do this WITHOUT calling UI.makeFocalCluster because this
|
370
|
+
// clears the selection and redraws the graph
|
371
|
+
MODEL.focal_cluster = ue.cluster;
|
372
|
+
//console.log('undo' + ue.fullAction);
|
373
|
+
//console.log(ue);
|
374
|
+
if(ue.action === 'move') {
|
375
|
+
this.doMove(ue);
|
376
|
+
// NOTE: doMove modifies the undo edit so that it can be used as redo edit
|
377
|
+
this.redoables.push(ue);
|
378
|
+
} else if(ue.action === 'add') {
|
379
|
+
// UNDO add means deleting the lastly added entity
|
380
|
+
let obj = MODEL.objectByID(ue.object_id);
|
381
|
+
if(obj) {
|
382
|
+
// Prepare UndoEdit for redo
|
383
|
+
const ot = obj.type;
|
384
|
+
// Set properties to [class name, identifier] (for tooltip display and redo)
|
385
|
+
ue.properties = [ot, ue.object_id];
|
386
|
+
// NOTE: `action` remains "add", but ID is set to null because otherwise
|
387
|
+
// the fullAction method would fail
|
388
|
+
ue.object_id = null;
|
389
|
+
// Push the "delete" UndoEdit back onto the undo stack so that XML will
|
390
|
+
// be added to it
|
391
|
+
this.undoables.push(ue);
|
392
|
+
// Mimic the exact selection state immediately after adding the entity
|
393
|
+
MODEL.clearSelection();
|
394
|
+
MODEL.select(obj);
|
395
|
+
// Execute the proper delete, depending on the type of entity
|
396
|
+
if(ot === 'Link') {
|
397
|
+
MODEL.deleteLink(obj);
|
398
|
+
} else if(ot === 'Note') {
|
399
|
+
MODEL.focal_cluster.deleteNote(obj);
|
400
|
+
} else if(ot === 'Cluster') {
|
401
|
+
MODEL.deleteCluster(obj);
|
402
|
+
} else if(ot === 'Product') {
|
403
|
+
// NOTE: `deleteProduct` deletes the ProductPosition, and the product
|
404
|
+
// itself only if needed
|
405
|
+
MODEL.focal_cluster.deleteProduct(obj);
|
406
|
+
} else if(ot === 'Process') {
|
407
|
+
MODEL.deleteNode(obj);
|
408
|
+
}
|
409
|
+
// Clear the model's selection, since we've bypassed the regular
|
410
|
+
// `deleteSelection` routine
|
411
|
+
MODEL.selection.length = 0;
|
412
|
+
// Move the UndoEdit to the redo stack
|
413
|
+
this.redoables.push(this.undoables.pop());
|
414
|
+
}
|
415
|
+
} else if(ue.action === 'delete') {
|
416
|
+
this.restoreFromXML(ue.xml);
|
417
|
+
// Restore the selection as it was at the time of the "delete" action
|
418
|
+
MODEL.selectList(ue.getSelection);
|
419
|
+
// Clear the XML (not useful for REDO delete)
|
420
|
+
ue.xml = null;
|
421
|
+
this.redoables.push(ue);
|
422
|
+
} else if(ue.action === 'drop' || ue.action === 'lift') {
|
423
|
+
// Restore the selection as it was at the time of the action
|
424
|
+
MODEL.selectList(ue.getSelection);
|
425
|
+
// NOTE: first focus on the original target cluster
|
426
|
+
MODEL.focal_cluster = MODEL.objectByID(ue.object_id);
|
427
|
+
// Drop the selection "back" to the focal cluster
|
428
|
+
MODEL.dropSelectionIntoCluster(ue.cluster);
|
429
|
+
// Refocus on the original focal cluster
|
430
|
+
MODEL.focal_cluster = ue.cluster;
|
431
|
+
// NOTE: now restore the selection in THIS cluster!
|
432
|
+
MODEL.selectList(ue.getSelection);
|
433
|
+
// Now restore the position of the nodes
|
434
|
+
MODEL.setSelectionPositions(ue.properties);
|
435
|
+
this.redoables.push(ue);
|
436
|
+
// NOTE: a drop action will always be preceded by a move action
|
437
|
+
if(ue.action === 'drop') {
|
438
|
+
// Double-check, and if so, undo this move as well
|
439
|
+
if(this.topUndo === 'move') this.undo();
|
440
|
+
}
|
441
|
+
} else if(ue.action === 'replace') {
|
442
|
+
let uep = ue.properties,
|
443
|
+
p = MODEL.objectByName(uep.p);
|
444
|
+
// First check whether product P needs to be restored
|
445
|
+
if(!p && ue.xml) {
|
446
|
+
const n = parseXML(MODEL.xml_header + `<edits>${ue.xml}</edits>`);
|
447
|
+
if(n && n.childNodes) {
|
448
|
+
let c = n.childNodes[0];
|
449
|
+
if(c.nodeName === 'product') {
|
450
|
+
p = MODEL.addProduct(
|
451
|
+
xmlDecoded(nodeContentByTag(c, 'name')), c);
|
452
|
+
p.selected = false;
|
453
|
+
}
|
454
|
+
}
|
455
|
+
}
|
456
|
+
if(p) {
|
457
|
+
// Restore product position of P in focal cluster
|
458
|
+
MODEL.focal_cluster.addProductPosition(p, uep.x, uep.y);
|
459
|
+
// Restore links in/out of P
|
460
|
+
for(let i = 0; i < uep.lt.length; i++) {
|
461
|
+
const l = MODEL.linkByID(uep.lt[i]);
|
462
|
+
if(l) {
|
463
|
+
const ml = MODEL.addLink(l.from_node, p);
|
464
|
+
ml.copyPropertiesFrom(l);
|
465
|
+
MODEL.deleteLink(l);
|
466
|
+
}
|
467
|
+
}
|
468
|
+
for(let i = 0; i < uep.lf.length; i++) {
|
469
|
+
const l = MODEL.linkByID(uep.lf[i]);
|
470
|
+
if(l) {
|
471
|
+
const ml = MODEL.addLink(p, l.to_node);
|
472
|
+
ml.copyPropertiesFrom(l);
|
473
|
+
MODEL.deleteLink(l);
|
474
|
+
}
|
475
|
+
}
|
476
|
+
// Restore constraints on/by P
|
477
|
+
for(let i = 0; i < uep.ct.length; i++) {
|
478
|
+
const c = MODEL.constraintByID(uep.ct[i]);
|
479
|
+
if(c) {
|
480
|
+
const mc = MODEL.addConstraint(c.from_node, p);
|
481
|
+
mc.copyPropertiesFrom(c);
|
482
|
+
MODEL.deleteConstraint(c);
|
483
|
+
}
|
484
|
+
}
|
485
|
+
for(let i = 0; i < uep.cf.length; i++) {
|
486
|
+
const c = MODEL.constraintByID(uep.cf[i]);
|
487
|
+
if(c) c.fromNode = p;
|
488
|
+
if(c) {
|
489
|
+
const mc = MODEL.addConstraint(p, c.to_node);
|
490
|
+
mc.copyPropertiesFrom(c);
|
491
|
+
MODEL.deleteConstraint(c);
|
492
|
+
}
|
493
|
+
}
|
494
|
+
// NOTE: same UndoEdit object can be used for REDO
|
495
|
+
this.redoables.push(ue);
|
496
|
+
} else {
|
497
|
+
throw 'Failed to UNDO replace action';
|
498
|
+
}
|
499
|
+
}
|
500
|
+
// Update the main window
|
501
|
+
MODEL.focal_cluster.clearAllProcesses();
|
502
|
+
UI.drawDiagram(MODEL);
|
503
|
+
UI.updateButtons();
|
504
|
+
// Update the Finder if needed
|
505
|
+
if(ue.action !== 'move') FINDER.updateDialog();
|
506
|
+
}
|
507
|
+
//console.log('undo');
|
508
|
+
//console.log(UNDO_STACK);
|
509
|
+
}
|
510
|
+
|
511
|
+
redo() {
|
512
|
+
// Restore the model to its state prior to the last undo
|
513
|
+
if(this.redoables.length > 0) {
|
514
|
+
UI.reset();
|
515
|
+
let re = this.redoables.pop();
|
516
|
+
//console.log('redo ' + re.fullAction);
|
517
|
+
//console.log(UNDO_STACK);
|
518
|
+
// Focus on the cluster that was focal at the time of action
|
519
|
+
// NOTE: no call to UI.makeFocalCluster because this clears the selection
|
520
|
+
// and redraws the graph
|
521
|
+
MODEL.focal_cluster = re.cluster;
|
522
|
+
if(re.action === 'move') {
|
523
|
+
// NOTE: this is a mirror operation of the UNDO
|
524
|
+
this.doMove(re);
|
525
|
+
// NOTE: doMove modifies the RedoEdit so that it can be used as UndoEdit
|
526
|
+
this.undoables.push(re);
|
527
|
+
// NOTE: when next redoable action is "drop", redo this as well
|
528
|
+
if(this.topRedo === 'drop') this.redo();
|
529
|
+
} else if(re.action === 'add') {
|
530
|
+
//console.log('ADD redo properties:', re.properties);
|
531
|
+
// NOTE: redo an undone "add" => mimick undoing a "delete"
|
532
|
+
this.restoreFromXML(re.xml);
|
533
|
+
// Clear the XML and restore the object identifier
|
534
|
+
re.xml = null;
|
535
|
+
re.object_id = re.properties[1];
|
536
|
+
this.undoables.push(re);
|
537
|
+
} else if(re.action === 'delete') {
|
538
|
+
// Restore the selection as it was at the time of the "delete" action
|
539
|
+
MODEL.selectList(re.getSelection);
|
540
|
+
this.undoables.push(re);
|
541
|
+
// Then perform a delete action
|
542
|
+
MODEL.deleteSelection();
|
543
|
+
} else if(re.action === 'drop' || re.action === 'lift') {
|
544
|
+
const c = MODEL.objectByID(re.object_id);
|
545
|
+
if(c instanceof Cluster) MODEL.dropSelectionIntoCluster(c);
|
546
|
+
} else if(re.action === 'replace') {
|
547
|
+
const
|
548
|
+
p = MODEL.objectByName(re.properties.p),
|
549
|
+
r = MODEL.objectByName(re.properties.r);
|
550
|
+
if(p instanceof Product && r instanceof Product) {
|
551
|
+
MODEL.doReplace(p, r, re.properties.g);
|
552
|
+
}
|
553
|
+
}
|
554
|
+
MODEL.focal_cluster.clearAllProcesses();
|
555
|
+
UI.drawDiagram(MODEL);
|
556
|
+
UI.updateButtons();
|
557
|
+
if(re.action !== 'move') FINDER.updateDialog();
|
558
|
+
}
|
559
|
+
}
|
560
|
+
} // END of class UndoStack
|
@@ -2988,7 +2988,8 @@ class LinnyRModel {
|
|
2988
2988
|
if(names.indexOf(n) < 0) {
|
2989
2989
|
// Here, too, NULL can be used as "owner chart".
|
2990
2990
|
const cv = new ChartVariable(null);
|
2991
|
-
|
2991
|
+
// NOTE: For equations, the object is the dataset modifier.
|
2992
|
+
cv.setProperties(eq ? dm : ds, dm.selector, false, '#000000');
|
2992
2993
|
vbls.push(cv);
|
2993
2994
|
}
|
2994
2995
|
}
|
@@ -5389,11 +5390,17 @@ class NodeBox extends ObjectWithXYWH {
|
|
5389
5390
|
}
|
5390
5391
|
|
5391
5392
|
drawWithLinks() {
|
5392
|
-
|
5393
|
+
const fc = this.cluster;
|
5394
|
+
// Do not redraw if this node is not visible in the focal cluster.
|
5395
|
+
if(this instanceof Product) {
|
5396
|
+
if(!this.positionInFocalCluster) return;
|
5397
|
+
} else {
|
5398
|
+
if(fc !== MODEL.focal_cluster) return;
|
5399
|
+
}
|
5393
5400
|
UI.drawObject(this);
|
5401
|
+
// @@TO DO: Also draw relevant arrows when this is a cluster.
|
5394
5402
|
if(this instanceof Cluster) return;
|
5395
|
-
// Draw
|
5396
|
-
const fc = this.cluster;
|
5403
|
+
// Draw all *visible* arrows associated with this node.
|
5397
5404
|
fc.categorizeEntities();
|
5398
5405
|
// Make list of arrows that represent a link related to this node.
|
5399
5406
|
let a,
|
@@ -5413,6 +5420,11 @@ class NodeBox extends ObjectWithXYWH {
|
|
5413
5420
|
}
|
5414
5421
|
// Draw all arrows in this list.
|
5415
5422
|
for(let i = 0; i < alist.length; i++) UI.drawObject(alist[i]);
|
5423
|
+
// Also draw related constraint arrows.
|
5424
|
+
for(let k in MODEL.constraints) if(MODEL.constraints.hasOwnProperty(k)) {
|
5425
|
+
const c = MODEL.constraints[k];
|
5426
|
+
if(c.from_node === this || c.to_node === this) UI.drawObject(c);
|
5427
|
+
}
|
5416
5428
|
}
|
5417
5429
|
|
5418
5430
|
clearHiddenIO() {
|
@@ -6936,7 +6948,7 @@ class Node extends NodeBox {
|
|
6936
6948
|
|
6937
6949
|
setConstraintOffsets() {
|
6938
6950
|
// Sets the offset properties of the constraints that relate to this
|
6939
|
-
// node; these properties are used when drawing these constraints
|
6951
|
+
// node; these properties are used when drawing these constraints.
|
6940
6952
|
const tbc = {top: [], bottom: [], thumb: []};
|
6941
6953
|
for(let k in MODEL.constraints) if(MODEL.constraints.hasOwnProperty(k)) {
|
6942
6954
|
const
|
@@ -6952,21 +6964,21 @@ class Node extends NodeBox {
|
|
6952
6964
|
continue;
|
6953
6965
|
}
|
6954
6966
|
if(vn[0] && vn[1]) {
|
6955
|
-
// Both nodes visible => arrow
|
6967
|
+
// Both nodes visible => arrow.
|
6956
6968
|
if(this.y < q.y - (q.height/2 + 3) - this.height) {
|
6957
|
-
//
|
6969
|
+
// Arrow from bottom of THIS to top of q.
|
6958
6970
|
c.bottom_x = q.x;
|
6959
6971
|
c.bottom_y = q.y;
|
6960
6972
|
tbc.bottom.push(c);
|
6961
6973
|
} else {
|
6962
|
-
//
|
6974
|
+
// Arrow from bottom of THIS to top of q.
|
6963
6975
|
c.top_x = q.x;
|
6964
6976
|
c.top_y = q.y;
|
6965
6977
|
tbc.top.push(c);
|
6966
6978
|
}
|
6967
6979
|
} else {
|
6968
|
-
// One node visible => thumbnail at top of this process
|
6969
|
-
// NOTE: X coordinate not needed for sorting
|
6980
|
+
// One node visible => thumbnail at top of this process.
|
6981
|
+
// NOTE: X coordinate not needed for sorting.
|
6970
6982
|
tbc.thumb.push(c);
|
6971
6983
|
}
|
6972
6984
|
}
|
@@ -7012,7 +7024,8 @@ class Node extends NodeBox {
|
|
7012
7024
|
// Keep 10px between connection points, and for processes
|
7013
7025
|
// also keep this distance from the rectangle corners
|
7014
7026
|
margin = 10,
|
7015
|
-
aw = (this instanceof Process ?
|
7027
|
+
aw = (this instanceof Process ?
|
7028
|
+
(this.collapsed ? 8.5 : this.width / 2) :
|
7016
7029
|
(this.width - this.height) / 2 + margin) - 9 * thl;
|
7017
7030
|
if(tl > 0) {
|
7018
7031
|
tbc.top.sort(tcmp);
|
@@ -7485,11 +7498,12 @@ class Process extends Node {
|
|
7485
7498
|
}
|
7486
7499
|
|
7487
7500
|
attributeExpression(a) {
|
7488
|
-
// Processes have
|
7501
|
+
// Processes have four expression attributes
|
7489
7502
|
if(a === 'LB') return this.lower_bound;
|
7490
7503
|
if(a === 'UB') {
|
7491
7504
|
return (this.equal_bounds ? this.lower_bound : this.upper_bound);
|
7492
7505
|
}
|
7506
|
+
if(a === 'LCF') return this.pace_expression;
|
7493
7507
|
if(a === 'IL') return this.initial_level;
|
7494
7508
|
return null;
|
7495
7509
|
}
|
@@ -8900,9 +8914,12 @@ class ChartVariable {
|
|
8900
8914
|
// by its scale factor unless it equals 1 (no scaling).
|
8901
8915
|
const sf = (this.scale_factor === 1 ? '' :
|
8902
8916
|
` (x${VM.sig4Dig(this.scale_factor)})`);
|
8903
|
-
//
|
8904
|
-
if(this.object
|
8905
|
-
|
8917
|
+
//Display name of equation is just the equations dataset selector.
|
8918
|
+
if(this.object instanceof DatasetModifier ||
|
8919
|
+
// NOTE: Same holds for "dummy variables" added for wildcard
|
8920
|
+
// dataset selectors.
|
8921
|
+
this.object === MODEL.equations_dataset) {
|
8922
|
+
let eqn = this.object.selector;
|
8906
8923
|
if(this.wildcard_index !== false) {
|
8907
8924
|
eqn = eqn.replace('??', this.wildcard_index);
|
8908
8925
|
}
|
@@ -9286,6 +9303,8 @@ class Chart {
|
|
9286
9303
|
vlist.push(v);
|
9287
9304
|
}
|
9288
9305
|
return vlist;
|
9306
|
+
// FALL-THROUGH: Equation name has no wildcard => use the equation
|
9307
|
+
// object (= dataset modifier) as `obj`.
|
9289
9308
|
}
|
9290
9309
|
const v = new ChartVariable(this);
|
9291
9310
|
v.setProperties(obj, a, false, this.nextAvailableDefaultColor, 1, 1);
|