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.advanced.ports;
29  
30  import y.geom.YPoint;
31  import y.io.graphml.NamespaceConstants;
32  import y.io.graphml.input.DeserializationEvent;
33  import y.io.graphml.input.DeserializationHandler;
34  import y.io.graphml.input.GraphMLParseException;
35  import y.io.graphml.output.GraphMLWriteException;
36  import y.io.graphml.output.SerializationEvent;
37  import y.io.graphml.output.SerializationHandler;
38  import y.io.graphml.output.XmlWriter;
39  import y.view.NodePort;
40  import y.view.NodeRealizer;
41  import y.view.PortLocationModel;
42  import y.view.PortLocationModelParameter;
43  
44  import java.util.StringTokenizer;
45  import org.w3c.dom.NamedNodeMap;
46  import org.w3c.dom.Node;
47  
48  /**
49   * A {@link y.view.PortLocationModel} for node ports whose location is
50   * restricted to one or more sides of the associated node's visual bounds
51   * (as represented by the associated {@link y.view.NodeRealizer}).
52   * Internally, the location is stored as the ratio by which the width and height
53   * of the realizer need to be scaled to obtain the offset to the center of the
54   * node layout.
55   */
56  public class SidePortLocationModel implements PortLocationModel {
57    /**
58     * Side specifier for ports that may be located along their owner node's
59     * top border.
60     * @see SidePortLocationModel#newInstance(int)
61     * @see #getSides()
62     */
63    public static final byte SIDE_TOP = 1;    // 0001
64    /**
65     * Side specifier for ports that may be located along their owner node's
66     * left border.
67     * @see SidePortLocationModel#newInstance(int)
68     * @see #getSides()
69     */
70    public static final byte SIDE_LEFT = 2;   // 0010
71    /**
72     * Side specifier for ports that may be located along their owner node's
73     * bottom border.
74     * @see SidePortLocationModel#newInstance(int)
75     * @see #getSides()
76     */
77    public static final byte SIDE_BOTTOM = 4; // 0100
78    /**
79     * Side specifier for ports that may be located along their owner node's
80     * right border.
81     * @see SidePortLocationModel#newInstance(int)
82     * @see #getSides()
83     */
84    public static final byte SIDE_RIGHT = 8;  // 1000
85  
86    private static final byte SIDE_ALL = SIDE_TOP|SIDE_LEFT|SIDE_BOTTOM|SIDE_RIGHT;
87  
88  
89    private final int sides;
90  
91    private SidePortLocationModel( final int sides ) {
92      this.sides = sides;
93    }
94  
95    /**
96     * Creates a parameter that tries to match the specified location in absolute
97     * world coordinates.
98     * @param owner The realizer that will own the port for which the parameter
99     * has to be created.
100    * @param location The location in the world coordinate system that should be
101    * matched as best as possible.
102    * @return a parameter that tries to match the specified location in absolute
103    * world coordinates.
104    * @see #getSides()
105    * @see #SIDE_TOP
106    * @see #SIDE_LEFT
107    * @see #SIDE_BOTTOM
108    * @see #SIDE_RIGHT
109    */
110   public PortLocationModelParameter createParameter(
111           final NodeRealizer owner,
112           final YPoint location
113   ) {
114     if (owner == null) {
115       return new Parameter(this, -0.5, -0.5);
116     } else {
117       final double x = owner.getX();
118       final double w = owner.getWidth();
119       final double y = owner.getY();
120       final double h = owner.getHeight();
121 
122       YPoint result = null;
123       double dist = Double.POSITIVE_INFINITY;
124       final byte[] s = {SIDE_TOP, SIDE_LEFT, SIDE_BOTTOM, SIDE_RIGHT};
125       for (int i = 0; i < s.length; ++i) {
126         if ((sides & s[i]) == s[i]) {
127           final YPoint p = calculateSideLocation(x, y, w, h, location, s[i]);
128           final double d = distSquared(p, location);
129           if (dist > d) {
130             dist = d;
131             result = p;
132           }
133         }
134       }
135 
136       // result is never null at this point
137       return createParameterImpl(x, y, w, h, result);
138     }
139   }
140 
141   /**
142    * Calculates the relative position of the given location in the
143    * specified reference rectangle.
144    * @param x the x-coordinate of the reference rectangle.
145    * @param y the y-coordinate of the reference rectangle.
146    * @param w the width of the reference rectangle.
147    * @param h the height of the reference rectangle.
148    * @param location the point to convert to relative coordinates
149    * @return the parameter representing the relative position of the given
150    * location in the specified reference rectangle.
151    */
152   private PortLocationModelParameter createParameterImpl(
153           final double x,
154           final double y,
155           final double w,
156           final double h,
157           final YPoint location
158   ) {
159     final double rx;
160     if (w > 0) {
161       rx = (location.getX() - x - w*0.5) / w;
162     } else {
163       rx = 0;
164     }
165 
166     final double ry;
167     if (h > 0) {
168       ry = (location.getY() - y - h*0.5) / h;
169     } else {
170       ry = 0;
171     }
172 
173     return new Parameter(this, rx, ry);
174   }
175 
176   /**
177    * Determines the location of the port for the given parameter.
178    * @param port The port to determine the location for.
179    * @param parameter The parameter to use.
180    * @return the calculated location of the port.
181    * @throws ClassCastException if the given parameter is not of the type
182    * created by this model.
183    */
184   public YPoint getLocation(
185           final NodePort port,
186           final PortLocationModelParameter parameter
187   ) {
188     final Parameter p = (Parameter) parameter;
189     final NodeRealizer nr = port.getRealizer();
190     return new YPoint(
191             nr.getCenterX() + p.ratioX * nr.getWidth(),
192             nr.getCenterY() + p.ratioY * nr.getHeight());
193   }
194 
195   /**
196    * Returns a bit mask that determines at which sides a port that is handled
197    * by this model may be located.
198    * @return a bit wise combination of {@link #SIDE_TOP}, {@link #SIDE_LEFT},
199    * {@link #SIDE_BOTTOM}, and/or {@link #SIDE_RIGHT}.
200    */
201   public int getSides() {
202     return sides;
203   }
204 
205 
206   /**
207    * Creates a new side port location model.
208    * @param sides determines at which sides a port that is handled
209    * by this model may be located. Must be a bit wise combination of
210    * {@link #SIDE_TOP}, {@link #SIDE_LEFT}, {@link #SIDE_BOTTOM}, and/or
211    * {@link #SIDE_RIGHT}. May not be <code>0</code>.
212    * @return a new side port location model.
213    */
214   public static SidePortLocationModel newInstance( final int sides ) {
215     if ((sides & SIDE_ALL) == 0) {
216       throw new IllegalArgumentException("Unsupported sides mask: " + sides);
217     }
218 
219     return new SidePortLocationModel(sides);
220   }
221 
222   /**
223    * Calculates the projection of the given location onto one of the specified
224    * rectangle's sides.
225    * @param x the x-coordinate of the rectangle.
226    * @param y the y-coordinate of the rectangle.
227    * @param w the width of the rectangle.
228    * @param h the height of the rectangle.
229    * @param location the point to project onto one of the given rectangle's
230    * sides.
231    * @param side determines the side onto which the given location is projected.
232    * Must be one of {@link #SIDE_TOP}, {@link #SIDE_LEFT}, {@link #SIDE_BOTTOM},
233    * and {@link #SIDE_RIGHT}.
234    * @return the projection of the given location onto one of the specified
235    * rectangle's sides.
236    */
237   private static YPoint calculateSideLocation(
238           final double x,
239           final double y,
240           final double w,
241           final double h,
242           final YPoint location,
243           final byte side
244   ) {
245     if (side == SIDE_TOP || side == SIDE_BOTTOM) {
246       double lx = location.getX();
247       if (lx < x) {
248         lx = x;
249       } else if (lx > x + w) {
250         lx = x + w;
251       }
252       final double ly = side == SIDE_TOP ? y : y + h;
253       return new YPoint(lx, ly);
254     } else { // side == SIDE_LEFT || side == SIDE_RIGHT
255       final double lx = side == SIDE_LEFT ? x : x + w;
256       double ly = location.getY();
257       if (ly < y) {
258         ly = y;
259       } else if (ly > y + h) {
260         ly = y + h;
261       }
262       return new YPoint(lx, ly);
263     }
264   }
265 
266   /**
267    * Returns the squared distance between the two given points.
268    * @param p1 the first point.
269    * @param p2 the second point.
270    * @return the squared distance between the two given points.
271    */
272   private static double distSquared( final YPoint p1, final YPoint p2 ) {
273     final double dx = p1.getX() - p2.getX();
274     final double dy = p1.getY() - p2.getY();
275     return dx*dx + dy*dy;
276   }
277 
278 
279   /**
280    * Stores the port location relative to the owner node bounds.
281    * A relative location of <code>(0.0, 0.0)</code> means the node's center.
282    * A relative location of <code>(-0.5, -0.5)</code> means the node's
283    * top left corner.
284    * A relative location of <code>(0.5, 0.5)</code> means the node's bottom
285    * right corner.
286    */
287   static final class Parameter implements PortLocationModelParameter {
288     /** The parameter's associated model. */
289     private final SidePortLocationModel model;
290     /** Relative x-coordinate of the port's location. */
291     private final double ratioX;
292     /** Relative y-coordinate of the port's location. */
293     private final double ratioY;
294 
295     /**
296      * Initializes a new parameter.
297      * @param model the parameter's associated model.
298      * @param ratioX relative x-coordinate of the port's location.
299      * @param ratioY relative y-coordinate of the port's location.
300      */
301     Parameter(
302             final SidePortLocationModel model,
303             final double ratioX,
304             final double ratioY
305     ) {
306       this.model = model;
307       this.ratioX = ratioX;
308       this.ratioY = ratioY;
309     }
310 
311     public PortLocationModel getModel() {
312       return model;
313     }
314   }
315 
316 
317   /**
318    * Provides GraphML (de-)serialization support for
319    * {@link SidePortLocationModel} and its parameters.
320    */
321   public static final class Handler implements SerializationHandler, DeserializationHandler {
322     private static final String NS_NAME = "demo";
323     private static final String MODEL_NODE_NAME = "SidePortLocationModel";
324 
325 
326     /**
327      * Writes {@link SidePortLocationModel} models and parameters.
328      * @param event contains all data that is needed for serialization.
329      */
330     public void onHandleSerialization(
331             final SerializationEvent event
332     ) throws GraphMLWriteException {
333       final Object item = event.getItem();
334       if (item instanceof Parameter) {
335         final Parameter param = (Parameter) item;
336         final XmlWriter writer = event.getWriter();
337         writer.writeStartElement(MODEL_NODE_NAME, NS_NAME);
338         writer.writeAttribute("sides", sidesToString(param));
339         writer.writeAttribute("ratioX", param.ratioX);
340         writer.writeAttribute("ratioY", param.ratioY);
341         writer.writeEndElement();
342         event.setHandled(true);
343       }
344     }
345 
346     private static String sidesToString( final Parameter p ) {
347       final int sides = ((SidePortLocationModel) p.getModel()).getSides();
348       final StringBuffer sb = new StringBuffer();
349       String del = "";
350       if ((sides & SIDE_TOP) == SIDE_TOP) {
351         sb.append(del).append("SIDE_TOP");
352         del = "|";
353       }
354       if ((sides & SIDE_LEFT) == SIDE_LEFT) {
355         sb.append(del).append("SIDE_LEFT");
356         del = "|";
357       }
358       if ((sides & SIDE_BOTTOM) == SIDE_BOTTOM) {
359         sb.append(del).append("SIDE_BOTTOM");
360         del = "|";
361       }
362       if ((sides & SIDE_RIGHT) == SIDE_RIGHT) {
363         sb.append(del).append("SIDE_RIGHT");
364         del = "|";
365       }
366       return sb.toString();
367     }
368 
369     /**
370      * Reads {@link SidePortLocationModel} models and parameters.
371      * @param event contains all data that is needed for deserialization.
372      * @throws GraphMLParseException if required attributes are missing or
373      * invalid.
374      */
375     public void onHandleDeserialization(
376             final DeserializationEvent event
377     ) throws GraphMLParseException {
378       final Node node = event.getXmlNode();
379       if (isNamespaceElement(node, NamespaceConstants.YFILES_JAVA_NS)) {
380         for (Node child = node.getFirstChild(); child != null; child = child.getNextSibling()) {
381           if (isNamespaceElement(child, NS_NAME) &&
382               MODEL_NODE_NAME.equals(child.getLocalName())) {
383             final NamedNodeMap attrs = child.getAttributes();
384 
385             double ratioX = 0;
386             final Node rxAttr = attrs.getNamedItem("ratioX");
387             if (rxAttr == null) {
388               throw new GraphMLParseException(
389                       "Missing attribute ratioX for element " +
390                       MODEL_NODE_NAME + ".");
391             } else {
392               ratioX = Double.parseDouble(rxAttr.getNodeValue());
393             }
394             double ratioY = 0;
395             final Node ryAttr = attrs.getNamedItem("ratioY");
396             if (ryAttr == null) {
397               throw new GraphMLParseException(
398                       "Missing attribute ratioY for element " +
399                       MODEL_NODE_NAME + ".");
400             } else {
401               ratioY = Double.parseDouble(ryAttr.getNodeValue());
402             }
403 
404             int sides = 0;
405             final Node sAttr = attrs.getNamedItem("sides");
406             if (sAttr == null) {
407               throw new GraphMLParseException(
408                       "Missing attribute sides for element " +
409                       MODEL_NODE_NAME + ".");
410             } else {
411               sides = stringToSides(sAttr.getNodeValue().toUpperCase());
412               if ((sides & SIDE_ALL) == 0) {
413                 throw new GraphMLParseException("Unsupported sides mask: " + sides);
414               }
415             }
416 
417             event.setResult(new Parameter(new SidePortLocationModel(sides), ratioX, ratioY));
418             break;
419           }
420         }
421       }
422     }
423 
424     private static int stringToSides( final String value ) {
425       int sides = 0;
426       for (StringTokenizer st = new StringTokenizer(value, "|"); st.hasMoreTokens();) {
427         final String token = st.nextToken().trim();
428         if ("SIDE_TOP".equals(token)) {
429           sides |= SIDE_TOP;
430         } else if ("SIDE_LEFT".equals(token)) {
431           sides |= SIDE_LEFT;
432         } else if ("SIDE_BOTTOM".equals(token)) {
433           sides |= SIDE_BOTTOM;
434         } else if ("SIDE_RIGHT".equals(token)) {
435           sides |= SIDE_RIGHT;
436         } else {
437           throw new IllegalArgumentException("Unsupported side value: " + token);
438         }
439       }
440       return sides;
441     }
442 
443     private static boolean isNamespaceElement( final Node node, final String ns ) {
444       return node.getNodeType() == Node.ELEMENT_NODE &&
445              ns.equals(node.getNamespaceURI());
446     }
447   }
448 }
449