1   /****************************************************************************
2    * This demo file is part of yFiles for Java 2.14.
3    * Copyright (c) 2000-2017 by yWorks GmbH, Vor dem Kreuzberg 28,
4    * 72070 Tuebingen, Germany. All rights reserved.
5    * 
6    * yFiles demo files exhibit yFiles for Java functionalities. Any redistribution
7    * of demo files in source code or binary form, with or without
8    * modification, is not permitted.
9    * 
10   * Owners of a valid software license for a yFiles for Java version that this
11   * demo is shipped with are allowed to use the demo source code as basis
12   * for their own yFiles for Java powered applications. Use of such programs is
13   * governed by the rights and conditions as set out in the yFiles for Java
14   * license agreement.
15   * 
16   * THIS SOFTWARE IS PROVIDED ''AS IS'' AND ANY EXPRESS OR IMPLIED
17   * WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
18   * MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN
19   * NO EVENT SHALL yWorks BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
20   * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED
21   * TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
22   * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
23   * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
24   * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
25   * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
26   *
27   ***************************************************************************/
28  package demo.layout;
29  
30  import demo.view.DemoBase;
31  import y.base.Edge;
32  import y.base.EdgeCursor;
33  import y.view.DefaultGraph2DRenderer;
34  import y.layout.CurveFittingLayoutStage;
35  import y.layout.EdgeBundleDescriptor;
36  import y.layout.EdgeBundling;
37  import y.layout.Layouter;
38  import y.layout.circular.CircularLayouter;
39  import y.layout.radial.RadialLayouter;
40  import y.layout.router.OrganicEdgeRouter;
41  import y.layout.tree.BalloonLayouter;
42  import y.layout.tree.TreeReductionStage;
43  import y.view.Arrow;
44  import y.view.BezierPathCalculator;
45  import y.view.EdgeRealizer;
46  import y.view.EditMode;
47  import y.view.GenericEdgeRealizer;
48  import y.view.Graph2D;
49  import y.view.Graph2DLayoutExecutor;
50  import y.view.PolyLineEdgeRealizer;
51  
52  import java.awt.Dimension;
53  import java.awt.EventQueue;
54  import java.awt.FlowLayout;
55  import java.awt.event.ActionEvent;
56  import java.awt.event.ActionListener;
57  import java.awt.event.ItemEvent;
58  import java.awt.event.ItemListener;
59  import java.net.URL;
60  import java.util.Locale;
61  import java.util.Map;
62  import javax.swing.AbstractAction;
63  import javax.swing.JCheckBox;
64  import javax.swing.JComboBox;
65  import javax.swing.JLabel;
66  import javax.swing.JSlider;
67  import javax.swing.JTextField;
68  import javax.swing.JToolBar;
69  import javax.swing.SwingConstants;
70  import javax.swing.border.EtchedBorder;
71  import javax.swing.event.ChangeEvent;
72  import javax.swing.event.ChangeListener;
73  
74  /**
75   * This demo shows possible usages of {@link CurveFittingLayoutStage} that fits piecewise <em>cubic bezier curves</em>
76   * to given arbitrary edge paths.
77   * <p>
78   *   In this demo, layout/routing algorithms that can produce curved edge paths (i.e., polyline paths, bundles or arcs) 
79   *   are applied. The {@link CurveFittingLayoutStage} is enabled by the corresponding check-box while the fitting error
80   *   can be selected using the slider in the tool bar. Small error values provide better approximations of the edge
81   *   paths, but may result in a large number of bends. 
82   * </p>
83   * <p>
84   *   To visualize the paths produced after applying the {@link CurveFittingLayoutStage}, a specific bezier edge realizer is 
85   *   required to correctly translate the control and curve points of the edges. After applying the stage,
86   *   each four consecutive points of an edge path form a bezier curve. The first and last of the four points
87   *   represent the start and end of the curve and the second and third point are the control points defining
88   *   how the curve looks. The second and third point do not necessarily lie on the actual curve.
89   * </p>
90   *
91   */
92  public class CurveFittingLayoutStageDemo extends DemoBase {
93  
94    private static final byte STYLE_BALLOON = 1;
95    private static final byte STYLE_CIRCULAR = 2;
96    private static final byte STYLE_RADIAL = 3;
97    private static final byte STYLE_ORGANIC_ROUTER = 4;
98    
99    private static final double NODE_WIDTH = 50;
100   private static final double NODE_HEIGHT = 25;
101 
102   private static final String BEZIER_CONFIG = "BEZIER_CONFIGURATION";
103 
104   private JSlider errorSlider;
105   private boolean isCurveFitted = false;
106 
107   private byte style = STYLE_BALLOON;
108 
109   public CurveFittingLayoutStageDemo(){
110     this(null);
111   }
112   
113   public CurveFittingLayoutStageDemo( final String helpFilePath ) {
114     // load and layout the sample graph 
115     loadGraph("resource/curve-fitting-sample.graphml");
116     addHelpPane(helpFilePath);
117   }
118  
119   protected void loadGraph(URL resource) {
120     super.loadGraph(resource);
121   }
122 
123   protected JToolBar createToolBar() {
124     final JToolBar toolBar = super.createToolBar();
125     
126     final int height = 20;
127     final int separatorWidth = 10;
128 
129     toolBar.setLayout(new FlowLayout(FlowLayout.LEFT));
130     toolBar.addSeparator(new Dimension(separatorWidth, height));
131 
132     toolBar.add(createActionControl(new LayoutAction()));
133 
134     final Graph2D graph = view.getGraph2D();
135 
136     final JComboBox comboBox = new JComboBox(new Object[]{"Balloon Layout", "Circular Layout", "Radial Layout", "Organic Edge Router"});
137     comboBox.addActionListener(new ActionListener() {
138       public void actionPerformed( ActionEvent e ) {
139         switch (comboBox.getSelectedIndex()) {
140           case 0:
141             style = STYLE_BALLOON;
142             break;
143           case 1:
144             style = STYLE_CIRCULAR;
145             break;
146           case 2:
147             style = STYLE_RADIAL;
148             break;
149           case 3:
150             style = STYLE_ORGANIC_ROUTER;
151             break;
152           default:
153             style = STYLE_BALLOON;
154         }
155         // Apply the selected layout
156         layout(graph, true);
157       }
158     });
159     toolBar.add(comboBox);
160     
161     toolBar.addSeparator(new Dimension(separatorWidth, height));
162 
163     final JTextField errorTextField = new JTextField();
164 
165     final JLabel fittingErrorLabel = new JLabel("Error:", getIconResource("resource/properties.png"), SwingConstants.LEFT);
166     
167     final JCheckBox fitCurve = new JCheckBox("Apply Bezier Fitting");
168     fitCurve.addItemListener(new ItemListener() {
169       public void itemStateChanged( final ItemEvent e ) {
170         isCurveFitted = e.getStateChange() == ItemEvent.SELECTED;
171         layout(graph, true);
172         // enable/disable slider if fitting is selected/deselected
173         errorSlider.setEnabled(isCurveFitted);
174         errorTextField.setEnabled(isCurveFitted);
175         fittingErrorLabel.setEnabled(isCurveFitted);
176       }
177     });
178 
179     toolBar.add(fitCurve);
180     toolBar.add(fittingErrorLabel);
181     
182     errorSlider = new JSlider(0, 50, 2);
183     errorSlider.addChangeListener(new ChangeListener() {
184       public void stateChanged( final ChangeEvent e ) {
185         errorTextField.setText(String.valueOf(errorSlider.getValue()));
186         layout(graph, false);
187       }
188     });
189     errorSlider.setEnabled(isCurveFitted);
190     errorTextField.setEnabled(isCurveFitted);
191     fittingErrorLabel.setEnabled(isCurveFitted);
192     
193     toolBar.add(errorSlider);
194 
195     errorTextField.setText(String.valueOf(errorSlider.getValue()));
196     errorTextField.setHorizontalAlignment(SwingConstants.CENTER);
197     errorTextField.setPreferredSize(new Dimension(height, height));
198     errorTextField.setBorder(new EtchedBorder(EtchedBorder.LOWERED));
199     errorTextField.setEditable(false);
200 
201     toolBar.add(errorTextField);
202     
203     return toolBar;
204   }
205 
206   protected EditMode createEditMode() {
207     EditMode editMode = super.createEditMode();
208     // do not allow new bends to the edges
209     editMode.allowBendCreation(false);
210     // do not assign labels automatically
211     editMode.assignNodeLabel(false);
212     return editMode;
213   }
214 
215   protected void initialize() {
216     super.initialize();
217     // Enables the drawing of nodes over edges
218     final DefaultGraph2DRenderer graph2DRenderer = (DefaultGraph2DRenderer) view.getGraph2DRenderer();
219     graph2DRenderer.setDrawEdgesFirst(true);
220     // Draws the selected elements on top
221     graph2DRenderer.setDrawSelectionOnTop(true);
222   }
223 
224   protected void configureDefaultRealizers() {
225     super.configureDefaultRealizers();
226     // set the size of the default node realizers
227     view.getGraph2D().getDefaultNodeRealizer().setSize(NODE_WIDTH, NODE_HEIGHT);
228 
229     view.getGraph2D().getDefaultEdgeRealizer().setTargetArrow(Arrow.NONE);
230     
231     // Register a realizer configuration using BezierPathCalculator that draws piecewise bezier curves
232     final GenericEdgeRealizer.Factory f = GenericEdgeRealizer.getFactory();
233     final Map impls = f.createDefaultConfigurationMap();
234     impls.put(GenericEdgeRealizer.PathCalculator.class, new BezierPathCalculator(false));
235     f.addConfiguration(BEZIER_CONFIG, impls);
236   }
237   
238   /**
239    * Updates all edge realizers when needed to switch between a polyline and bezier realizer and vice-versa
240    */
241   private void updateEdgeRealizers(Graph2D graph2D, EdgeCursor ec, boolean isBezier) {
242     for (; ec.ok(); ec.next()) {
243       final Edge edge = ec.edge();
244       final EdgeRealizer oldRealizer = graph2D.getRealizer(edge);
245       EdgeRealizer newRealizer;
246       if (isBezier) {
247         // Set the configuration of the realizer to the previously registered Bezier configuration
248         GenericEdgeRealizer ger = new GenericEdgeRealizer(oldRealizer);
249         ger.setConfiguration(BEZIER_CONFIG);
250         newRealizer = ger;
251       } else {
252         PolyLineEdgeRealizer per = new PolyLineEdgeRealizer(oldRealizer);
253         per.setSmoothedBends(true);
254         newRealizer = per;
255       }
256       graph2D.setRealizer(edge, newRealizer);
257     }
258     view.updateView();
259   }
260 
261   /**
262    * Layout the graph according to the selected layout/routing algorithm.
263    *
264    * @param graph the input graph which will be laid out
265    */
266   private void layout(Graph2D graph, boolean morphAndFit) {
267     final CurveFittingLayoutStage curveFittingLayoutStage = new CurveFittingLayoutStage();
268     curveFittingLayoutStage.setMaximumError(errorSlider.getValue());
269     
270     switch (style) {
271       case STYLE_BALLOON:
272         final BalloonLayouter balloon = new BalloonLayouter();
273         // Applies TreeReductionStage so that edge bundling can be applied.
274         final TreeReductionStage trs = new TreeReductionStage();
275 
276         enableBundling(trs.getEdgeBundling());
277         
278         if (isCurveFitted) {
279           // Appends the CurveFittingLayoutStage
280           balloon.appendStage(curveFittingLayoutStage);
281         }
282         // Appends the TreeReductionStage 
283         balloon.appendStage(trs);
284         
285         runLayouter(balloon, graph, morphAndFit);
286         break;
287       case STYLE_CIRCULAR:
288         final CircularLayouter circular = new CircularLayouter();
289         enableBundling(circular.getEdgeBundling());
290         
291         if (isCurveFitted) {
292           // Appends the CurveFittingLayoutStage
293           circular.appendStage(curveFittingLayoutStage);
294         }
295         runLayouter(circular, graph, morphAndFit);
296         break;
297       case STYLE_RADIAL:
298         final RadialLayouter radial = new RadialLayouter();
299         radial.setEdgeRoutingStrategy(RadialLayouter.EDGE_ROUTING_STRATEGY_ARC);
300 
301         if (isCurveFitted) {
302           // Appends the CurveFittingLayoutStage
303           radial.appendStage(curveFittingLayoutStage);
304         }
305         runLayouter(radial, graph, morphAndFit);
306         break;
307       case STYLE_ORGANIC_ROUTER:
308         final OrganicEdgeRouter organicEdgeRouter = new OrganicEdgeRouter();
309 
310         // Runs the organic edge router that routes only the edges while not affecting the node positions.
311         // The routing is performed based on the locations of the nodes produced by the last applied layout algorithm.
312         runLayouter(organicEdgeRouter, graph, morphAndFit);
313         
314         if (isCurveFitted){
315           // Run the fitting stage
316           runLayouter(curveFittingLayoutStage, graph, morphAndFit);
317         }
318         break;
319     }
320     // Updating the realizers is necessary because the edges fitted by CurveFittingLayoutStage will only be
321     // correctly visualized when using an appropriate realizer which draws cubic bezier curves.
322     updateEdgeRealizers(graph, graph.edges(), isCurveFitted);
323   }
324 
325   /**
326    * Runs a {@link Layouter} using a {@link Graph2DLayoutExecutor} on the given graph.
327    */
328   private void runLayouter(Layouter layouter, Graph2D graph, boolean morphAndFit) {
329     final Graph2DLayoutExecutor layoutExecutor = new Graph2DLayoutExecutor();
330     if (morphAndFit) {
331       layoutExecutor.doLayout(view, layouter);
332     } else {
333       layoutExecutor.doLayout(graph, layouter);
334     }
335   }
336   
337   // Sets the edge bundling
338   private void enableBundling( final EdgeBundling ebl ) {
339     final EdgeBundleDescriptor descriptor = new EdgeBundleDescriptor();
340     descriptor.setBundled(true);
341     ebl.setDefaultBundleDescriptor(descriptor);
342     ebl.setBundlingStrength(0.99);
343   }
344 
345   /**
346    * Launches the selected layout.
347    */
348   class LayoutAction extends AbstractAction {
349     LayoutAction() {
350       super("Layout", SHARED_LAYOUT_ICON);
351     }
352 
353     public void actionPerformed(ActionEvent e ) {
354       layout(view.getGraph2D(), true);
355     }
356   }
357 
358   public static void main(String[] args) {
359     EventQueue.invokeLater(new Runnable() {
360       public void run() {
361         Locale.setDefault(Locale.ENGLISH);
362         initLnF();
363         (new CurveFittingLayoutStageDemo("resource/curveFittingDemohelp.html")).start();
364       }
365     });
366   }
367 }
368