| SidePortLocationModel.java |
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