-
Notifications
You must be signed in to change notification settings - Fork 3
/
Copy pathInteractiveOrganicLayoutDemo.java
403 lines (362 loc) · 15.8 KB
/
InteractiveOrganicLayoutDemo.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
/****************************************************************************
**
** 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.interactiveorganic;
import com.yworks.yfiles.algorithms.GraphConnectivity;
import com.yworks.yfiles.algorithms.INodeMap;
import com.yworks.yfiles.algorithms.Node;
import com.yworks.yfiles.geometry.IPoint;
import com.yworks.yfiles.geometry.PointD;
import com.yworks.yfiles.graph.GraphItemTypes;
import com.yworks.yfiles.graph.IGraph;
import com.yworks.yfiles.graph.INode;
import com.yworks.yfiles.graphml.GraphMLIOHandler;
import com.yworks.yfiles.layout.CopiedLayoutGraph;
import com.yworks.yfiles.layout.LayoutGraphAdapter;
import com.yworks.yfiles.layout.organic.InteractiveOrganicLayout;
import com.yworks.yfiles.utils.FlagsEnum;
import com.yworks.yfiles.utils.IEventArgs;
import com.yworks.yfiles.view.CanvasControl;
import com.yworks.yfiles.view.GraphControl;
import com.yworks.yfiles.view.Animator;
import com.yworks.yfiles.view.IAnimationCallback;
import com.yworks.yfiles.view.input.GraphEditorInputMode;
import com.yworks.yfiles.view.input.IInputMode;
import com.yworks.yfiles.view.input.IInputModeContext;
import com.yworks.yfiles.view.input.IPositionHandler;
import javafx.application.Platform;
import javafx.scene.web.WebView;
import toolkit.DemoApplication;
import toolkit.DemoStyles;
import toolkit.Themes;
import toolkit.WebViewUtils;
import java.io.IOException;
import java.time.Duration;
import java.util.HashSet;
import java.util.Set;
import java.util.Timer;
import java.util.TimerTask;
/**
* Sample application that demonstrates the usage of {@link com.yworks.yfiles.layout.organic.InteractiveOrganicLayout}.
*/
public class InteractiveOrganicLayoutDemo extends DemoApplication {
public GraphControl graphControl;
public WebView help;
private InteractiveOrganicLayout layout;
private CopiedLayoutGraph copiedLayoutIGraph;
/**
* 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 input mode
initializeInputModes();
}
/**
* Calls {@link #createEditorMode()} and registers the result with
* {@link CanvasControl#setInputMode(IInputMode)}.
*/
private void initializeInputModes() {
graphControl.setInputMode(createEditorMode());
}
/**
* Creates the default input mode for the GraphControl, a {@link GraphEditorInputMode}.
* @return a new GraphEditorInputMode instance
*/
private IInputMode createEditorMode() {
GraphEditorInputMode mode = new GraphEditorInputMode();
mode.getCreateBendInputMode().setEnabled(false);
mode.setSelectableItems(FlagsEnum.or(GraphItemTypes.NODE, GraphItemTypes.EDGE));
mode.setMarqueeSelectableItems(GraphItemTypes.NODE);
mode.setClickSelectableItems(FlagsEnum.or(GraphItemTypes.NODE, GraphItemTypes.EDGE));
mode.setClickableItems(GraphItemTypes.NODE);
mode.setShowHandleItems(GraphItemTypes.NONE);
mode.getCreateEdgeInputMode().setCreateBendAllowed(false);
// wrap the position handler used for the MoveInputMode to update the layout
// when graph elements have been moved interactively
mode.getMoveInputMode().setPositionHandler(new MyPositionHandler(mode.getMoveInputMode().getPositionHandler()));
return mode;
}
/**
* Called when the stage is shown and the {@link GraphControl} is already resized to its preferred size.
*/
public void onLoaded() {
// initialize the graph
initializeGraph();
}
/**
* Initializes the graph instance setting default styles and loads a sample graph.
*/
private void initializeGraph() {
IGraph graph = graphControl.getGraph();
DemoStyles.initDemoStyles(graph);
graph.getEdgeDefaults().setStyle(DemoStyles.createDemoEdgeStyle(Themes.PALETTE_ORANGE, false));
// load a sample graph
try {
new GraphMLIOHandler().read(graph, getClass().getResource("resources/sample.graphml"));
} catch (IOException e) {
e.printStackTrace();
}
// set some defaults
graph.getNodes().stream().findFirst().ifPresent(node -> {
graph.getNodeDefaults().setStyle(node.getStyle());
graph.getNodeDefaults().setStyleInstanceSharingEnabled(true);
});
// center the initial graph
graphControl.fitGraphBounds();
// create a copy of the graph for the layout algorithm
copiedLayoutIGraph = new LayoutGraphAdapter(graph).createCopiedLayoutGraph();
// create and start the layout algorithm. It runs in a thread and
// can update the current layout with its wake-up method.
layout = startLayout();
wakeUp();
// register a listeners so that structure updates between the
// graph layout and the graph are handled automatically
graph.addNodeCreatedListener((source, evt) -> {
if (layout != null) {
PointD center = evt.getItem().getLayout().getCenter();
layout.syncStructure(true);
// we nail down all newly created nodes
Node copiedNode = copiedLayoutIGraph.getCopiedNode(evt.getItem());
layout.setCenter(copiedNode, center.getX(), center.getY());
layout.setInertia(copiedNode, 1);
layout.setStress(copiedNode, 0);
}
});
graph.addNodeRemovedListener(this::synchronize);
graph.addEdgeCreatedListener(this::synchronize);
graph.addEdgeRemovedListener(this::synchronize);
}
/**
* Creates a new layout algorithm instance and starts it in a new thread.
*/
private InteractiveOrganicLayout startLayout() {
// create the layout algorithm
InteractiveOrganicLayout organicLayout = new InteractiveOrganicLayout();
organicLayout.setMaximumDuration(2000);
// use an animator that animates an infinite animation. This means that all
// changes to the layout are animated, as long as the demo is running.
Animator animator = new Animator(graphControl);
animator.setAutoUpdateEnabled(false);
animator.setUserInteractionAllowed(true);
animator.animate(new IAnimationCallback() {
private boolean hasLayoutStarted;
@Override
public void animate(double time) {
// wait until the layout algorithm has been started
if (!hasLayoutStarted){
if (organicLayout.isRunning()){
hasLayoutStarted = true;
}
return;
}
// now the layout algorithm has been started and we check if it is still running.
// If not we destroy the animator otherwise we apply the layout to the graph
if (!organicLayout.isRunning()) {
animator.stop();
return;
}
if (organicLayout.commitPositionsSmoothly(50, 0.05) > 0) {
graphControl.invalidate();
}
}
}, Duration.ofDays(100));
// run the layout algorithm in a separate thread. If we want a recalculation of the layout,
// we need to call the wake-up method of the InteractiveOrganicLayout
Thread thread = new Thread(() -> {
organicLayout.applyLayout(copiedLayoutIGraph);
// stop the animator when the layout returns (does not normally happen at all)
Platform.runLater(animator::stop);
});
thread.setDaemon(true);
thread.start();
return organicLayout;
}
/**
* Wakes the layout algorithm up to calculate an initial layout.
*/
private void wakeUp() {
if (layout != null) {
// we make all nodes freely movable
copiedLayoutIGraph.getNodes().forEach(node -> layout.setInertia(node, 0));
// then wake up the layout algorithm
layout.wakeUp();
// and after two second we freeze the nodes again...
Timer timer = new Timer();
timer.schedule(new TimerTask() {
@Override
public void run() {
copiedLayoutIGraph.getNodes().forEach(node -> layout.setInertia(node, 1));
timer.cancel();
}
}, 2000);
}
}
/**
* EventHandler method that synchronizes the structure with the layout
* algorithm if the graph structure has been changed interactively.
*/
private void synchronize(Object sender, IEventArgs args) {
if (layout != null) {
layout.syncStructure(true);
}
}
public static void main(String[] args) {
launch(args);
}
/**
* Custom position handler that automatically triggers layout updates when graph elements have been moved
* interactively.
*/
private class MyPositionHandler implements IPositionHandler {
private IPositionHandler originalHandler;
MyPositionHandler(IPositionHandler originalHandler) {
this.originalHandler = originalHandler;
}
@Override
public IPoint getLocation() {
return originalHandler.getLocation();
}
@Override
public void initializeDrag(IInputModeContext inputModeContext) {
InteractiveOrganicLayout layout = InteractiveOrganicLayoutDemo.this.layout;
if (layout != null) {
CopiedLayoutGraph copy = copiedLayoutIGraph;
INodeMap componentNumber = copy.createNodeMap();
GraphConnectivity.connectedComponents(copy, componentNumber);
Set<Integer> movedComponents = new HashSet<>();
Set<Node> selectedNodes = new HashSet<>();
for (INode node : graphControl.getSelection().getSelectedNodes()) {
Node copiedNode = copy.getCopiedNode(node);
if (copiedNode != null) {
// remember that we nailed down this node
selectedNodes.add(copiedNode);
// remember that we are moving this component
movedComponents.add(componentNumber.getInt(copiedNode));
// update the position of the node in the CopiedLayoutGraph to match the one in the IGraph
layout.setCenter(copiedNode, node.getLayout().getCenter().getX(), node.getLayout().getCenter().getY());
// actually, the node itself is fixed at the start of a drag gesture
layout.setInertia(copiedNode, 1.0);
// increasing the heat has the effect that the layout algorithm will consider this node as not completely placed...
// In this case, the node itself is fixed, but its neighbors will wake up
increaseHeat(copiedNode, layout, 0.5);
}
}
// there are components that won't be moved - nail the nodes down so that they don't spread apart infinitely
for (Node copiedNode : copy.getNodes()) {
if (!movedComponents.contains(componentNumber.getInt(copiedNode))) {
layout.setInertia(copiedNode, 1);
} else {
if (!selectedNodes.contains(copiedNode)) {
// make it float freely
layout.setInertia(copiedNode, 0);
}
}
}
// dispose the map
copy.disposeNodeMap(componentNumber);
// notify the layout algorithm that there is new work to do...
layout.wakeUp();
}
originalHandler.initializeDrag(inputModeContext);
}
@Override
public void handleMove(IInputModeContext inputModeContext, PointD originalLocation, PointD newLocation) {
originalHandler.handleMove(inputModeContext, originalLocation, newLocation);
InteractiveOrganicLayout layout = InteractiveOrganicLayoutDemo.this.layout;
if (layout != null) {
CopiedLayoutGraph copy = copiedLayoutIGraph;
for (INode node : graphControl.getSelection().getSelectedNodes()) {
Node copiedNode = copy.getCopiedNode(node);
if (copiedNode != null) {
// update the position of the node in the CopiedLayoutGraph to match the one in the IGraph
layout.setCenter(copiedNode, node.getLayout().getCenter().getX(), node.getLayout().getCenter().getY());
// increasing the heat has the effect that the layout algorithm will consider these nodes as not completely placed...
increaseHeat(copiedNode, layout, 0.05);
}
}
// notify the layout algorithm that there is new work to do...
layout.wakeUp();
}
}
private void increaseHeat(Node copiedNode, InteractiveOrganicLayout layout, double delta) {
// increase heat of neighbors
for (Node neighbor : copiedNode.getNeighbors()) {
double oldStress = layout.getStress(neighbor);
layout.setStress(neighbor, Math.min(1, oldStress + delta));
}
}
@Override
public void cancelDrag(IInputModeContext inputModeContext, PointD originalLocation) {
originalHandler.cancelDrag(inputModeContext, originalLocation);
InteractiveOrganicLayout layout = InteractiveOrganicLayoutDemo.this.layout;
if (layout != null) {
CopiedLayoutGraph copy = copiedLayoutIGraph;
for (INode node : graphControl.getSelection().getSelectedNodes()) {
Node copiedNode = copy.getCopiedNode(node);
if (copiedNode != null) {
// update the position of the node in the CLG to match the one in the IGraph
layout.setCenter(copiedNode, node.getLayout().getCenter().getX(), node.getLayout().getCenter().getY());
layout.setStress(copiedNode, 0);
}
}
for (Node copiedNode : copy.getNodes()) {
// reset the node's inertia to be fixed
layout.setInertia(copiedNode, 1.0);
layout.setStress(copiedNode, 0);
}
// we don't want to restart the layout (since we canceled the drag anyway...)
}
}
@Override
public void dragFinished(IInputModeContext inputModeContext, PointD originalLocation, PointD newLocation) {
originalHandler.dragFinished(inputModeContext, originalLocation, newLocation);
InteractiveOrganicLayout layout = InteractiveOrganicLayoutDemo.this.layout;
if (layout != null) {
CopiedLayoutGraph copy = copiedLayoutIGraph;
for (INode node : graphControl.getSelection().getSelectedNodes()) {
Node copiedNode = copy.getCopiedNode(node);
if (copiedNode != null) {
// update the position of the node in the CLG to match the one in the IGraph
layout.setCenter(copiedNode, node.getLayout().getCenter().getX(), node.getLayout().getCenter().getY());
layout.setStress(copiedNode, 0);
}
}
for (Node copiedNode : copy.getNodes()) {
// reset the node's inertia to be fixed
layout.setInertia(copiedNode, 1.0);
layout.setStress(copiedNode, 0);
}
}
}
}
}