-
Notifications
You must be signed in to change notification settings - Fork 3
/
Copy pathHierarchicLayoutDemo.java
477 lines (413 loc) · 18.4 KB
/
HierarchicLayoutDemo.java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
/****************************************************************************
**
** This demo file is part of yFiles for JavaFX 3.6.
**
** Copyright (c) 2000-2023 by yWorks GmbH, Vor dem Kreuzberg 28,
** 72070 Tuebingen, Germany. All rights reserved.
**
** yFiles demo files exhibit yFiles for JavaFX functionalities. Any redistribution
** of demo files in source code or binary form, with or without
** modification, is not permitted.
**
** Owners of a valid software license for a yFiles for JavaFX version that this
** demo is shipped with are allowed to use the demo source code as basis
** for their own yFiles for JavaFX powered applications. Use of such programs is
** governed by the rights and conditions as set out in the yFiles for JavaFX
** license agreement.
**
** THIS SOFTWARE IS PROVIDED ''AS IS'' AND ANY EXPRESS OR IMPLIED
** WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
** MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN
** NO EVENT SHALL yWorks BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
** SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED
** TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
** PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
** LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
** NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
** SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
**
***************************************************************************/
package layout.hierarchiclayout;
import com.yworks.yfiles.geometry.InsetsD;
import com.yworks.yfiles.graph.IBend;
import com.yworks.yfiles.graph.IEdge;
import com.yworks.yfiles.graph.IGraph;
import com.yworks.yfiles.graph.IMapper;
import com.yworks.yfiles.graph.IModelItem;
import com.yworks.yfiles.graph.INode;
import com.yworks.yfiles.graph.LayoutUtilities;
import com.yworks.yfiles.graph.Mapper;
import com.yworks.yfiles.graphml.GraphMLIOHandler;
import com.yworks.yfiles.layout.LayoutExecutor;
import com.yworks.yfiles.layout.PortConstraint;
import com.yworks.yfiles.layout.hierarchic.GivenLayersLayerer;
import com.yworks.yfiles.layout.hierarchic.HierarchicLayout;
import com.yworks.yfiles.layout.hierarchic.HierarchicLayoutData;
import com.yworks.yfiles.layout.hierarchic.LayoutMode;
import com.yworks.yfiles.utils.IEventArgs;
import com.yworks.yfiles.utils.IListEnumerable;
import com.yworks.yfiles.utils.ItemEventArgs;
import com.yworks.yfiles.view.GraphControl;
import com.yworks.yfiles.view.IAnimation;
import com.yworks.yfiles.view.ICanvasObjectDescriptor;
import com.yworks.yfiles.view.ISelectionModel;
import com.yworks.yfiles.view.input.GraphEditorInputMode;
import com.yworks.yfiles.view.input.IHandle;
import com.yworks.yfiles.view.input.IInputMode;
import com.yworks.yfiles.view.input.PopulateItemContextMenuEventArgs;
import javafx.scene.control.ContextMenu;
import javafx.scene.control.MenuItem;
import javafx.scene.web.WebView;
import toolkit.DemoApplication;
import toolkit.DemoStyles;
import toolkit.Themes;
import toolkit.WebViewUtils;
import java.time.Duration;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
/**
* See the usage of {@link com.yworks.yfiles.layout.hierarchic.HierarchicLayout} and how to
* <ul>
* <li>incrementally add nodes and edges</li>
* <li>dynamically assign port constraints</li>
* <li>create new nodes and observe how they are inserted into the drawing near the place they have been created</li>
* <li>create new edges and watch the routings being calculated immediately</li>
* <li>drag the first and last bend of an edge to interactively assign or reset port constraints</li>
* <li>use the popup menu to reroute selected edges or optimize selected nodes locations</li>
* </ul>
*/
public class HierarchicLayoutDemo extends DemoApplication {
public GraphControl graphControl;
public WebView help;
// visualizes the layers and manages layer regions and contains tests
private LayerVisualCreator layerVisualCreator = new LayerVisualCreator();
// holds for each node the layer
private IMapper<INode, Integer> layerMapper = new Mapper<>();
// whether a layout is running
private boolean updateLayout;
// holds temporary layer reassignments that will be assigned during the next layout
private Mapper<INode, Integer> newLayerMapper = new Mapper<>();
// holds for each edge a port constraint for the source end
private Mapper<IEdge, PortConstraint> sourcePortConstraints = new Mapper<>();
// holds for each edge a port constraint for the target end
private Mapper<IEdge, PortConstraint> targetPortConstraints = new Mapper<>();
// holds a list of nodes to insert incrementally during the next layout
private List<INode> incrementalNodes = new ArrayList<>();
// holds a list of edges to reroute incrementally during the next layout
private List<IEdge> incrementalEdges = new ArrayList<>();
/**
* Initializes the controller. This is called when the FXMLLoader instantiates the scene graph.
* At the time this method is called, all nodes in the scene graph are available. Most importantly,
* the GraphControl instance is initialized.
*/
public void initialize() {
// setup the help text on the right side.
WebViewUtils.initHelp(help, this);
// initialize the graph
initializeGraph();
// initialize the input mode
initializeInputModes();
// set a custom prepared GraphMLIOHandler instance to the GraphControl which will be used for default file operations
GraphMLIOHandler handler = new GraphMLIOHandler();
graphControl.setGraphMLIOHandler(handler);
// enable file operations
graphControl.setFileIOEnabled(true);
// configure the GraphMLIOHandler so that a layout automatically runs after loading a graph
handler.addParsedListener((source, args) -> {
if (!this.updateLayout) {
IGraph graph = graphControl.getGraph();
// create an HierarchicLayout instance to provide an initial layout
HierarchicLayout ihl = createLayoutAlgorithm();
// provide additional data to configure the layout algorithm
HierarchicLayoutData ihlData = new HierarchicLayoutData();
// retrieve the layer of each node after the layout run to update the layer visualization
ihlData.setLayerIndices(layerMapper);
// run the layout algorithm
graph.applyLayout(ihl, ihlData);
// update the layer visualization using the collected the layer information
layerVisualCreator.updateLayerBounds(graph, layerMapper);
// fit it nicely into the component
graphControl.fitGraphBounds();
}
});
}
/**
* Called when the stage is shown and the {@link GraphControl} is already resized to its preferred size.
* The graph is moved to the center of the <code>GraphControl</code>.
*/
public void onLoaded() {
graphControl.fitGraphBounds();
}
/**
* Factory method that creates the layout algorithm instances that are used by this demo.
*/
private HierarchicLayout createLayoutAlgorithm() {
HierarchicLayout ihl = new HierarchicLayout();
ihl.setOrthogonalRoutingEnabled(true);
ihl.setRecursiveGroupLayeringEnabled(false);
return ihl;
}
/**
* Calls {@link #createEditorMode()} and registers the result as the {@link
* GraphControl#getInputMode()}.
*/
private void initializeInputModes() {
// create the interaction mode
graphControl.setInputMode(createEditorMode());
// display the layers
graphControl.getBackgroundGroup().addChild(layerVisualCreator, ICanvasObjectDescriptor.ALWAYS_DIRTY_INSTANCE);
}
/**
* Creates the default input mode for the GraphControl, a {@link GraphEditorInputMode}.
* @return a specializes new GraphEditorInputMode instance
*/
private IInputMode createEditorMode() {
GraphEditorInputMode mode = new GraphEditorInputMode();
// register hooks whenever something is dragged or resized
mode.getHandleInputMode().addDragFinishedListener(this::updateLayout);
mode.getMoveInputMode().addDragFinishedListener(this::updateLayout);
// ... and when new nodes are created interactively
mode.addNodeCreatedListener(this::onNodeCreated);
// ... or edges
mode.getCreateEdgeInputMode().addEdgeCreatedListener(this::onEdgeCreated);
// create context menu that allows the user to reroute an edge or relayout a node
mode.addPopulateItemContextMenuListener(this::onPopulateItemContextMenu);
return mode;
}
/**
* Called when an edge has been created interactively.
*/
private void onEdgeCreated(Object source, ItemEventArgs<IEdge> args) {
incrementalEdges.add(args.getItem());
updateLayout(source, args);
}
/**
* Called when the context menu should be populated for a given item.
* @param source the event source
* @param args the event argument instance containing the event data
*/
private void onPopulateItemContextMenu(Object source, PopulateItemContextMenuEventArgs<IModelItem> args) {
// see if it's a node but not a not empty group node
ContextMenu contextMenu = (ContextMenu) args.getMenu();
if (args.getItem() instanceof INode) {
INode node = (INode) args.getItem();
if (graphControl.getGraph().getChildren(node).size() == 0) {
// see if it's already selected
ISelectionModel<INode> selectedNodes = graphControl.getSelection().getSelectedNodes();
if (!selectedNodes.isSelected(node)) {
// no - make it the only selected node
selectedNodes.clear();
}
// make sure the node is selected
selectedNodes.setSelected(node, true);
graphControl.setCurrentItem(node);
// mark all selected nodes for incremental layout
MenuItem item = new MenuItem("Reinsert Incrementally");
item.setOnAction(e -> {
selectedNodes.forEach(incrementalNodes::add);
updateLayout(item, args);
});
contextMenu.getItems().add(item);
args.setHandled(true);
}
}
// if it's an edge...
if (args.getItem() instanceof IEdge) {
IEdge edge = (IEdge) args.getItem();
// update selection state
ISelectionModel<IEdge> selectedEdges = graphControl.getSelection().getSelectedEdges();
if (!selectedEdges.isSelected(edge)) {
selectedEdges.clear();
}
selectedEdges.setSelected(edge, true);
graphControl.setCurrentItem(edge);
// and offer option to reroute selected edges
final MenuItem item = new MenuItem("Reroute");
item.setOnAction(e -> {
selectedEdges.forEach(incrementalEdges::add);
updateLayout(item, args);
});
contextMenu.getItems().add(item);
args.setHandled(true);
}
}
/**
* Called when a node has been created interactively.
*/
private void onNodeCreated(Object source, ItemEventArgs<INode> args) {
int newLayer = layerVisualCreator.getLayer(args.getItem().getLayout().getCenter());
newLayerMapper.setValue(args.getItem(), newLayer);
updateLayout(source, args);
}
/**
* Core method that recalculates and updates the layout.
*/
private void updateLayout(Object source, IEventArgs args) {
// make sure we are not re-entrant
if (updateLayout) {
return;
}
updateLayout = true;
// update the layers for moved nodes
updateMovedNodes();
// create and configure the HierarchicLayout
HierarchicLayout ihl = createLayoutAlgorithm();
// rearrange only the incremental graph elements, the
// remaining elements are not, or only slightly, changed
ihl.setLayoutMode(LayoutMode.INCREMENTAL);
// use the GivenLayersLayerer for all non-incremental nodes
ihl.setFixedElementsLayerer(new GivenLayersLayerer());
// provide additional data to configure the HierarchicLayout
HierarchicLayoutData ihlData = new HierarchicLayoutData();
// specify the layer of each non-incremental node
ihlData.setGivenLayersLayererIds(layerMapper);
// retrieve the layer of each incremental node after the layout run to update the layer visualization
ihlData.setLayerIndices(layerMapper);
// specify port constrains for the source of each edge
ihlData.setSourcePortConstraints(sourcePortConstraints);
// specify port constrains for the target of each edge
ihlData.setTargetPortConstraints(targetPortConstraints);
// specify the nodes to rearrange
ihlData.getIncrementalHints().setIncrementalLayeringNodes(new ArrayList<>(incrementalNodes));
// specify the edges to rearrange
ihlData.getIncrementalHints().setIncrementalSequencingItems(new ArrayList<>(incrementalEdges));
// forget the nodes and edges for the next run
incrementalNodes.clear();
incrementalEdges.clear();
// now layout the graph using the HierarchicLayout and animate the result
LayoutExecutor executor = new LayoutExecutor(graphControl, graphControl.getGraph(), ihl) {
@Override
protected IAnimation createMorphAnimation() {
// use a customized morph animation that also morphs the layer visualization
IAnimation layoutAnimation = LayoutUtilities.createLayoutAnimation(this.getGraph(), this.getLayoutGraph(), this.getDuration());
IAnimation updateLayerAnimation = new IAnimation() {
@Override
public void initialize() {}
@Override
public void animate(final double time) {
layerVisualCreator.updateLayerBounds(graphControl.getGraph(), layerMapper);
}
@Override
public void cleanUp() {}
@Override
public Duration getPreferredDuration() {
return Duration.ofMillis(1);
}
};
return IAnimation.createParallelAnimation(layoutAnimation, updateLayerAnimation);
}
};
executor.setDuration(Duration.ofSeconds(1));
executor.setViewportAnimationEnabled(true);
executor.setEasedAnimationEnabled(true);
executor.setTargetBoundsInsets(new InsetsD(20));
executor.setContentRectUpdatingEnabled(true);
executor.addLayoutFinishedListener((source1, args1) -> updateLayout = false);
executor.setLayoutData(ihlData);
executor.start();
}
/**
* Updates the layers for moved nodes.
*/
private void updateMovedNodes() {
if (newLayerMapper.getEntries().iterator().hasNext()) {
// spread out existing layers
for (INode node : graphControl.getGraph().getNodes()) {
int layer = layerMapper.getValue(node) != null ? layerMapper.getValue(node) : 0;
layerMapper.setValue(node, layer * 2);
}
for (Map.Entry<INode, Integer> pair : newLayerMapper.getEntries()) {
INode node = pair.getKey();
// if a node has been moved, reinsert the adjacent edges incrementally and not from sketch
graphControl.getGraph().edgesAt(node).forEach(incrementalEdges::add);
int newLayerIndex = pair.getValue();
if (newLayerIndex == Integer.MAX_VALUE) {
continue;
}
if (newLayerIndex < 0) {
int beforeLayer = -(newLayerIndex + 1);
layerMapper.setValue(node, beforeLayer * 2 - 1);
} else {
layerMapper.setValue(node, newLayerIndex * 2);
}
}
newLayerMapper.clear();
}
}
/**
* Initializes the graph instance setting default styles and creating a small sample graph.
*/
private void initializeGraph() {
IGraph graph = graphControl.getGraph();
// set some nice defaults
DemoStyles.initDemoStyles(graph, Themes.PALETTE21);
// register a custom PositionHandler for the nodes.
// this enables interactive layer reassignment with layer preview
graphControl.getGraph().getDecorator().getNodeDecorator().getPositionHandlerDecorator().setImplementationWrapper(
node -> graph.getChildren(node).size() == 0,
(node, positionHandler) -> new LayerPositionHandler(layerVisualCreator, node, positionHandler, newLayerMapper)
);
// register custom handles for the first and last bends of an edge
// this enables interactive port constraint assignment.
graphControl.getGraph().getDecorator().getBendDecorator().getHandleDecorator().setImplementationWrapper(
bend -> {
IListEnumerable<IBend> bends = bend.getOwner().getBends();
return bends.getItem(0) == bend || bends.getItem(bends.size() - 1) == bend;
},
this::createBendHandle
);
// create a small sample graph with given layers
createSampleGraph(graph);
}
/**
* Creates the small sample graph, calculates a layout and updates the layer visualization.
*/
private void createSampleGraph(IGraph graph) {
INode n1 = graph.createNode();
INode n2 = graph.createNode();
INode n3 = graph.createNode();
INode n4 = graph.createNode();
graph.createEdge(n1, n2);
graph.createEdge(n2, n3);
graph.createEdge(n1, n4);
// assign each node to a layer
layerMapper.setValue(n1, 0);
layerMapper.setValue(n2, 1);
layerMapper.setValue(n3, 2);
layerMapper.setValue(n4, 2);
// create an HierarchicLayout instance to provide an initial layout
HierarchicLayout ihl = createLayoutAlgorithm();
// use the GivenLayersLayerer to respect the above node to layer assignment
ihl.setFromScratchLayerer(new GivenLayersLayerer());
// provide additional data to configure the layout algorithm
HierarchicLayoutData ihlData = new HierarchicLayoutData();
// respect the above node to layer assignment
ihlData.setGivenLayersLayererIds(layerMapper);
// run the layout algorithm
graph.applyLayout(ihl, ihlData);
// and update the layer visualization
layerVisualCreator.updateLayerBounds(graph, layerMapper);
}
/**
* Callback that creates the bend IHandle for the first and last bends.
* @param bend the bend
* @param originalImplementation the original implementation to delegate to
* @return the new handle that allows for interactively assign the port constraints
*/
private IHandle createBendHandle(IBend bend, IHandle originalImplementation) {
IListEnumerable<IBend> bends = bend.getOwner().getBends();
if (bends.getItem(0) == bend) {
// decorate first bend
originalImplementation = new PortConstraintBendHandle(true, bend, originalImplementation, sourcePortConstraints);
}
if (bends.getItem(bends.size() - 1) == bend) {
// decorate last bend - could be both first and last
originalImplementation = new PortConstraintBendHandle(false, bend, originalImplementation, targetPortConstraints);
}
return originalImplementation;
}
public static void main(String[] args) {
launch(args);
}
}