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.view.uml;
29  
30  import y.base.Node;
31  import y.geom.YPoint;
32  import y.view.Drawable;
33  import y.view.Graph2D;
34  import y.view.Graph2DView;
35  import y.view.LineType;
36  import y.view.NodeRealizer;
37  
38  import java.awt.Color;
39  import java.awt.Graphics2D;
40  import java.awt.Rectangle;
41  import java.awt.Shape;
42  import java.awt.geom.GeneralPath;
43  import java.awt.geom.Point2D;
44  import java.awt.geom.Rectangle2D;
45  
46  /**
47   * A {@link y.view.Drawable} that draws a set of edge creation buttons.
48   * <p>
49   *   It is possible to show/hide the buttons with an animation that fans the buttons out or rolls them behind the node.
50   *   To change the positions of the buttons during animation the {@link #setProgress(double) progress} must be set.
51   * </p>
52   */
53  class UmlEdgeCreationButtons implements Drawable {
54    private static final int BUTTON_COUNT = 6;
55    private static final int RADIUS = 15;
56    private static final int DIAMETER = RADIUS * 2;
57    private static final int GAP = 20;
58  
59    static final int TYPE_ASSOCIATION = 0;
60    static final int TYPE_DEPENDENCY = 1;
61    static final int TYPE_GENERALIZATION = 2;
62    static final int TYPE_REALIZATION = 3;
63    static final int TYPE_AGGREGATION = 4;
64    static final int TYPE_COMPOSITION = 5;
65  
66    private final Graph2DView view;
67    private final Graph2D graph;
68    private final Node node;
69    private final YPoint startOffset;
70    private final double[] angles;
71    private int selectedIndex;
72    private double progress;
73  
74    UmlEdgeCreationButtons(Graph2DView view, final Node node) {
75      this.view = view;
76      this.node = node;
77      this.graph = view.getGraph2D();
78  
79      // initialize start position and angles between start and end positions for the buttons
80      // start at 22.5 degrees inside the node and then add 45 degrees more to every next end position
81      startOffset = new YPoint(DIAMETER * -1.35, DIAMETER * 0.6);
82      angles = new double[BUTTON_COUNT];
83      for (int i = 0; i < BUTTON_COUNT; i++) {
84        angles[i] = (i + 1) * 0.7853;
85      }
86  
87      selectedIndex = -1;
88      progress = 1;
89    }
90  
91    public void paint(final Graphics2D graphics) {
92      final Graphics2D gfx = (Graphics2D) graphics.create();
93  
94      try {
95        // set a clip to avoid painting the buttons when they are behind the node
96        gfx.clip(createClip(graphics));
97  
98        // scale graphics context for drawing the zoom-invariant buttons
99        final double zoom = 1 / view.getZoom();
100       gfx.scale(zoom, zoom);
101 
102       paintButtons(gfx);
103 
104     } finally {
105       gfx.dispose();
106     }
107   }
108 
109   /**
110    * Creates a {@link java.awt.Shape} that covers the whole area except the size and location of the associated node.
111    * That way, the buttons are first covered by the node and seem to appear from behind the node.
112    */
113   private Shape createClip(final Graphics2D graphics) {
114     final float outline = (UmlRealizerFactory.LINE_EDGE_CREATION_BUTTON_OUTLINE.getLineWidth()) / 2;
115     final GeneralPath clip = new GeneralPath(GeneralPath.WIND_EVEN_ODD);
116     final Rectangle2D.Double bounds = graph.getRealizer(node).getBoundingBox();
117     clip.append(graphics.getClip(), true);
118     final double x = bounds.getX();
119     final double y = bounds.getY();
120     final double w = bounds.getWidth();
121     final double height = bounds.getHeight();
122     final double buttonHeight = (DIAMETER + GAP) / view.getZoom();
123     final double h = height < buttonHeight ? buttonHeight : height;
124 
125     clip.moveTo((float) (x - outline), (float) (y - outline));
126     clip.lineTo((float) (x + w + outline), (float) (y - outline));
127     clip.lineTo((float) (x + w + outline), (float) (y + h + outline));
128     clip.lineTo((float) (x - outline), (float) (y + h + outline));
129     clip.closePath();
130     return clip;
131   }
132 
133   /**
134    * Paints the circular buttons to their current position depending on the node's position and the progress value.
135    */
136   private void paintButtons(final Graphics2D graphics) {
137     final GeneralPath path = new GeneralPath();
138     final Point2D position = new Point2D.Double(0, 0);
139     for (int i = 0; i < BUTTON_COUNT; i++) {
140       calcPosition(i, position);
141       if (i != selectedIndex) {
142         graphics.setColor(UmlRealizerFactory.COLOR_BACKGROUND);
143       } else {
144         graphics.setColor(UmlRealizerFactory.COLOR_SELECTION);
145       }
146       graphics.fillOval((int) position.getX(), (int) position.getY(), DIAMETER, DIAMETER);
147 
148       graphics.setColor(Color.DARK_GRAY);
149       graphics.setStroke(LineType.LINE_2);
150       graphics.drawOval((int) position.getX(), (int) position.getY(), DIAMETER, DIAMETER);
151 
152       paintIcon(graphics, position, i, path);
153     }
154   }
155 
156   /**
157    * Calculates the positions of the buttons depending on the current time step. The result position is returned in the
158    * passed point.
159    */
160   private void calcPosition(final int buttonIndex, final Point2D position) {
161     final int part = buttonIndex / BUTTON_COUNT;
162     final Point2D anchor = getAnchor();
163     if (progress >= part) {
164       final double angle = angles[buttonIndex] * (progress - part);
165       final double offsetX = startOffset.getX() * Math.cos(angle) - startOffset.getY() * Math.sin(angle);
166       final double offsetY = startOffset.getX() * Math.sin(angle) + startOffset.getY() * Math.cos(angle);
167 
168       position.setLocation(anchor.getX() + offsetX - RADIUS, anchor.getY() + offsetY - RADIUS);
169     } else {
170       position.setLocation(anchor.getX() + startOffset.getX() - RADIUS, anchor.getY() + startOffset.getY() - RADIUS);
171     }
172   }
173 
174   /**
175    * Returns the upper-right corner of the associated node as anchor point for the buttons.
176    */
177   private Point2D getAnchor() {
178     final NodeRealizer realizer = graph.getRealizer(node);
179     final double zoom = view.getZoom();
180     final double outline = (UmlRealizerFactory.LINE_EDGE_CREATION_BUTTON_OUTLINE.getLineWidth() * zoom) * 0.5;
181     final double x = (realizer.getX() + realizer.getWidth()) * zoom + outline;
182     final double y = realizer.getY() * zoom - outline;
183     return new Point2D.Double(x, y);
184   }
185 
186   /**
187    * Paints the passed type of edge icon on the at the given position.
188    *
189    * @param graphics the current graphics context.
190    * @param position the position of the icon.
191    * @param type which icon to paint.
192    * @param path a path object that can be used to paint the icon and also can be reused for the next icon.
193    */
194   private void paintIcon(final Graphics2D graphics, final Point2D position, final int type, final GeneralPath path) {
195     path.reset();
196     graphics.setColor(Color.DARK_GRAY);
197     switch (type) {
198       case TYPE_ASSOCIATION:
199         graphics.setStroke(LineType.LINE_1);
200         graphics.drawLine((int) (position.getX() + DIAMETER * 0.25), (int) (position.getY() + DIAMETER * 0.75),
201             (int) (position.getX() + DIAMETER * 0.75), (int) (position.getY() + DIAMETER * 0.25));
202         break;
203       case TYPE_DEPENDENCY:
204         graphics.setStroke(LineType.DASHED_1);
205         path.moveTo((float) (position.getX() + DIAMETER * 0.25), (float) (position.getY() + DIAMETER * 0.75));
206         path.lineTo((float) (position.getX() + DIAMETER * 0.75), (float) (position.getY() + DIAMETER * 0.25));
207         graphics.draw(path);
208 
209         path.reset();
210         graphics.setStroke(LineType.LINE_1);
211         path.moveTo((float) (position.getX() + DIAMETER * 0.75), (float) (position.getY() + DIAMETER * 0.25));
212         path.lineTo((float) (position.getX() + DIAMETER * 0.5), (float) (position.getY() + DIAMETER * 0.375));
213         path.moveTo((float) (position.getX() + DIAMETER * 0.625), (float) (position.getY() + DIAMETER * 0.5));
214         path.lineTo((float) (position.getX() + DIAMETER * 0.75), (float) (position.getY() + DIAMETER * 0.25));
215         graphics.draw(path);
216 
217         break;
218       case TYPE_GENERALIZATION:
219         graphics.setStroke(LineType.LINE_1);
220         path.moveTo((float) (position.getX() + DIAMETER * 0.25), (float) (position.getY() + DIAMETER * 0.75));
221         path.lineTo((float) (position.getX() + DIAMETER * 0.5625), (float) (position.getY() + DIAMETER * 0.4375));
222         path.moveTo((float) (position.getX() + DIAMETER * 0.75), (float) (position.getY() + DIAMETER * 0.25));
223         path.lineTo((float) (position.getX() + DIAMETER * 0.5), (float) (position.getY() + DIAMETER * 0.375));
224         path.lineTo((float) (position.getX() + DIAMETER * 0.625), (float) (position.getY() + DIAMETER * 0.5));
225         path.lineTo((float) (position.getX() + DIAMETER * 0.75), (float) (position.getY() + DIAMETER * 0.25));
226         graphics.draw(path);
227 
228         break;
229       case TYPE_REALIZATION:
230         graphics.setStroke(LineType.DASHED_1);
231         path.moveTo((float) (position.getX() + DIAMETER * 0.25), (float) (position.getY() + DIAMETER * 0.75));
232         path.lineTo((float) (position.getX() + DIAMETER * 0.5625), (float) (position.getY() + DIAMETER * 0.4375));
233         graphics.draw(path);
234 
235         path.reset();
236         graphics.setStroke(LineType.LINE_1);
237         path.moveTo((float) (position.getX() + DIAMETER * 0.75), (float) (position.getY() + DIAMETER * 0.25));
238         path.lineTo((float) (position.getX() + DIAMETER * 0.5), (float) (position.getY() + DIAMETER * 0.375));
239         path.lineTo((float) (position.getX() + DIAMETER * 0.625), (float) (position.getY() + DIAMETER * 0.5));
240         path.lineTo((float) (position.getX() + DIAMETER * 0.625), (float) (position.getY() + DIAMETER * 0.5));
241         path.lineTo((float) (position.getX() + DIAMETER * 0.75), (float) (position.getY() + DIAMETER * 0.25));
242         graphics.draw(path);
243 
244         break;
245       case TYPE_AGGREGATION:
246         graphics.setStroke(LineType.LINE_1);
247         path.moveTo((float) (position.getX() + DIAMETER * 0.5), (float) (position.getY() + DIAMETER * 0.5));
248         path.lineTo((float) (position.getX() + DIAMETER * 0.3125), (float) (position.getY() + DIAMETER * 0.5625));
249         path.lineTo((float) (position.getX() + DIAMETER * 0.25), (float) (position.getY() + DIAMETER * 0.75));
250         path.lineTo((float) (position.getX() + DIAMETER * 0.4375), (float) (position.getY() + DIAMETER * 0.6875));
251         path.lineTo((float) (position.getX() + DIAMETER * 0.5), (float) (position.getY() + DIAMETER * 0.5));
252         path.lineTo((float) (position.getX() + DIAMETER * 0.75), (float) (position.getY() + DIAMETER * 0.25));
253 
254         graphics.draw(path);
255         break;
256       case TYPE_COMPOSITION:
257         graphics.setStroke(LineType.LINE_1);
258         path.moveTo((float) (position.getX() + DIAMETER * 0.5), (float) (position.getY() + DIAMETER * 0.5));
259         path.lineTo((float) (position.getX() + DIAMETER * 0.3125), (float) (position.getY() + DIAMETER * 0.5625));
260         path.lineTo((float) (position.getX() + DIAMETER * 0.25), (float) (position.getY() + DIAMETER * 0.75));
261         path.lineTo((float) (position.getX() + DIAMETER * 0.4375), (float) (position.getY() + DIAMETER * 0.6875));
262         path.lineTo((float) (position.getX() + DIAMETER * 0.5), (float) (position.getY() + DIAMETER * 0.5));
263         path.lineTo((float) (position.getX() + DIAMETER * 0.75), (float) (position.getY() + DIAMETER * 0.25));
264 
265         graphics.draw(path);
266         graphics.fill(path);
267     }
268   }
269 
270   /**
271    * Returns a rectangle that contains all buttons and some space around them.
272    */
273   public Rectangle getBounds() {
274     final double zoom = view.getZoom();
275     final Point2D position = new Point2D.Double(0, 0);
276     double minX = Double.MAX_VALUE;
277     double minY = Double.MAX_VALUE;
278     double maxX = -Double.MAX_VALUE;
279     double maxY = -Double.MAX_VALUE;
280     for (int i = 0; i < BUTTON_COUNT; i++) {
281       calcPosition(i, position);
282 
283       // update bounds
284       final int diameter = (int) (DIAMETER / zoom);
285       final int gap = (int) (GAP / zoom);
286       minX = Math.min(minX, position.getX() / zoom - gap);
287       minY = Math.min(minY, position.getY() / zoom - gap);
288       maxX = Math.max(maxX, position.getX() / zoom + gap + diameter);
289       maxY = Math.max(maxY, position.getY() / zoom + gap + diameter);
290     }
291 
292     final int x1 = (int) Math.floor(minX);
293     final int y1 = (int) Math.floor(minY);
294     final int x2 = (int) Math.ceil(maxX);
295     final int y2 = (int) Math.ceil(maxY);
296     return new Rectangle(x1, y1, x2 - x1, y2 - y1);
297   }
298 
299   /**
300    * Returns the progress on the way from the start positions to the end positions.
301    *
302    * @return a number in the interval [0,1] that specifies the progress on the way from the start positions to the end
303    *         positions.
304    */
305   public double getProgress() {
306     return progress;
307   }
308 
309   /**
310    * Specifies the progress on the way from the start positions to the end positions.
311    *
312    * @param progress a number in the interval [0,1] that specifies the progress on the way from the start positions to
313    *                 the end positions
314    */
315   public void setProgress(final double progress) {
316     this.progress = progress;
317   }
318 
319   /**
320    * Returns the node that is associated with the buttons.
321    *
322    * @return the node that is associated with the buttons.
323    */
324   public Node getNode() {
325     return node;
326   }
327 
328   /**
329    * Selects the button at the given coordinates.
330    *
331    * @param x x-coordinate in world coordinates.
332    * @param y y-coordinate in world coordinates.
333    */
334   public void selectButtonAt(final double x, final double y) {
335     final int index = calculateButtonIndexAt(x, y);
336     setSelectedIndex(index);
337   }
338 
339   /**
340    * Returns the index of the currently selected button.
341    *
342    * @return the index of the currently selected button.
343    */
344   public int getSelectedButtonIndex() {
345     return selectedIndex;
346   }
347 
348   /**
349    * Selects the button at the given index.
350    */
351   public void setSelectedIndex(final int index) {
352     this.selectedIndex = index;
353   }
354 
355   /**
356    * Checks whether or not there is a button at the given coordinates.
357    *
358    * @param x x-coordinate in world coordinates.
359    * @param y y-coordinate in world coordinates.
360    *
361    * @return <code>true</code> if there is a button at the given coordinates, <code>false</code> otherwise.
362    */
363   public boolean hasButtonAt(final double x, final double y) {
364     final int index = calculateButtonIndexAt(x, y);
365     return index >= 0;
366   }
367 
368   /**
369    * Determines the index of the button at the given coordinates.
370    *
371    * @param x x-coordinate in world coordinates.
372    * @param y y-coordinate in world coordinates.
373    *
374    * @return the index of the button at the given coordinates or <code>-1</code> if there is no button at that location.
375    */
376   private int calculateButtonIndexAt(final double x, final double y) {
377     final double zoom = view.getZoom();
378 
379     final Point2D.Double position = new Point2D.Double(0, 0);
380     for (int i = 0; i < BUTTON_COUNT; i++) {
381       calcPosition(i, position);
382       final double posX = (position.getX() + RADIUS) / zoom;
383       final double posY = (position.getY() + RADIUS) / zoom;
384       final double radius = RADIUS / zoom;
385       if (radius > Math.sqrt((posX - x) * (posX - x) + (posY - y) * (posY - y))) {
386         return i;
387       }
388     }
389     return -1;
390   }
391 
392   /**
393    * Determines whether or not these edge creation buttons contain the given coordinates.
394    */
395   public boolean contains(final double x, final double y) {
396     return getBounds().contains(x, y);
397   }
398 }
399