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.radial;
29  
30  import y.base.Node;
31  import y.base.NodeCursor;
32  import y.base.NodeMap;
33  import y.geom.YPoint;
34  import y.layout.radial.RadialLayouter;
35  import y.view.Drawable;
36  import y.view.Graph2D;
37  import y.view.Graph2DView;
38  
39  import java.awt.BasicStroke;
40  import java.awt.Color;
41  import java.awt.Graphics2D;
42  import java.awt.Rectangle;
43  import java.awt.Stroke;
44  import java.awt.geom.Arc2D;
45  import java.awt.geom.Ellipse2D;
46  import java.util.ArrayList;
47  import java.util.Arrays;
48  import java.util.Collections;
49  import java.util.Comparator;
50  import java.util.HashSet;
51  import java.util.Iterator;
52  
53  /**
54   * Drawable to visualize the circles and sectors nodes are placed on/in by the RadialLayouter.
55   * The necessary render information are provided by RadialLayouter.NodeInfo objects filled by the RadialLayouter.
56   *
57   */
58  public final class SectorDrawable implements Drawable {
59  
60    private final Graph2DView view;
61    private final NodeMap nodeInfoMap;
62    private Rectangle bounds;
63  
64    private final Stroke stroke;
65    private Color[][] colors;
66    private boolean drawingCircles;
67    private boolean drawingSectors;
68  
69    private double[] radii;
70    private YPoint center;
71    private ArrayList nodeInfos;
72  
73    /**
74     * Creates a new drawable for circles and sectors.
75     *
76     * @param view The view containing the {@link Graph2D}.
77     * @param nodeInfoMap A node map containing {@link RadialLayouter.NodeInfo} objects for the nodes.
78     */
79    public SectorDrawable(final Graph2DView view, final NodeMap nodeInfoMap
80    ) {
81      this.view = view;
82      this.nodeInfoMap = nodeInfoMap;
83      this.bounds = new Rectangle(0, 0, 0, 0);
84  
85      this.drawingCircles = true;
86      this.drawingSectors = false;
87  
88      this.stroke = new BasicStroke(1.25f);
89      this.colors = new Color[][]{
90              {new Color(180, 200, 255), new Color(120, 150, 220)},
91              {new Color(180, 255, 200), new Color(120, 220, 150)}};
92    }
93  
94    /**
95     * Returns whether the circles shall be drawn.
96     * <p>
97     * Default is <code>true</code>.
98     * </p>
99     *
100    * @return <code>true</code>, iff the circles shall be drawn.
101    */
102   public boolean isDrawingCircles() {
103     return drawingCircles;
104   }
105 
106   /**
107    * Sets whether the circles shall be drawn.
108    *
109    * @return <code>true</code>, iff the circles shall be drawn.
110    */
111   public void setDrawingCircles(boolean drawingCircles) {
112     this.drawingCircles = drawingCircles;
113   }
114 
115   /**
116    * Returns whether the sectors shall be drawn.
117    * <p>
118    * Default is <code>false</code>.
119    * </p>
120    *
121    * @return <code>true</code>, iff the sectors shall be drawn.
122    */
123   public boolean isDrawingSectors() {
124     return drawingSectors;
125   }
126 
127   /**
128    * Sets whether the sectors shall be drawn.
129    *
130    * @return <code>true</code>, iff the sectors shall be drawn.
131    */
132   public void setDrawingSectors(boolean drawingSectors) {
133     this.drawingSectors = drawingSectors;
134   }
135 
136   /**
137    * Returns the bounding box of the largest circle.
138    * @return The bounding box of the largest circle.
139    */
140   public Rectangle getBounds() {
141     return bounds;
142   }
143 
144   /**
145    * Updates the radii and sector information based on the nodes' NodeInfo objects.
146    */
147   public void updateSectors() {
148     final Graph2D g = view.getGraph2D();
149 
150     if (g.N() < 1) {
151       return;
152     }
153 
154     // the common center of the circles
155     center = null;
156     // a list of RadialLayouter.NodeInfo objects drawn in the paint method
157     nodeInfos = new ArrayList(g.N());
158 
159     // collect all RadialLayouter.NodeInfo objects as well as the common center and radii of the circles
160     HashSet radiiSet = new HashSet();
161     for (NodeCursor nc = g.nodes(); nc.ok(); nc.next()) {
162       Node node = nc.node();
163       RadialLayouter.NodeInfo info = (RadialLayouter.NodeInfo) nodeInfoMap.get(node);
164       if (info != null) {
165         // a layout with NodeInfos has been calculated for the current graph
166         if (center == null) { // only calculate the center once
167           YPoint nodeCenter = g.getCenter(node);
168           // RadialLayouter.NodeInfo contains the offset from the center of the circle the
169           // node is placed on to the center of the node.
170           center = new YPoint(info.getCenterOffset().getX() - nodeCenter.getX(), info.getCenterOffset().getY() - nodeCenter.getY());
171         }
172         nodeInfos.add(info);
173 
174         // we collect the radii of all circles the  nodes are placed on
175         radiiSet.add(new Double(info.getRadius()));
176       }
177     }
178     radii = new double[radiiSet.size()];
179 
180     if (center == null) {
181       // no node data is available
182       bounds.setFrame(0, 0, 0, 0);
183     } else {
184       // fill radii array and sort it ascending
185       int index = 0;
186       for (Iterator it = radiiSet.iterator(); it.hasNext(); index++) {
187         radii[index] = ((Double) it.next()).doubleValue();
188       }
189       Arrays.sort(radii);
190 
191       // sort NodeInfos by descending circleIndex and ascending wedgeStart
192       Collections.sort(nodeInfos, new Comparator() {
193         public int compare(Object o1, Object o2) {
194           RadialLayouter.NodeInfo info1 = (RadialLayouter.NodeInfo) o1;
195           RadialLayouter.NodeInfo info2 = (RadialLayouter.NodeInfo) o2;
196           if (info1.getCircleIndex() != info2.getCircleIndex()) {
197             return info2.getCircleIndex() - info1.getCircleIndex();
198           } else {
199             double dWedgeStart = info1.getSectorStart() - info2.getSectorStart();
200             return dWedgeStart < 0 ? -1 : dWedgeStart > 0 ? 1 : 0;
201           }
202         }
203       });
204 
205       // update bounds to include the out-most circle
206       double maxRadius = radii[radii.length - 1];
207       bounds.setFrame(center.getX() - maxRadius, center.getY() - maxRadius, 2 * maxRadius, 2 * maxRadius);
208     }
209     g.updateViews();
210   }
211 
212   public void paint(final Graphics2D g) {
213     if (radii == null || radii.length == 0) {
214       // no layout has been calculated for the current graph
215       return;
216     }
217 
218     final Color oldColor = g.getColor();
219     final Stroke oldStroke = g.getStroke();
220 
221     g.setStroke(stroke);
222     if (isDrawingSectors()) {
223       int colorIndex = 0;
224       for (int i = 0; i < nodeInfos.size(); i++) {
225 
226         RadialLayouter.NodeInfo info = (RadialLayouter.NodeInfo) nodeInfos.get(i);
227         double radius = info.getRadius();
228 
229         // the fill colors of the sectors alternate between two color sets depending on the circle index
230         // sectors on the same circle toggle their fill color between the two colors in their set
231         g.setColor(colors[info.getCircleIndex() % 2][colorIndex]);
232         colorIndex = (colorIndex + 1) % 2;
233 
234         g.fill(new Arc2D.Double(center.getX() - radius, center.getY() - radius, 2 * radius, 2 * radius,
235                 info.getSectorStart(), info.getSectorSize(), Arc2D.PIE));
236       }
237     }
238 
239     if (isDrawingCircles()) {
240       g.setColor(colors[0][0]);
241 
242       for (int i = 0; i < radii.length; i++) {
243         double radius = radii[i];
244 
245         g.draw(new Ellipse2D.Double(center.getX() - radius, center.getY() - radius, 2 * radius, 2 * radius));
246       }
247     }
248 
249     g.setStroke(oldStroke);
250     g.setColor(oldColor);
251   }
252 }
253