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.anim.AnimationFactory;
31  import y.anim.AnimationObject;
32  import y.anim.AnimationPlayer;
33  import y.anim.CompositeAnimationObject;
34  import y.base.DataProvider;
35  import y.base.Edge;
36  import y.base.EdgeCursor;
37  import y.base.EdgeMap;
38  import y.base.Node;
39  import y.base.NodeCursor;
40  import y.geom.YDimension;
41  import y.geom.YPoint;
42  import y.layout.AbstractLayoutStage;
43  import y.layout.BufferedLayouter;
44  import y.layout.GraphLayout;
45  import y.layout.LayoutGraph;
46  import y.layout.Layouter;
47  import y.layout.router.polyline.EdgeRouter;
48  import y.util.DataProviderAdapter;
49  import y.util.Maps;
50  import y.view.Drawable;
51  import y.view.Graph2D;
52  import y.view.Graph2DView;
53  import y.view.Graph2DViewRepaintManager;
54  import y.view.LayoutMorpher;
55  import y.view.NodePort;
56  import y.view.NodeRealizer;
57  
58  import java.awt.Graphics2D;
59  import java.awt.Rectangle;
60  import java.util.HashMap;
61  
62  /**
63   * This class animates opening and closing of sections of the UML class. First it is an {@link AnimationObject} that
64   * controls the progress of the animation and the update of the view. The progress is expressed by a value that varies
65   * between <code>0</code> and <code>1</code>. On the other hand it is an {@link Drawable} that is repainted on every
66   * view update. The repaint is delegated to {@link UmlClassConfiguration#paintAnimatedNode(y.view.NodeRealizer,
67   * java.awt.Graphics2D, double, double, double)} with the current progress.
68   */
69  abstract class UmlClassAnimation implements AnimationObject, Drawable {
70    protected static final int DURATION = 300;
71  
72    protected final Graph2DView view;
73    protected final NodeRealizer context;
74    protected final boolean isClosing;
75    private final UmlClassConfiguration painter;
76    private final Graph2DViewRepaintManager repaintManager;
77    private double state;
78  
79    /**
80     * Creates an animation to animate opening and closing of sections of the UML class.
81     *
82     * @param view      the view to paint the animation
83     * @param context   the realizer to animate
84     * @param isClosing whether to animate opening and closing of sections
85     */
86    UmlClassAnimation(
87        final Graph2DView view,
88        final NodeRealizer context,
89        final boolean isClosing
90    ) {
91      this.view = view;
92      this.context = context;
93      this.isClosing = isClosing;
94      painter = new UmlClassConfiguration();
95      this.repaintManager = new Graph2DViewRepaintManager(view);
96    }
97  
98    /**
99     * Animates the opening and closing of the a node and the movement of its adjacent edges. The animated painting of the
100    * node is done by the method {@link UmlClassConfiguration#paintAnimatedNode(y.view.NodeRealizer, java.awt.Graphics2D, double, double, double)},
101    * and is called from here. The animation of the adjacent edges is done here.
102    * The animation of the node works only on open nodes for both cases opening and closing. In the latter case the
103    * node will be closed after the animation has been finished. But we need the closed size of the node to calculate the
104    * target layout for the layout morpher with the edge router. Therefore the {@link PreserveNodeSizeLayoutStage} shrinks
105    * the nodes to the closed size before and resize them to the opened size after the edge routing.
106    * To avoid that the adjacent edges are clipped at the opened size while the animation we add {@link NodePort}s at the
107    * connection points of the closed node.
108    *
109    */
110   public void play() {
111     final AnimationPlayer player = new AnimationPlayer();
112     player.addAnimationListener(repaintManager);
113     view.addDrawable(this);
114 
115     // add all adjacent edges to repaint manager
116     final Graph2D graph = view.getGraph2D();
117     final Node currentNode = context.getNode();
118     for (EdgeCursor ec = currentNode.edges(); ec.ok(); ec.next()) {
119       final Edge edge = ec.edge();
120       repaintManager.add(graph.getRealizer(edge));
121     }
122 
123     // store source and target ports to set node ports at these positions
124     EdgeMap sourcePorts = Maps.createHashedEdgeMap();
125     EdgeMap targetPorts = Maps.createHashedEdgeMap();
126     for (EdgeCursor ec = currentNode.edges(); ec.ok(); ec.next()) {
127       final Edge edge = ec.edge();
128       sourcePorts.set(edge, graph.getSourcePointAbs(edge));
129       targetPorts.set(edge, graph.getTargetPointAbs(edge));
130     }
131     if (!isClosing) {
132       open();
133     }
134 
135     // add node ports for all adjacent edges, these node ports get animated afterwards
136     final NodeRealizer currentRealizer = graph.getRealizer(currentNode);
137     for (EdgeCursor ec = currentNode.outEdges(); ec.ok(); ec.next()) {
138       final Edge outEdge = ec.edge();
139       final NodePort port = UmlRealizerFactory.createDummyNodePort(currentRealizer, (YPoint) sourcePorts.get(outEdge));
140       currentRealizer.addPort(port);
141       port.bindSourcePort(outEdge);
142     }
143     for (EdgeCursor ec = currentNode.inEdges(); ec.ok(); ec.next()) {
144       final Edge inEdge = ec.edge();
145       final NodePort port = UmlRealizerFactory.createDummyNodePort(currentRealizer, (YPoint) targetPorts.get(inEdge));
146       currentRealizer.addPort(port);
147       port.bindTargetPort(inEdge);
148     }
149 
150     // if it is a closing animation, store the future size of the current node for layout
151     if (isClosing) {
152       graph.addDataProvider(PreserveNodeSizeLayoutStage.NODE_SIZE_DPKEY, new DataProviderAdapter() {
153         public Object get(Object dataHolder) {
154           if (dataHolder == currentNode) {
155             return getClosedSize();
156           } else {
157             return null;
158           }
159         }
160       });
161     }
162 
163     // only route adjacent edges of the current node
164     final EdgeRouter edgeRouter = new EdgeRouter();
165     edgeRouter.setSphereOfAction(EdgeRouter.ROUTE_EDGES_AT_SELECTED_NODES);
166     graph.addDataProvider(EdgeRouter.SELECTED_NODES, new DataProviderAdapter() {
167       public boolean getBool(Object dataHolder) {
168         if (dataHolder instanceof Node) {
169           Node node = (Node) dataHolder;
170           return node == currentNode;
171         }
172         return false;
173       }
174     });
175 
176     // configure layouter
177     final BufferedLayouter layouter = new BufferedLayouter(new PreserveNodeSizeLayoutStage(edgeRouter));
178     final GraphLayout graphLayout = layouter.calcLayout(graph);
179 
180     // clean up data providers
181     graph.removeDataProvider(PreserveNodeSizeLayoutStage.NODE_SIZE_DPKEY);
182     graph.removeDataProvider(EdgeRouter.SELECTED_NODES);
183 
184     // construct and run the animation
185     final CompositeAnimationObject concurrentAnimation = AnimationFactory.createConcurrency();
186     final LayoutMorpher morpher = new LayoutMorpher(view, graphLayout);
187     morpher.setPreferredDuration(DURATION);
188     morpher.setKeepZoomFactor(true);
189     concurrentAnimation.addAnimation(morpher);
190     concurrentAnimation.addAnimation(this);
191 
192     player.animate(concurrentAnimation);
193 
194     // if it is a closing animation, close the node while keeping the calculated port positions
195     if (isClosing) {
196       sourcePorts = Maps.createHashedEdgeMap();
197       targetPorts = Maps.createHashedEdgeMap();
198       for (EdgeCursor ec = currentNode.edges(); ec.ok(); ec.next()) {
199         final Edge edge = ec.edge();
200         sourcePorts.set(edge, graph.getSourcePointAbs(edge));
201         targetPorts.set(edge, graph.getTargetPointAbs(edge));
202       }
203 
204       close();
205 
206       for (EdgeCursor ec = currentNode.edges(); ec.ok(); ec.next()) {
207         final Edge edge = ec.edge();
208         graph.setSourcePointAbs(edge, (YPoint) sourcePorts.get(edge));
209         graph.setTargetPointAbs(edge, (YPoint) targetPorts.get(edge));
210       }
211     }
212 
213     // clean up
214     for (int i = currentRealizer.portCount() - 1; i >= 0; i--) {
215       currentRealizer.removePort(i);
216     }
217     for (EdgeCursor ec = currentNode.edges(); ec.ok(); ec.next()) {
218       final Edge edge = ec.edge();
219       repaintManager.remove(graph.getRealizer(edge));
220     }
221     view.removeDrawable(this);
222     player.removeAnimationListener(repaintManager);
223   }
224 
225   /**
226    * Closes the part of the node whose change gets animated.
227    */
228   protected abstract void close();
229 
230   /**
231    * Opens the part of the node whose change gets animated.
232    */
233   protected abstract void open();
234 
235   /**
236    * Returns the size of the realizer in closed state.
237    */
238   protected abstract YDimension getClosedSize();
239 
240   /**
241    * Returns the lower y-coordinate of the part of the realizer that is fixed during the animation.
242    *
243    * @return the lower y-coordinate of the part of the realizer that is fixed during the animation
244    */
245   protected abstract double getFixedY();
246 
247   /**
248    * Returns the upper y-coordinate of the part of the realizer that moves during the animation.
249    *
250    * @return the upper y-coordinate of the part of the realizer that moves during the animation
251    */
252   protected abstract double getMovingY();
253 
254   /**
255    * This method gets called after the state of the animation has been updated. It serves as a hook to perform some
256    * actions after this event has happened. By default this method does nothing.
257    */
258   protected void stateUpdated(final double state) {
259   }
260 
261   public void initAnimation() {
262     context.setVisible(false);
263     repaintManager.add(this);
264   }
265 
266   public void calcFrame(final double time) {
267     state = isClosing ? 1.0 - time : time;
268     stateUpdated(state);
269   }
270 
271   public void disposeAnimation() {
272     repaintManager.remove(this);
273     context.setVisible(true);
274   }
275 
276   public long preferredDuration() {
277     return DURATION;
278   }
279 
280   public void paint(final Graphics2D graphics) {
281     painter.paintAnimatedNode(context, graphics, getFixedY(), getMovingY(), state);
282   }
283 
284   public Rectangle getBounds() {
285     final double lineWidth = UmlRealizerFactory.LINE_EDGE_CREATION_BUTTON_OUTLINE.getLineWidth();
286     final int minX = (int) Math.floor(context.getX() - lineWidth * 0.5);
287     final int minY = (int) Math.floor(context.getY() - lineWidth * 0.5);
288     final int maxX = (int) Math.ceil(context.getX() + context.getWidth() + lineWidth * 0.5);
289     final int maxY = (int) Math.ceil(context.getY() + context.getHeight() + lineWidth * 0.5);
290     return new Rectangle(minX, minY, maxX - minX, maxY - minY);
291   }
292 
293   /**
294    * Layout stage that takes care about the nodes sizes.
295    * It resizes the current node to its future size after the animation so the layout is calculated correctly.
296    * Afterwards, it restores the nodes size so it is consistent with node in the view graph.
297    */
298   private static final class PreserveNodeSizeLayoutStage extends AbstractLayoutStage {
299     public static final Object NODE_SIZE_DPKEY = "PreserveNodeSizeLayoutStage.NODE_SIZE_DPKEY";
300 
301     public PreserveNodeSizeLayoutStage(Layouter layouter) {
302       super(layouter);
303     }
304 
305     public boolean canLayout(LayoutGraph graph) {
306       return canLayoutCore(graph);
307     }
308 
309     public void doLayout(LayoutGraph graph) {
310       // store old sizes and apply new sizes
311       final HashMap oldSizes = new HashMap();
312       final DataProvider dp = graph.getDataProvider(NODE_SIZE_DPKEY);
313       if (dp != null) {
314         for (NodeCursor nc = graph.nodes(); nc.ok(); nc.next()) {
315           final Node node = nc.node();
316           final Object size = dp.get(node);
317           if (size instanceof YDimension) {
318             oldSizes.put(node, graph.getSize(node));
319             final YPoint location = graph.getLocation(node);
320             graph.setSize(node, (YDimension) size);
321             graph.setLocation(node, location);
322           }
323         }
324       }
325 
326       // do the actual layout
327       doLayoutCore(graph);
328 
329       // restore the old sizes
330       for (NodeCursor nc = graph.nodes(); nc.ok(); nc.next()) {
331         final Node node = nc.node();
332         final YDimension size = (YDimension) oldSizes.get(node);
333         if (size != null) {
334           // the node gets closed
335           // store source and target ports
336           final EdgeMap sourcePorts = Maps.createHashedEdgeMap();
337           final EdgeMap targetPorts = Maps.createHashedEdgeMap();
338           for (EdgeCursor ec = node.edges(); ec.ok(); ec.next()) {
339             final Edge edge = ec.edge();
340             sourcePorts.set(edge, graph.getSourcePointAbs(edge));
341             targetPorts.set(edge, graph.getTargetPointAbs(edge));
342           }
343 
344           // change node size (ports are changed)
345           final YPoint location = graph.getLocation(node);
346           graph.setSize(node, size);
347           graph.setLocation(node, location);
348 
349           // restore source and target ports
350           for (EdgeCursor ec = node.edges(); ec.ok(); ec.next()) {
351             final Edge edge = ec.edge();
352             graph.setSourcePointAbs(edge, (YPoint) sourcePorts.get(edge));
353             graph.setTargetPointAbs(edge, (YPoint) targetPorts.get(edge));
354           }
355         }
356       }
357     }
358   }
359 }
360