annotate java/src/uk/ac/qmul/eecs/ccmi/gui/Edge.java @ 1:e3935c01cde2 tip

moved license of PdPersistenceManager to the beginning of the file
author Fiore Martin <f.martin@qmul.ac.uk>
date Tue, 08 Jul 2014 19:52:03 +0100
parents 78b7fc5391a2
children
rev   line source
f@0 1 /*
f@0 2 CCmI Editor - A Collaborative Cross-Modal Diagram Editing Tool
f@0 3
f@0 4 Copyright (C) 2002 Cay S. Horstmann (http://horstmann.com)
f@0 5 Copyright (C) 2011 Queen Mary University of London (http://ccmi.eecs.qmul.ac.uk/)
f@0 6
f@0 7 This program is free software: you can redistribute it and/or modify
f@0 8 it under the terms of the GNU General Public License as published by
f@0 9 the Free Software Foundation, either version 3 of the License, or
f@0 10 (at your option) any later version.
f@0 11
f@0 12 This program is distributed in the hope that it will be useful,
f@0 13 but WITHOUT ANY WARRANTY; without even the implied warranty of
f@0 14 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
f@0 15 GNU General Public License for more details.
f@0 16
f@0 17 You should have received a copy of the GNU General Public License
f@0 18 along with this program. If not, see <http://www.gnu.org/licenses/>.
f@0 19 */
f@0 20
f@0 21 package uk.ac.qmul.eecs.ccmi.gui;
f@0 22
f@0 23 import java.awt.BasicStroke;
f@0 24 import java.awt.Color;
f@0 25 import java.awt.Graphics2D;
f@0 26 import java.awt.Shape;
f@0 27 import java.awt.geom.Line2D;
f@0 28 import java.awt.geom.Point2D;
f@0 29 import java.awt.geom.Rectangle2D;
f@0 30 import java.io.IOException;
f@0 31 import java.util.ArrayList;
f@0 32 import java.util.BitSet;
f@0 33 import java.util.LinkedHashMap;
f@0 34 import java.util.LinkedHashSet;
f@0 35 import java.util.LinkedList;
f@0 36 import java.util.List;
f@0 37 import java.util.ListIterator;
f@0 38 import java.util.Map;
f@0 39 import java.util.ResourceBundle;
f@0 40
f@0 41 import org.w3c.dom.Document;
f@0 42 import org.w3c.dom.Element;
f@0 43 import org.w3c.dom.NodeList;
f@0 44
f@0 45 import uk.ac.qmul.eecs.ccmi.diagrammodel.ConnectNodesException;
f@0 46 import uk.ac.qmul.eecs.ccmi.diagrammodel.DiagramEdge;
f@0 47 import uk.ac.qmul.eecs.ccmi.diagrammodel.DiagramNode;
f@0 48 import uk.ac.qmul.eecs.ccmi.diagrammodel.ElementChangedEvent;
f@0 49 import uk.ac.qmul.eecs.ccmi.gui.persistence.PersistenceManager;
f@0 50
f@0 51 /**
f@0 52 * An edge in a graph. Edge objects are used in a GraphPanel to render a diagram edge visually.
f@0 53 * Edge objects are used in the tree representation of the diagram as well, as they're
f@0 54 * subclasses of {@link DiagramEdge}
f@0 55 *
f@0 56 */
f@0 57 @SuppressWarnings("serial")
f@0 58 public abstract class Edge extends DiagramEdge implements GraphElement{
f@0 59
f@0 60 /**
f@0 61 * Creates a new Edge
f@0 62 *
f@0 63 * @param type the type of the edge. The type is just a string that the user assign to the edge.
f@0 64 * All the edges created via clonation from this edge will have the same type.
f@0 65 * @param availableEndDescriptions all the possible end description ends of this edge can be
f@0 66 * associated to. An end description is a text associated to a possible arrow head of an edge end.
f@0 67 * It's used to give awareness of arrow heads via speech.
f@0 68 * @param minAttachedNodes the minimum number of nodes that can be attached to this edge
f@0 69 * @param maxAttachedNodes the minimum number of nodes that can be attached to this edge
f@0 70 * @param style the style of the edge: whether it's solid, dotted or dashed
f@0 71 */
f@0 72 public Edge(String type, String[] availableEndDescriptions, int minAttachedNodes, int maxAttachedNodes,LineStyle style){
f@0 73 super(type,availableEndDescriptions);
f@0 74 this.minAttachedNodes = minAttachedNodes;
f@0 75 this.maxAttachedNodes = maxAttachedNodes;
f@0 76 this.style = style;
f@0 77 nodes = new ArrayList<Node>();
f@0 78 }
f@0 79
f@0 80 /* --- Methods inherited from DiagramEdge --- */
f@0 81 @Override
f@0 82 public Node getNodeAt(int index){
f@0 83 return nodes.get(index);
f@0 84 }
f@0 85
f@0 86 @Override
f@0 87 public int getNodesNum(){
f@0 88 return nodes.size();
f@0 89 }
f@0 90
f@0 91 @Override
f@0 92 public boolean removeNode(DiagramNode diagramNode, Object source){
f@0 93 Node n = (Node)diagramNode;
f@0 94 if(nodes.size() == 2)
f@0 95 throw new RuntimeException("Cannot remove a node from a two ends edge");
f@0 96 else{
f@0 97 for(InnerPoint p : points)
f@0 98 if(p.hasNeighbour(n)){
f@0 99 p.neighbours.remove(n);
f@0 100 if(p.neighbours.size() == 1)
f@0 101 removePoint(p);
f@0 102 break;
f@0 103 }
f@0 104 boolean nodeRemoved = nodes.remove(n);
f@0 105 /* for update in the haptic device */
f@0 106 notifyChange(new ElementChangedEvent(this,n,"remove_node",source));
f@0 107 return nodeRemoved;
f@0 108 }
f@0 109 }
f@0 110
f@0 111 @Override
f@0 112 public void connect(List<DiagramNode> nodes) throws ConnectNodesException{
f@0 113 assert(getNodesNum() == 0);
f@0 114 /* this is to eliminate duplicates */
f@0 115 LinkedHashSet<Node> nodeSet = new LinkedHashSet<Node>();
f@0 116 for(DiagramNode n : nodes)
f@0 117 nodeSet.add((Node)n);
f@0 118
f@0 119 /* checks on connection consistency */
f@0 120 if((nodeSet == null)||(nodeSet.size() < minAttachedNodes))
f@0 121 throw new ConnectNodesException("You must select at least "+ minAttachedNodes + "nodes");
f@0 122 if((nodeSet.size() > maxAttachedNodes))
f@0 123 throw new ConnectNodesException("You must select at most " + maxAttachedNodes +" nodes");
f@0 124
f@0 125 points = new ArrayList<InnerPoint>();
f@0 126 if(nodeSet.size() > 2){
f@0 127 /* there are more than three nodes. compute the central inner point *
f@0 128 * which will connect all the nodes, as the middle points of the edge bound */
f@0 129 Rectangle2D bounds = new Rectangle2D.Double();
f@0 130 for(Node n : nodeSet){
f@0 131 bounds.add(n.getBounds());
f@0 132 }
f@0 133 InnerPoint p = new InnerPoint();
f@0 134 p.translate(new Point2D.Double(0,0), bounds.getCenterX(), bounds.getCenterY(),DiagramEventSource.NONE);
f@0 135 p.neighbours.addAll(nodeSet);
f@0 136 points.add(p);
f@0 137 }
f@0 138 this.nodes.addAll(nodeSet);
f@0 139
f@0 140 if(!points.isEmpty())// points is empty when the edge has two nodes only
f@0 141 masterInnerPoint = points.get(0);
f@0 142 }
f@0 143
f@0 144 @Override
f@0 145 public abstract void draw(Graphics2D g2);
f@0 146
f@0 147 @Override
f@0 148 public void translate(Point2D p, double dx, double dy, Object source){
f@0 149 for(InnerPoint ip : points)
f@0 150 ip.translate(p, dx, dy,source);
f@0 151 }
f@0 152
f@0 153 /**
f@0 154 * To be called before {@code bend}, determines from {@code downPoint} whether
f@0 155 * a line is going to be break into two lines (with the creation of a new inner point)
f@0 156 * or if the bending is determined by an already existing inner point being translated
f@0 157 */
f@0 158 @Override
f@0 159 public void startMove(Point2D downPoint,Object source){
f@0 160 this.downPoint = downPoint;
f@0 161 newInnerPoint = null;
f@0 162 for(InnerPoint itrPoint : points)
f@0 163 if(itrPoint.contains(downPoint)){
f@0 164 /* clicked on an already existing EdgePoint */
f@0 165 newInnerPoint = itrPoint;
f@0 166 newPointCreated = false;
f@0 167 }
f@0 168 if(newInnerPoint == null){
f@0 169 /* no point under the click, create a new one */
f@0 170 newInnerPoint = new InnerPoint();
f@0 171 newInnerPoint.translate(downPoint, downPoint.getX() - newInnerPoint.getBounds().getCenterX(),
f@0 172 downPoint.getY() - newInnerPoint.getBounds().getCenterY(),DiagramEventSource.NONE);
f@0 173 newPointCreated = true;
f@0 174 /* this methods checks for segments of the edge which are aligned and makes a unique edge out of them */
f@0 175 }
f@0 176 }
f@0 177
f@0 178 /**
f@0 179 * If this edge is made out of several lines and two or more of them becomes
f@0 180 * aligned then they are blended in one single line and the inner point that was
f@0 181 * at the joint is removed.
f@0 182 *
f@0 183 * @param source the source of the {@code stopMove} action
f@0 184 */
f@0 185 @Override
f@0 186 public void stopMove(Object source){
f@0 187 for(ListIterator<InnerPoint> pItr = points.listIterator(); pItr.hasNext(); ){
f@0 188 InnerPoint ePoint = pItr.next();
f@0 189 if(ePoint.neighbours.size() > 2)
f@0 190 continue;
f@0 191 Rectangle2D startBounds = ePoint.getBounds();
f@0 192 Rectangle2D endBounds = ePoint.neighbours.get(0).getBounds();
f@0 193 Direction d1 = new Direction(endBounds.getCenterX() - startBounds.getCenterX(), endBounds.getCenterY() - startBounds.getCenterY());
f@0 194 endBounds = ePoint.neighbours.get(1).getBounds();
f@0 195 Direction d2 = new Direction(endBounds.getCenterX() - startBounds.getCenterX(), endBounds.getCenterY() - startBounds.getCenterY());
f@0 196 if(d1.isParallel(d2)){
f@0 197 InnerPoint p = null;
f@0 198 GraphElement q = null;
f@0 199 if(ePoint.neighbours.get(0) instanceof InnerPoint){
f@0 200 p = (InnerPoint)ePoint.neighbours.get(0);
f@0 201 q = ePoint.neighbours.get(1);
f@0 202 p.neighbours.add(q);
f@0 203 p.neighbours.remove(ePoint);
f@0 204 }
f@0 205 if(ePoint.neighbours.get(1) instanceof InnerPoint){
f@0 206 p = (InnerPoint)ePoint.neighbours.get(1);
f@0 207 q = ePoint.neighbours.get(0);
f@0 208 p.neighbours.add(q);
f@0 209 p.neighbours.remove(ePoint);
f@0 210 }
f@0 211 pItr.remove();
f@0 212 }
f@0 213 }
f@0 214 notifyChange(new ElementChangedEvent(this,this,"stop_move",source));
f@0 215 }
f@0 216
f@0 217 @Override
f@0 218 public abstract Rectangle2D getBounds();
f@0 219
f@0 220 @Override
f@0 221 public Point2D getConnectionPoint(Direction d){return null;}
f@0 222
f@0 223 @Override
f@0 224 public boolean contains(Point2D aPoint){
f@0 225 if(points.isEmpty()){
f@0 226 return fatStrokeContains (nodes.get(0), nodes.get(1), aPoint);
f@0 227 }
f@0 228 for(InnerPoint p : points){
f@0 229 for(GraphElement ge : p.neighbours){
f@0 230 if(fatStrokeContains(p,ge,aPoint))
f@0 231 return true;
f@0 232 }
f@0 233 }
f@0 234 return false;
f@0 235 }
f@0 236
f@0 237 /**
f@0 238 * Look for the node attached to this edge which lays at the minimum distance
f@0 239 * from the point passed as argument. The distance cannot be lower than the
f@0 240 * value passed as argument.
f@0 241 *
f@0 242 * @param aPoint the point the distance is measured from
f@0 243 * @param distanceLimit the limit from the distance between the nodes and the point
f@0 244 * @return the closest node or null if the node lays at an higher distance than distanceLimit
f@0 245 */
f@0 246 public Node getClosestNode(Point2D aPoint, double distanceLimit){
f@0 247 Node closestNode = null;
f@0 248 double minDist = distanceLimit;
f@0 249
f@0 250 if(points.isEmpty()){
f@0 251 Line2D line = getSegment(nodes.get(0),nodes.get(1));
f@0 252 if(line.getP1().distance(aPoint) < minDist){
f@0 253 minDist = line.getP1().distance(aPoint);
f@0 254 closestNode = nodes.get(0);
f@0 255 }
f@0 256 if(line.getP2().distance(aPoint) < minDist){
f@0 257 minDist = line.getP2().distance(aPoint);
f@0 258 closestNode = nodes.get(1);
f@0 259 }
f@0 260 return closestNode;
f@0 261 }else{
f@0 262 for(InnerPoint p : points){
f@0 263 for(GraphElement ge : p.getNeighbours())
f@0 264 if(ge instanceof Node){
f@0 265 Node n = (Node)ge;
f@0 266 Direction d = new Direction(p.getBounds().getCenterX() - n.getBounds().getCenterX(),p.getBounds().getCenterY() - n.getBounds().getCenterY());
f@0 267 if(n.getConnectionPoint(d).distance(aPoint) < minDist){
f@0 268 minDist = n.getConnectionPoint(d).distance(aPoint);
f@0 269 closestNode = n;
f@0 270 }
f@0 271 }
f@0 272 }
f@0 273 return closestNode;
f@0 274 }
f@0 275 }
f@0 276
f@0 277 private void removePoint(InnerPoint p){
f@0 278 /* we assume at this moment p has one neighbour only */
f@0 279 InnerPoint neighbour = (InnerPoint)p.neighbours.get(0);
f@0 280 points.remove(p);
f@0 281 neighbour.neighbours.remove(p);
f@0 282 if(neighbour.neighbours.size() == 1)
f@0 283 removePoint(neighbour);
f@0 284 }
f@0 285
f@0 286 /* checks if a point belongs to a shape with a margin of MAX_DIST*/
f@0 287 private boolean fatStrokeContains(GraphElement ge1, GraphElement ge2, Point2D p){
f@0 288 BasicStroke fatStroke = new BasicStroke((float) (2 * MAX_DIST));
f@0 289 Line2D line = new Line2D.Double(
f@0 290 ge1.getBounds().getCenterX(),
f@0 291 ge1.getBounds().getCenterY(),
f@0 292 ge2.getBounds().getCenterX(),
f@0 293 ge2.getBounds().getCenterY());
f@0 294 Shape fatPath = fatStroke.createStrokedShape(line);
f@0 295 return fatPath.contains(p);
f@0 296 }
f@0 297
f@0 298 /**
f@0 299 * Returns a line connecting the centre of the graph elements passed as argument.
f@0 300 *
f@0 301 * @param start the first graph element
f@0 302 * @param end the second graph element
f@0 303 *
f@0 304 * @return a line connecting {@code start} and {@code end}
f@0 305 */
f@0 306 protected Line2D.Double getSegment(GraphElement start, GraphElement end){
f@0 307 Rectangle2D startBounds = start.getBounds();
f@0 308 Rectangle2D endBounds = end.getBounds();
f@0 309 Direction d = new Direction(endBounds.getCenterX() - startBounds.getCenterX(), endBounds.getCenterY() - startBounds.getCenterY());
f@0 310 return new Line2D.Double(start.getConnectionPoint(d), end.getConnectionPoint(d.turn(180)));
f@0 311 }
f@0 312
f@0 313 /**
f@0 314 * Returns a list of the points where this edge and the nodes it connects come to a contact
f@0 315 *
f@0 316 * @return a list of points
f@0 317 */
f@0 318 public List<Point2D> getConnectionPoints(){
f@0 319 List<Point2D> list = new LinkedList<Point2D>();
f@0 320 if(points.isEmpty()){
f@0 321 Line2D line = getSegment(nodes.get(0),nodes.get(1));
f@0 322 list.add(line.getP1());
f@0 323 list.add(line.getP2());
f@0 324 }else{
f@0 325 for(InnerPoint p : points){
f@0 326 for(GraphElement ge : p.neighbours)
f@0 327 if(ge instanceof Node){
f@0 328 Direction d = new Direction(p.getBounds().getCenterX() - ge.getBounds().getCenterX(),p.getBounds().getCenterY() - ge.getBounds().getCenterY());
f@0 329 list.add(((Node)ge).getConnectionPoint(d));
f@0 330 }
f@0 331 }
f@0 332 }
f@0 333 return list;
f@0 334 }
f@0 335
f@0 336 /**
f@0 337 * Gets the stipple pattern of this edge line style
f@0 338 *
f@0 339 * @see LineStyle#getStipplePattern()
f@0 340 *
f@0 341 * @return an int representing the stipple pattern of this edge
f@0 342 */
f@0 343 public int getStipplePattern(){
f@0 344 return getStyle().getStipplePattern();
f@0 345 }
f@0 346
f@0 347 /**
f@0 348 * Bends one of the lines forming this edge.
f@0 349 *
f@0 350 * When an line is bent, if the location where the bending happens is a line then a new
f@0 351 * inner point is created and the line is broken into two sub lines. If the location is an
f@0 352 * already existing inner point, then the point is translated.
f@0 353 *
f@0 354 * @param p the starting point of the bending
f@0 355 * @param source the source of the bending action
f@0 356 */
f@0 357 public void bend(Point2D p,Object source) {
f@0 358 boolean found = false;
f@0 359 if(points.isEmpty()){
f@0 360 newInnerPoint.neighbours.addAll(nodes);
f@0 361 points.add(newInnerPoint);
f@0 362 newPointCreated = false;
f@0 363 }else if(newPointCreated){
f@0 364 /* find the segment closest to where the new point lays */
f@0 365 InnerPoint closestP1 = null;
f@0 366 GraphElement closestP2 = null;
f@0 367 double minDist = 0;
f@0 368 for(ListIterator<InnerPoint> pItr = points.listIterator(); pItr.hasNext(); ){
f@0 369 InnerPoint ePoint = pItr.next();
f@0 370 for(ListIterator<GraphElement> geItr = ePoint.neighbours.listIterator(); geItr.hasNext() && !found;){
f@0 371 /* find the neighbour of the current edge point whose line the new point lays on */
f@0 372 GraphElement next = geItr.next();
f@0 373 double dist = Line2D.ptSegDist(
f@0 374 ePoint.getBounds().getCenterX(),
f@0 375 ePoint.getBounds().getCenterY(),
f@0 376 next.getBounds().getCenterX(),
f@0 377 next.getBounds().getCenterY(),
f@0 378 downPoint.getX(),
f@0 379 downPoint.getY()
f@0 380 );
f@0 381
f@0 382 if(closestP1 == null || dist < minDist){
f@0 383 closestP1 = ePoint;
f@0 384 closestP2 = next;
f@0 385 minDist = dist;
f@0 386 continue;
f@0 387 }
f@0 388 }
f@0 389 }
f@0 390
f@0 391 if(closestP2 instanceof InnerPoint ){
f@0 392 /* remove current edge point from the neighbour's neighbours */
f@0 393 ((InnerPoint)closestP2).neighbours.remove(closestP1);
f@0 394 /* add the new inner point */
f@0 395 ((InnerPoint)closestP2).neighbours.add(newInnerPoint);
f@0 396 }
f@0 397 /*remove old neighbour from edge inner point neighbours */
f@0 398 closestP1.neighbours.remove(closestP2);
f@0 399 newInnerPoint.neighbours.add(closestP1);
f@0 400 newInnerPoint.neighbours.add(closestP2);
f@0 401 /* add the new node to the list of EdgeNodes of this edge */
f@0 402 points.add(newInnerPoint);
f@0 403 closestP1.neighbours.add(newInnerPoint);
f@0 404 found = true;
f@0 405 newPointCreated = false;
f@0 406 }
f@0 407 newInnerPoint.translate(p, p.getX() - newInnerPoint.getBounds().getCenterX(),
f@0 408 p.getY() - newInnerPoint.getBounds().getCenterY(),DiagramEventSource.NONE);
f@0 409 notifyChange(new ElementChangedEvent(this,this,"bend",source));
f@0 410 }
f@0 411
f@0 412 /**
f@0 413 * Returns the line where the edge name will be painted. If the edge is only made out
f@0 414 * of a straight line then this will be returned.
f@0 415 *
f@0 416 * If the edge has been broken into several segments (by bending it) then the central
f@0 417 * line is returned. If the edge connects more than two nodes then a line (not necessarily
f@0 418 * matching the edge) that has the central point in its centre is returned. Note that a
f@0 419 * edge connecting more than two nodes is painted as a central point connected to all the nodes.
f@0 420 *
f@0 421 * @return the line where the name will be painted
f@0 422 */
f@0 423 public Line2D getNameLine(){
f@0 424 if(points.isEmpty()){ // straight line
f@0 425 return getSegment(nodes.get(0),nodes.get(1));
f@0 426 }else{
f@0 427 if(masterInnerPoint != null){/* multiended edge */
f@0 428 Rectangle2D bounds = masterInnerPoint.getBounds();
f@0 429 Point2D p = new Point2D.Double(bounds.getCenterX() - 1,bounds.getCenterY());
f@0 430 Point2D q = new Point2D.Double(bounds.getCenterX() + 1,bounds.getCenterY());
f@0 431 return new Line2D.Double(p, q);
f@0 432 }else{
f@0 433 GraphElement ge1 = nodes.get(0);
f@0 434 GraphElement ge2 = nodes.get(1);
f@0 435 InnerPoint c1 = null;
f@0 436 InnerPoint c2 = null;
f@0 437
f@0 438 for(InnerPoint innp : points){
f@0 439 if(innp.getNeighbours().contains(ge1)){
f@0 440 c1 = innp;
f@0 441 }
f@0 442 if(innp.getNeighbours().contains(ge2)){
f@0 443 c2 = innp;
f@0 444 }
f@0 445 }
f@0 446
f@0 447 /* we only have two nodes but the edge has been bended */
f@0 448 while((c1 != c2)&&(!c2.getNeighbours().contains(c1))){
f@0 449 if(c1.getNeighbours().get(0) == ge1){
f@0 450 ge1 = c1;
f@0 451 c1 = (InnerPoint)c1.getNeighbours().get(1);
f@0 452 }
f@0 453 else{
f@0 454 ge1 = c1;
f@0 455 c1 = (InnerPoint)c1.getNeighbours().get(0);
f@0 456 }
f@0 457 if(c2.getNeighbours().get(0) == ge2){
f@0 458 ge2 = c2;
f@0 459 c2 = (InnerPoint)c2.getNeighbours().get(1);
f@0 460 }
f@0 461 else{
f@0 462 ge2 = c2;
f@0 463 c2 = (InnerPoint)c2.getNeighbours().get(0);
f@0 464 }
f@0 465 }
f@0 466
f@0 467 Point2D p = new Point2D.Double();
f@0 468 Point2D q = new Point2D.Double();
f@0 469 if(c1 == c2){
f@0 470 Rectangle2D bounds = c1.getBounds();
f@0 471 p.setLocation( bounds.getCenterX() - 1,bounds.getCenterY());
f@0 472 q.setLocation( bounds.getCenterX() + 1,bounds.getCenterY());
f@0 473 }else{
f@0 474 Rectangle2D bounds = c1.getBounds();
f@0 475 p.setLocation( bounds.getCenterX(),bounds.getCenterY());
f@0 476 bounds = c2.getBounds();
f@0 477 q.setLocation(bounds.getCenterX(),bounds.getCenterY());
f@0 478
f@0 479 }
f@0 480 return new Line2D.Double(p,q);
f@0 481 }
f@0 482 }
f@0 483 }
f@0 484
f@0 485 /**
f@0 486 * Encodes all the relevant data of this object in XML format.
f@0 487 *
f@0 488 * @param doc an XML document
f@0 489 * @param parent the parent XML element, where tag about this edge will be inserted
f@0 490 * @param nodes a list of all nodes of the diagram
f@0 491 */
f@0 492 public void encode(Document doc, Element parent, List<Node> nodes){
f@0 493 parent.setAttribute(PersistenceManager.TYPE,getType());
f@0 494 parent.setAttribute(PersistenceManager.NAME, getName());
f@0 495 parent.setAttribute(PersistenceManager.ID, String.valueOf(getId()));
f@0 496
f@0 497 int numNodes = getNodesNum();
f@0 498 if(numNodes > 0){
f@0 499 Element nodesTag = doc.createElement(PersistenceManager.NODES);
f@0 500 parent.appendChild(nodesTag);
f@0 501 for(int i=0; i<numNodes;i++){
f@0 502 Element nodeTag = doc.createElement(PersistenceManager.NODE);
f@0 503 nodeTag.setAttribute(PersistenceManager.ID, String.valueOf(getNodeAt(i).getId()));
f@0 504 nodeTag.setAttribute(PersistenceManager.LABEL, getEndLabel(getNodeAt(i)));
f@0 505 nodesTag.appendChild(nodeTag);
f@0 506 }
f@0 507 }
f@0 508
f@0 509 if(!points.isEmpty()){
f@0 510 Element pointsTag = doc.createElement(PersistenceManager.POINTS);
f@0 511 parent.appendChild(pointsTag);
f@0 512 for(InnerPoint point : points){
f@0 513 Element pointTag = doc.createElement(PersistenceManager.POINT);
f@0 514 pointsTag.appendChild(pointTag);
f@0 515 pointTag.setAttribute(PersistenceManager.ID, String.valueOf(-(points.indexOf(point)+1)));
f@0 516
f@0 517 Element positionTag = doc.createElement(PersistenceManager.POSITION);
f@0 518 pointTag.appendChild(positionTag);
f@0 519 Rectangle2D bounds = point.getBounds();
f@0 520 positionTag.setAttribute(PersistenceManager.X,String.valueOf(bounds.getX()));
f@0 521 positionTag.setAttribute(PersistenceManager.Y,String.valueOf(bounds.getY()));
f@0 522
f@0 523 Element neighboursTag = doc.createElement(PersistenceManager.NEIGHBOURS);
f@0 524 pointTag.appendChild(neighboursTag);
f@0 525 StringBuilder builder = new StringBuilder();
f@0 526 for(GraphElement ge : point.getNeighbours()){
f@0 527 if(ge instanceof Node){
f@0 528 builder.append(((Node)ge).getId());
f@0 529 }else{
f@0 530 builder.append(-(points.indexOf(ge)+1));
f@0 531 }
f@0 532 builder.append(" ");
f@0 533 }
f@0 534 builder.deleteCharAt(builder.length()-1);
f@0 535 neighboursTag.setTextContent(builder.toString());
f@0 536 }
f@0 537 }
f@0 538 }
f@0 539
f@0 540
f@0 541 /**
f@0 542 * Decodes an edge from the XML representation.
f@0 543 *
f@0 544 * @see #encode(Document, Element, List)
f@0 545 *
f@0 546 * @param doc an XML document
f@0 547 * @param edgeTag the tag in the XML file related to this edge
f@0 548 * @param nodesId a map linking node ids in the XML file to the {@code Node} objects they represent
f@0 549 * @throws IOException if something goes wrong while reading the XML file
f@0 550 */
f@0 551 public void decode(Document doc, Element edgeTag, Map<String,Node> nodesId) throws IOException{
f@0 552 setName(edgeTag.getAttribute(PersistenceManager.NAME),DiagramEventSource.PERS);
f@0 553 if(getName().isEmpty())
f@0 554 throw new IOException();
f@0 555 try{
f@0 556 setId(Integer.parseInt(edgeTag.getAttribute(PersistenceManager.ID)));
f@0 557 }catch(NumberFormatException nfe){
f@0 558 throw new IOException(nfe);
f@0 559 }
f@0 560
f@0 561 NodeList nodeList = edgeTag.getElementsByTagName(PersistenceManager.NODE);
f@0 562 List<DiagramNode> attachedNodes = new ArrayList<DiagramNode>(nodeList.getLength());
f@0 563 List<String> labels = new ArrayList<String>(nodeList.getLength());
f@0 564 for(int i=0; i<nodeList.getLength();i++){
f@0 565 String id = ((Element)nodeList.item(i)).getAttribute(PersistenceManager.ID);
f@0 566 if(!nodesId.containsKey(id))
f@0 567 throw new IOException();
f@0 568 attachedNodes.add(nodesId.get(id));
f@0 569 labels.add(((Element)nodeList.item(i)).getAttribute(PersistenceManager.LABEL));
f@0 570 }
f@0 571
f@0 572 try {
f@0 573 connect(attachedNodes);
f@0 574 } catch (ConnectNodesException e) {
f@0 575 throw new IOException(e);
f@0 576 }
f@0 577
f@0 578 for(int i=0; i < labels.size(); i++){
f@0 579 setEndLabel(attachedNodes.get(i), labels.get(i),DiagramEventSource.PERS);
f@0 580 }
f@0 581
f@0 582 Map<String, InnerPoint> pointsId = new LinkedHashMap<String, InnerPoint>();
f@0 583 NodeList pointTagList = edgeTag.getElementsByTagName(PersistenceManager.POINT);
f@0 584
f@0 585 for(int i=0; i<pointTagList.getLength(); i++){
f@0 586 InnerPoint point = new InnerPoint();
f@0 587 Element pointTag = (Element)pointTagList.item(i);
f@0 588 String id = pointTag.getAttribute(PersistenceManager.ID);
f@0 589 /* id of property nodes must be a negative value */
f@0 590 try{
f@0 591 if(Integer.parseInt(id) >= 0)
f@0 592 throw new IOException();
f@0 593 }catch(NumberFormatException nfe){
f@0 594 throw new IOException(nfe);
f@0 595 }
f@0 596
f@0 597 pointsId.put(id, point);
f@0 598
f@0 599 if(pointTag.getElementsByTagName(PersistenceManager.POSITION).item(0) == null)
f@0 600 throw new IOException();
f@0 601 Element pointPositionTag = (Element)pointTag.getElementsByTagName(PersistenceManager.POSITION).item(0);
f@0 602 double dx = 0,dy = 0;
f@0 603 try{
f@0 604 dx = Double.parseDouble(pointPositionTag.getAttribute(PersistenceManager.X));
f@0 605 dy = Double.parseDouble(pointPositionTag.getAttribute(PersistenceManager.Y));
f@0 606 }catch(NumberFormatException nfe){
f@0 607 throw new IOException();
f@0 608 }
f@0 609 point.translate(new Point2D.Double(), dx, dy,DiagramEventSource.PERS);
f@0 610 }
f@0 611
f@0 612 /* remove the master inner point eventually created by connect */
f@0 613 /* we're going to replace it with the one in the XML file */
f@0 614 points.clear();
f@0 615 /* re do the cycle when all the points id have been Map-ped */
f@0 616 for(int i=0; i<pointTagList.getLength(); i++){
f@0 617 Element pointTag = (Element)pointTagList.item(i);
f@0 618 InnerPoint point = pointsId.get(pointTag.getAttribute(PersistenceManager.ID));
f@0 619
f@0 620 if(pointTag.getElementsByTagName(PersistenceManager.NEIGHBOURS).item(0) == null)
f@0 621 throw new IOException();
f@0 622 Element pointNeighboursTag = (Element)pointTag.getElementsByTagName(PersistenceManager.NEIGHBOURS).item(0);
f@0 623 String pointNeighboursTagContent = pointNeighboursTag.getTextContent();
f@0 624 String[] neighboursId = pointNeighboursTagContent.split(" ");
f@0 625
f@0 626 for(String neighbourId : neighboursId){
f@0 627 GraphElement ge = nodesId.get(neighbourId);
f@0 628 if(ge == null) // it ain't a node
f@0 629 ge = pointsId.get(neighbourId);
f@0 630 if(ge == null)
f@0 631 throw new IOException();
f@0 632 point.neighbours.add(ge);
f@0 633 }
f@0 634 points.add(point);
f@0 635 if(i==0)
f@0 636 masterInnerPoint = point;
f@0 637 }
f@0 638 }
f@0 639
f@0 640 /**
f@0 641 * Returns the minimum number of nodes that edge of this type can connect
f@0 642 *
f@0 643 * @return the minimum nodes for edges of this type
f@0 644 */
f@0 645 public int getMinAttachedNodes(){
f@0 646 return minAttachedNodes;
f@0 647 }
f@0 648
f@0 649 /**
f@0 650 * Returns the maximum number of nodes that edge of this type can connect
f@0 651 *
f@0 652 * @return the maximum nodes for edges of this type
f@0 653 */
f@0 654 public int getMaxAttachedNodes(){
f@0 655 return maxAttachedNodes;
f@0 656 }
f@0 657
f@0 658 /**
f@0 659 * Return the line style of this edge
f@0 660 *
f@0 661 * @return the line style of this edge
f@0 662 */
f@0 663 public LineStyle getStyle(){
f@0 664 return style;
f@0 665 }
f@0 666
f@0 667 protected Point2D downPoint;
f@0 668 private List<Node> nodes;
f@0 669
f@0 670 /* list containing the vertex of the edge which are not nodes */
f@0 671 /**
f@0 672 * The list of the inner points of this edge
f@0 673 */
f@0 674 protected List<InnerPoint> points;
f@0 675 /**
f@0 676 * For edges connecting more than two nodes, this is the central inner point where
f@0 677 * all the lines from the nodes join
f@0 678 */
f@0 679 protected InnerPoint masterInnerPoint;
f@0 680
f@0 681 private boolean newPointCreated;
f@0 682 private InnerPoint newInnerPoint;
f@0 683 private int minAttachedNodes;
f@0 684 private int maxAttachedNodes;
f@0 685
f@0 686 private LineStyle style;
f@0 687
f@0 688
f@0 689 private static final double MAX_DIST = 5;
f@0 690 private static final Color POINT_COLOR = Color.GRAY;
f@0 691 /**
f@0 692 * The end description for an end that has no hand description set by the user
f@0 693 */
f@0 694 public static final String NO_ENDDESCRIPTION_STRING = ResourceBundle.getBundle(EditorFrame.class.getName()).getString("no_arrow_string");
f@0 695
f@0 696
f@0 697 /**
f@0 698 * When an edge's (straight) line is bent it breaks into two sub lines. At the point where this two
f@0 699 * sub lines join a square shaped point is painted. This class represent that point. Objects of this class
f@0 700 * are graph elements and the user can click on them and translate them along the graph.
f@0 701 *
f@0 702 */
f@0 703 protected static class InnerPoint implements GraphElement{
f@0 704 /**
f@0 705 * Creates a new inner point
f@0 706 */
f@0 707 public InnerPoint(){
f@0 708 bounds = new Rectangle2D.Double(0,0,DIM,DIM);
f@0 709 neighbours = new LinkedList<GraphElement>();
f@0 710 }
f@0 711
f@0 712 @Override
f@0 713 public void startMove(Point2D p, Object source){}
f@0 714
f@0 715 @Override
f@0 716 public void stopMove(Object source){}
f@0 717
f@0 718 @Override
f@0 719 public void draw(Graphics2D g2){
f@0 720 Color oldColor = g2.getColor();
f@0 721 g2.setColor(POINT_COLOR);
f@0 722 g2.fill(bounds);
f@0 723 g2.setColor(oldColor);
f@0 724 }
f@0 725
f@0 726 @Override
f@0 727 public boolean contains(Point2D p){
f@0 728 return bounds.contains(p);
f@0 729 }
f@0 730
f@0 731 @Override
f@0 732 public Point2D getConnectionPoint(Direction d){
f@0 733 return new Point2D.Double(bounds.getCenterX(),bounds.getCenterY());
f@0 734 }
f@0 735
f@0 736 @Override
f@0 737 public void translate(Point2D p, double dx, double dy, Object source){
f@0 738 bounds.setFrame(bounds.getX() + dx,
f@0 739 bounds.getY() + dy,
f@0 740 bounds.getWidth(),
f@0 741 bounds.getHeight());
f@0 742 }
f@0 743
f@0 744 @Override
f@0 745 public Rectangle2D getBounds(){
f@0 746 return (Rectangle2D)bounds.clone();
f@0 747 }
f@0 748
f@0 749 /**
f@0 750 * Neighbours are the graph elements (either nodes or other inner points) this inner point is
f@0 751 * directly connected to. Directly connected means there is a straight line from this node
f@0 752 * and the neighbour.
f@0 753 *
f@0 754 * @return a list of neighbours of this inner node
f@0 755 */
f@0 756 public List<GraphElement> getNeighbours(){
f@0 757 return neighbours;
f@0 758 }
f@0 759
f@0 760 /**
f@0 761 * Returns true if this inner node and {@code ge} are neighbours.
f@0 762 *
f@0 763 * @see #getNeighbours()
f@0 764 *
f@0 765 * @param ge the graph element to be tested
f@0 766 * @return {@code true} if {@code ge} is a neighbour of this graph element, {@code false} otherwise
f@0 767 *
f@0 768 */
f@0 769 public boolean hasNeighbour(GraphElement ge){
f@0 770 return neighbours.contains(ge);
f@0 771 }
f@0 772
f@0 773 @Override
f@0 774 public String toString(){
f@0 775 return "EdgePoint: "+bounds.getCenterX()+"-"+bounds.getCenterY();
f@0 776 }
f@0 777
f@0 778 private Rectangle2D bounds;
f@0 779 private List<GraphElement> neighbours;
f@0 780 private static final int DIM = 7;
f@0 781 }
f@0 782
f@0 783 /**
f@0 784 * A representation of this edge as a set of 2D points.
f@0 785 * Every node the edge connects and every inner point are represented as a pair with coordinates
f@0 786 * of their centre. Furthermore an adjacency matrix holds the information
f@0 787 * about which point is connected to which is. This representation of the edge is
f@0 788 * used in the haptic space, being more suitable for haptic devices.
f@0 789 *
f@0 790 */
f@0 791 public static class PointRepresentation {
f@0 792 public PointRepresentation(int size){
f@0 793 xs = new double[size];
f@0 794 ys = new double[size];
f@0 795 adjMatrix = new BitSet[size];
f@0 796 for(int i=0; i<size; i++){
f@0 797 adjMatrix[i] = new BitSet(size);
f@0 798 }
f@0 799 }
f@0 800 /**
f@0 801 * An array with all the x coordinate of the edge's points (nodes and inner points)
f@0 802 */
f@0 803 public double xs[];
f@0 804 /**
f@0 805 * An array with all the y coordinate of the edge's points (nodes and inner points)
f@0 806 */
f@0 807 public double ys[];
f@0 808 /**
f@0 809 * The adjacency matrix. If the i-th bit of {@code adjMatrix[j]} is set to {@code true}
f@0 810 * it means there is a direct line connecting the i-th point (coordinates {@code (xs[i],ys[i])}
f@0 811 * to the j-th point (coordinates {@code (xs[j],ys[j])}. Note that connection are represented only
f@0 812 * once to avoid double paintings by the haptic engine. So if the i-th bit of {@code adjMatrix[j]}
f@0 813 * is set to {@code true}, the j-th bit of {@code adjMatrix[i]} and information redundancy is avoided.
f@0 814 */
f@0 815 public BitSet adjMatrix[];
f@0 816 /**
f@0 817 * The index of the beginning of the nodes (after inner points) in {@code adjMatrix}.
f@0 818 * So if the edge has three nodes and two inner points. The inner points
f@0 819 * will be at {@code adjMatrix[0]} and {@code adjMatrix[1]} and the nodes at the following indexes.
f@0 820 * In this case {@code nodeStart} is equal to 2.
f@0 821 */
f@0 822 public int nodeStart;
f@0 823 }
f@0 824
f@0 825 /**
f@0 826 * Returns a new {@code PointRepresentation} of this edge
f@0 827 *
f@0 828 * @return a new {@code PointRepresentation} of this edge
f@0 829 */
f@0 830 public PointRepresentation getPointRepresentation(){
f@0 831 PointRepresentation pr = new PointRepresentation(points.size()+nodes.size());
f@0 832 if(points.isEmpty()){ // two ended edge
f@0 833 pr.xs[0] = nodes.get(0).getBounds().getCenterX();
f@0 834 pr.ys[0] = nodes.get(0).getBounds().getCenterY();
f@0 835 pr.xs[1] = nodes.get(1).getBounds().getCenterX();
f@0 836 pr.ys[1] = nodes.get(1).getBounds().getCenterY();
f@0 837 // we only need one edge, else it would be painted twice
f@0 838 pr.adjMatrix[0].set(1);
f@0 839 pr.nodeStart = 0;
f@0 840 }else{
f@0 841 //[ point 1, point 2, point 3, ... , point n, node, 1 node 2, ... , node n ]
f@0 842 int pSize = points.size();
f@0 843 pr.nodeStart = pSize; // the first node starts after the points
f@0 844 for(int i=0; i<pSize;i++){
f@0 845 pr.xs[i] = points.get(i).getBounds().getCenterX();
f@0 846 pr.ys[i] = points.get(i).getBounds().getCenterY();
f@0 847 for(GraphElement ge : points.get(i).neighbours){
f@0 848 if(ge instanceof InnerPoint)
f@0 849 pr.adjMatrix[i].set(points.indexOf(ge));
f@0 850 else //Node
f@0 851 pr.adjMatrix[i].set(pSize+nodes.indexOf(ge));
f@0 852 }
f@0 853 }
f@0 854 /* set the coordinates of the nodes, no adj matrix needed as the inner points are enough */
f@0 855 for(int i=0 ; i<nodes.size(); i++){
f@0 856 pr.xs[pSize+i] = nodes.get(i).getBounds().getCenterX();
f@0 857 pr.ys[pSize+i] = nodes.get(i).getBounds().getCenterY();
f@0 858 }
f@0 859 }
f@0 860 return pr;
f@0 861 }
f@0 862
f@0 863 }
f@0 864