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.
- 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 +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 +24 -11
- package/static/scripts/linny-r-utils.js +10 -0
- 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
|
@@ -5389,11 +5389,17 @@ class NodeBox extends ObjectWithXYWH {
|
|
5389
5389
|
}
|
5390
5390
|
|
5391
5391
|
drawWithLinks() {
|
5392
|
-
|
5392
|
+
const fc = this.cluster;
|
5393
|
+
// Do not redraw if this node is not visible in the focal cluster.
|
5394
|
+
if(this instanceof Product) {
|
5395
|
+
if(!this.positionInFocalCluster) return;
|
5396
|
+
} else {
|
5397
|
+
if(fc !== MODEL.focal_cluster) return;
|
5398
|
+
}
|
5393
5399
|
UI.drawObject(this);
|
5400
|
+
// @@TO DO: Also draw relevant arrows when this is a cluster.
|
5394
5401
|
if(this instanceof Cluster) return;
|
5395
|
-
// Draw
|
5396
|
-
const fc = this.cluster;
|
5402
|
+
// Draw all *visible* arrows associated with this node.
|
5397
5403
|
fc.categorizeEntities();
|
5398
5404
|
// Make list of arrows that represent a link related to this node.
|
5399
5405
|
let a,
|
@@ -5413,6 +5419,11 @@ class NodeBox extends ObjectWithXYWH {
|
|
5413
5419
|
}
|
5414
5420
|
// Draw all arrows in this list.
|
5415
5421
|
for(let i = 0; i < alist.length; i++) UI.drawObject(alist[i]);
|
5422
|
+
// Also draw related constraint arrows.
|
5423
|
+
for(let k in MODEL.constraints) if(MODEL.constraints.hasOwnProperty(k)) {
|
5424
|
+
const c = MODEL.constraints[k];
|
5425
|
+
if(c.from_node === this || c.to_node === this) UI.drawObject(c);
|
5426
|
+
}
|
5416
5427
|
}
|
5417
5428
|
|
5418
5429
|
clearHiddenIO() {
|
@@ -6936,7 +6947,7 @@ class Node extends NodeBox {
|
|
6936
6947
|
|
6937
6948
|
setConstraintOffsets() {
|
6938
6949
|
// Sets the offset properties of the constraints that relate to this
|
6939
|
-
// node; these properties are used when drawing these constraints
|
6950
|
+
// node; these properties are used when drawing these constraints.
|
6940
6951
|
const tbc = {top: [], bottom: [], thumb: []};
|
6941
6952
|
for(let k in MODEL.constraints) if(MODEL.constraints.hasOwnProperty(k)) {
|
6942
6953
|
const
|
@@ -6952,21 +6963,21 @@ class Node extends NodeBox {
|
|
6952
6963
|
continue;
|
6953
6964
|
}
|
6954
6965
|
if(vn[0] && vn[1]) {
|
6955
|
-
// Both nodes visible => arrow
|
6966
|
+
// Both nodes visible => arrow.
|
6956
6967
|
if(this.y < q.y - (q.height/2 + 3) - this.height) {
|
6957
|
-
//
|
6968
|
+
// Arrow from bottom of THIS to top of q.
|
6958
6969
|
c.bottom_x = q.x;
|
6959
6970
|
c.bottom_y = q.y;
|
6960
6971
|
tbc.bottom.push(c);
|
6961
6972
|
} else {
|
6962
|
-
//
|
6973
|
+
// Arrow from bottom of THIS to top of q.
|
6963
6974
|
c.top_x = q.x;
|
6964
6975
|
c.top_y = q.y;
|
6965
6976
|
tbc.top.push(c);
|
6966
6977
|
}
|
6967
6978
|
} else {
|
6968
|
-
// One node visible => thumbnail at top of this process
|
6969
|
-
// NOTE: X coordinate not needed for sorting
|
6979
|
+
// One node visible => thumbnail at top of this process.
|
6980
|
+
// NOTE: X coordinate not needed for sorting.
|
6970
6981
|
tbc.thumb.push(c);
|
6971
6982
|
}
|
6972
6983
|
}
|
@@ -7012,7 +7023,8 @@ class Node extends NodeBox {
|
|
7012
7023
|
// Keep 10px between connection points, and for processes
|
7013
7024
|
// also keep this distance from the rectangle corners
|
7014
7025
|
margin = 10,
|
7015
|
-
aw = (this instanceof Process ?
|
7026
|
+
aw = (this instanceof Process ?
|
7027
|
+
(this.collapsed ? 8.5 : this.width / 2) :
|
7016
7028
|
(this.width - this.height) / 2 + margin) - 9 * thl;
|
7017
7029
|
if(tl > 0) {
|
7018
7030
|
tbc.top.sort(tcmp);
|
@@ -7485,11 +7497,12 @@ class Process extends Node {
|
|
7485
7497
|
}
|
7486
7498
|
|
7487
7499
|
attributeExpression(a) {
|
7488
|
-
// Processes have
|
7500
|
+
// Processes have four expression attributes
|
7489
7501
|
if(a === 'LB') return this.lower_bound;
|
7490
7502
|
if(a === 'UB') {
|
7491
7503
|
return (this.equal_bounds ? this.lower_bound : this.upper_bound);
|
7492
7504
|
}
|
7505
|
+
if(a === 'LCF') return this.pace_expression;
|
7493
7506
|
if(a === 'IL') return this.initial_level;
|
7494
7507
|
return null;
|
7495
7508
|
}
|
@@ -483,6 +483,16 @@ function matchingNumber(m, s) {
|
|
483
483
|
return (n == m ? n : false);
|
484
484
|
}
|
485
485
|
|
486
|
+
function matchingNumberInList(ml, s) {
|
487
|
+
// Traverses list `ml` and returns the first matching number, or FALSE
|
488
|
+
// if no match is found.
|
489
|
+
for(let i = 0; i < ml.length; i++) {
|
490
|
+
const n = matchingNumber(ml[i], s);
|
491
|
+
if(n !== false) return n;
|
492
|
+
}
|
493
|
+
return false;
|
494
|
+
}
|
495
|
+
|
486
496
|
function compareWithTailNumbers(s1, s2) {
|
487
497
|
// Returns 0 on equal, an integer < 0 if `s1` comes before `s2`, and
|
488
498
|
// an integer > 0 if `s2` comes before `s1`.
|