annotate java/src/uk/ac/qmul/eecs/ccmi/gui/Edge.java @ 8:ea7885bd9bff tip

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