Mercurial > hg > ccmieditor
view java/src/uk/ac/qmul/eecs/ccmi/gui/Edge.java @ 0:9418ab7b7f3f
Initial import
author | Fiore Martin <fiore@eecs.qmul.ac.uk> |
---|---|
date | Fri, 16 Dec 2011 17:35:51 +0000 |
parents | |
children | 9e67171477bc |
line wrap: on
line source
/* CCmI Editor - A Collaborative Cross-Modal Diagram Editing Tool Copyright (C) 2002 Cay S. Horstmann (http://horstmann.com) Copyright (C) 2011 Queen Mary University of London (http://ccmi.eecs.qmul.ac.uk/) This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see <http://www.gnu.org/licenses/>. */ package uk.ac.qmul.eecs.ccmi.gui; import java.awt.BasicStroke; import java.awt.Color; import java.awt.Graphics2D; import java.awt.Shape; import java.awt.geom.Line2D; import java.awt.geom.Point2D; import java.awt.geom.Rectangle2D; import java.io.IOException; import java.util.ArrayList; import java.util.BitSet; import java.util.LinkedHashMap; import java.util.LinkedHashSet; import java.util.LinkedList; import java.util.List; import java.util.ListIterator; import java.util.Map; import java.util.ResourceBundle; import org.w3c.dom.Document; import org.w3c.dom.Element; import org.w3c.dom.NodeList; import uk.ac.qmul.eecs.ccmi.diagrammodel.ConnectNodesException; import uk.ac.qmul.eecs.ccmi.diagrammodel.DiagramEdge; import uk.ac.qmul.eecs.ccmi.diagrammodel.DiagramNode; import uk.ac.qmul.eecs.ccmi.diagrammodel.ElementChangedEvent; import uk.ac.qmul.eecs.ccmi.gui.persistence.PersistenceManager; /** * An edge in a graph. Edge objects are used in a GraphPanel to render a diagram edge visually. * Edge objects are used in the tree representation of the diagram as well, as they're * subclasses of {@link DiagramEdge} * */ @SuppressWarnings("serial") public abstract class Edge extends DiagramEdge implements GraphElement{ public Edge(String type, String[] availableEndDescriptions, int minAttachedNodes, int maxAttachedNodes,LineStyle style){ super(type,availableEndDescriptions); this.minAttachedNodes = minAttachedNodes; this.maxAttachedNodes = maxAttachedNodes; this.style = style; nodes = new ArrayList<Node>(); } @Override public Node getNodeAt(int index){ return nodes.get(index); } @Override public int getNodesNum(){ return nodes.size(); } public boolean removeNode(DiagramNode diagramNode){ Node n = (Node)diagramNode; if(nodes.size() == 2) throw new RuntimeException("Cannot remove a node from a two ends edge"); else{ for(InnerPoint p : points) if(p.hasNeighbour(n)){ p.neighbours.remove(n); if(p.neighbours.size() == 1) removePoint(p); break; } return nodes.remove(n); } } /** * Look for the node attached to this edge which lays at the minimum distance * from the point passed as argument. The distance cannot be lower than the * value passed as argument. * * @param aPoint the point the distance is measured from * @param distanceLimit the limit from the distance between the nodes and the point * @return the closest node or null if the node lays at an higher distance than distanceLimit */ public Node getClosestNode(Point2D aPoint, double distanceLimit){ Node closestNode = null; double minDist = distanceLimit; if(points.isEmpty()){ Line2D line = getSegment(nodes.get(0),nodes.get(1)); if(line.getP1().distance(aPoint) < minDist){ minDist = line.getP1().distance(aPoint); closestNode = nodes.get(0); } if(line.getP2().distance(aPoint) < minDist){ minDist = line.getP2().distance(aPoint); closestNode = nodes.get(1); } return closestNode; }else{ for(InnerPoint p : points){ for(GraphElement ge : p.getNeighbours()) if(ge instanceof Node){ Node n = (Node)ge; Direction d = new Direction(p.getBounds().getCenterX() - n.getBounds().getCenterX(),p.getBounds().getCenterY() - n.getBounds().getCenterY()); if(n.getConnectionPoint(d).distance(aPoint) < minDist){ minDist = n.getConnectionPoint(d).distance(aPoint); closestNode = n; } } } return closestNode; } } //------------------------ private void removePoint(InnerPoint p){ /* we assume at this moment p has one neighbours only */ InnerPoint neighbour = (InnerPoint)p.neighbours.get(0); points.remove(p); neighbour.neighbours.remove(p); if(neighbour.neighbours.size() == 1) removePoint(neighbour); } /** Draw the edge. @param g2 the graphics context */ public abstract void draw(Graphics2D g2); /** Tests whether the edge contains a point. @param aPoint the point to test @return true if this edge contains aPoint */ public boolean contains(Point2D aPoint){ if(points.isEmpty()){ return fatStrokeContains (getSegment(nodes.get(0), nodes.get(1)), aPoint); } for(InnerPoint p : points){ for(GraphElement ge : p.neighbours){ if(fatStrokeContains(getSegment(p,ge),aPoint)) return true; } } return false; } private boolean fatStrokeContains(Shape s, Point2D p){ BasicStroke fatStroke = new BasicStroke((float) (2 * MAX_DIST)); Shape fatPath = fatStroke.createStrokedShape(s); return fatPath.contains(p); } protected Line2D.Double getSegment(GraphElement start, GraphElement end){ Rectangle2D startBounds = start.getBounds(); Rectangle2D endBounds = end.getBounds(); Direction d = new Direction(endBounds.getCenterX() - startBounds.getCenterX(), endBounds.getCenterY() - startBounds.getCenterY()); return new Line2D.Double(start.getConnectionPoint(d), end.getConnectionPoint(d.turn(180))); } public List<Point2D> getConnectionPoints(){ List<Point2D> list = new LinkedList<Point2D>(); if(points.isEmpty()){ Line2D line = getSegment(nodes.get(0),nodes.get(1)); list.add(line.getP1()); list.add(line.getP2()); }else{ for(InnerPoint p : points){ for(GraphElement ge : p.neighbours) if(ge instanceof Node){ Direction d = new Direction(p.getBounds().getCenterX() - ge.getBounds().getCenterX(),p.getBounds().getCenterY() - ge.getBounds().getCenterY()); list.add(((Node)ge).getConnectionPoint(d)); } } } return list; } public abstract int getStipplePattern(); /** Connect this edge to the nodes. @param aStart the starting node @param anEnd the ending node */ public void connect(List<DiagramNode> nodes) throws ConnectNodesException{ assert(getNodesNum() == 0); /* this is to eliminate duplicates */ LinkedHashSet<Node> nodeSet = new LinkedHashSet<Node>(); for(DiagramNode n : nodes) nodeSet.add((Node)n); /* checks on connection consistency */ if((nodeSet == null)||(nodeSet.size() < minAttachedNodes)) throw new ConnectNodesException("You must select at least "+ minAttachedNodes + "nodes"); if((nodeSet.size() > maxAttachedNodes)) throw new ConnectNodesException("You must select at most " + maxAttachedNodes +" nodes"); points = new ArrayList<InnerPoint>(); if(nodeSet.size() > 2){ /* there are more than three nodes. compute the central inner point * * which will connect all the nodes, as the middle points of the edge bound */ Rectangle2D bounds = new Rectangle2D.Double(); for(Node n : nodeSet){ bounds.add(n.getBounds()); } InnerPoint p = new InnerPoint(); p.translate(new Point2D.Double(0,0), bounds.getCenterX(), bounds.getCenterY()); p.neighbours.addAll(nodeSet); points.add(p); } this.nodes.addAll(nodeSet); if(!points.isEmpty()) masterInnerPoint = points.get(0); } public void bend(Point2D p) { boolean found = false; if(points.isEmpty()){ newInnerPoint.neighbours.addAll(nodes); points.add(newInnerPoint); newPointCreated = false; }else if(newPointCreated){ /* find the segment where the new point lays */ for(ListIterator<InnerPoint> pItr = points.listIterator(); pItr.hasNext() && !found; ){ InnerPoint ePoint = pItr.next(); for(ListIterator<GraphElement> geItr = ePoint.neighbours.listIterator(); geItr.hasNext() && !found;){ /* find the neighbour of the current edge point whose line the new point lays on */ GraphElement ge = geItr.next(); if(fatStrokeContains(getSegment(ePoint, ge),downPoint)){ if(ge instanceof InnerPoint ){ /* remove current edge point from the neighbour's neighbours */ ((InnerPoint)ge).neighbours.remove(ePoint); ((InnerPoint)ge).neighbours.add(newInnerPoint); } /*remove old neighbour from edgePoint neighbours */ geItr.remove(); newInnerPoint.neighbours.add(ePoint); newInnerPoint.neighbours.add(ge); /* add the new node to the list of EdgeNodes of this edge */ pItr.add(newInnerPoint); geItr.add(newInnerPoint); found = true; } } } newPointCreated = false; } newInnerPoint.translate(p, p.getX() - newInnerPoint.getBounds().getCenterX(), p.getY() - newInnerPoint.getBounds().getCenterY()); notifyChange(new ElementChangedEvent(this,this,"bend")); } /* * this methods checks for segments of the edge which are aligned and makes a unique edge out of them */ public void stopMove(){ for(ListIterator<InnerPoint> pItr = points.listIterator(); pItr.hasNext(); ){ InnerPoint ePoint = pItr.next(); if(ePoint.neighbours.size() > 2) continue; Rectangle2D startBounds = ePoint.getBounds(); Rectangle2D endBounds = ePoint.neighbours.get(0).getBounds(); Direction d1 = new Direction(endBounds.getCenterX() - startBounds.getCenterX(), endBounds.getCenterY() - startBounds.getCenterY()); endBounds = ePoint.neighbours.get(1).getBounds(); Direction d2 = new Direction(endBounds.getCenterX() - startBounds.getCenterX(), endBounds.getCenterY() - startBounds.getCenterY()); if(d1.isParallel(d2)){ InnerPoint p = null; GraphElement q = null; if(ePoint.neighbours.get(0) instanceof InnerPoint){ p = (InnerPoint)ePoint.neighbours.get(0); q = ePoint.neighbours.get(1); p.neighbours.add(q); p.neighbours.remove(ePoint); } if(ePoint.neighbours.get(1) instanceof InnerPoint){ p = (InnerPoint)ePoint.neighbours.get(1); q = ePoint.neighbours.get(0); p.neighbours.add(q); p.neighbours.remove(ePoint); } pItr.remove(); } } notifyChange(new ElementChangedEvent(this,this,"stop_move")); } public void startMove(Point2D downPoint){ this.downPoint = downPoint; newInnerPoint = null; for(InnerPoint itrPoint : points) if(itrPoint.contains(downPoint)){ /* clicked on an already existing EdgePoint */ newInnerPoint = itrPoint; newPointCreated = false; } if(newInnerPoint == null){ /* no point under the click, create a new one */ newInnerPoint = new InnerPoint(); newInnerPoint.translate(downPoint, downPoint.getX() - newInnerPoint.getBounds().getCenterX(), downPoint.getY() - newInnerPoint.getBounds().getCenterY()); newPointCreated = true; } } @Override public void translate(Point2D p, double dx, double dy){ for(InnerPoint ip : points) ip.translate(p, dx, dy); } public Line2D getNameLine(){ if(points.isEmpty()){ // straight line return getSegment(nodes.get(0),nodes.get(1)); }else{ if(masterInnerPoint != null){/* multiended edge */ Rectangle2D bounds = masterInnerPoint.getBounds(); Point2D p = new Point2D.Double(bounds.getCenterX() - 1,bounds.getCenterY()); Point2D q = new Point2D.Double(bounds.getCenterX() + 1,bounds.getCenterY()); return new Line2D.Double(p, q); }else{ GraphElement ge1 = nodes.get(0); GraphElement ge2 = nodes.get(1); InnerPoint c1 = null; InnerPoint c2 = null; for(InnerPoint innp : points){ if(innp.getNeighbours().contains(ge1)){ c1 = innp; } if(innp.getNeighbours().contains(ge2)){ c2 = innp; } } /* we only have two nodes but the edge has been bended */ while((c1 != c2)&&(!c2.getNeighbours().contains(c1))){ if(c1.getNeighbours().get(0) == ge1){ ge1 = c1; c1 = (InnerPoint)c1.getNeighbours().get(1); } else{ ge1 = c1; c1 = (InnerPoint)c1.getNeighbours().get(0); } if(c2.getNeighbours().get(0) == ge2){ ge2 = c2; c2 = (InnerPoint)c2.getNeighbours().get(1); } else{ ge2 = c2; c2 = (InnerPoint)c2.getNeighbours().get(0); } } Point2D p = new Point2D.Double(); Point2D q = new Point2D.Double(); if(c1 == c2){ Rectangle2D bounds = c1.getBounds(); p.setLocation( bounds.getCenterX() - 1,bounds.getCenterY()); q.setLocation( bounds.getCenterX() + 1,bounds.getCenterY()); }else{ Rectangle2D bounds = c1.getBounds(); p.setLocation( bounds.getCenterX(),bounds.getCenterY()); bounds = c2.getBounds(); q.setLocation(bounds.getCenterX(),bounds.getCenterY()); } return new Line2D.Double(p,q); } } } @Override public abstract Rectangle2D getBounds(); public void encode(Document doc, Element parent, List<Node> nodes){ parent.setAttribute(PersistenceManager.TYPE,getType()); parent.setAttribute(PersistenceManager.NAME, getName()); parent.setAttribute(PersistenceManager.ID, String.valueOf(getId())); int numNodes = getNodesNum(); if(numNodes > 0){ Element nodesTag = doc.createElement(PersistenceManager.NODES); parent.appendChild(nodesTag); for(int i=0; i<numNodes;i++){ Element nodeTag = doc.createElement(PersistenceManager.NODE); nodeTag.setAttribute(PersistenceManager.ID, String.valueOf(getNodeAt(i).getId())); nodeTag.setAttribute(PersistenceManager.LABEL, getEndLabel(getNodeAt(i))); nodesTag.appendChild(nodeTag); } } if(!points.isEmpty()){ Element pointsTag = doc.createElement(PersistenceManager.POINTS); parent.appendChild(pointsTag); for(InnerPoint point : points){ Element pointTag = doc.createElement(PersistenceManager.POINT); pointsTag.appendChild(pointTag); pointTag.setAttribute(PersistenceManager.ID, String.valueOf(-(points.indexOf(point)+1))); Element positionTag = doc.createElement(PersistenceManager.POSITION); pointTag.appendChild(positionTag); Rectangle2D bounds = point.getBounds(); positionTag.setAttribute(PersistenceManager.X,String.valueOf(bounds.getX())); positionTag.setAttribute(PersistenceManager.Y,String.valueOf(bounds.getY())); Element neighboursTag = doc.createElement(PersistenceManager.NEIGHBOURS); pointTag.appendChild(neighboursTag); StringBuilder builder = new StringBuilder(); for(GraphElement ge : point.getNeighbours()){ if(ge instanceof Node){ builder.append(((Node)ge).getId()); }else{ builder.append(-(points.indexOf(ge)+1)); } builder.append(" "); } builder.deleteCharAt(builder.length()-1); neighboursTag.setTextContent(builder.toString()); } } } public void decode(Document doc, Element edgeTag, Map<String,Node> nodesId) throws IOException{ setName(edgeTag.getAttribute(PersistenceManager.NAME)); if(getName().isEmpty()) throw new IOException(); try{ setId(Integer.parseInt(edgeTag.getAttribute(PersistenceManager.ID))); }catch(NumberFormatException nfe){ throw new IOException(nfe); } NodeList nodeList = edgeTag.getElementsByTagName(PersistenceManager.NODE); List<DiagramNode> attachedNodes = new ArrayList<DiagramNode>(nodeList.getLength()); List<String> labels = new ArrayList<String>(nodeList.getLength()); for(int i=0; i<nodeList.getLength();i++){ String id = ((Element)nodeList.item(i)).getAttribute(PersistenceManager.ID); if(!nodesId.containsKey(id)) throw new IOException(); attachedNodes.add(nodesId.get(id)); labels.add(((Element)nodeList.item(i)).getAttribute(PersistenceManager.LABEL)); } try { connect(attachedNodes); } catch (ConnectNodesException e) { throw new IOException(e); } for(int i=0; i < labels.size(); i++){ setEndLabel(attachedNodes.get(i), labels.get(i)); } Map<String, InnerPoint> pointsId = new LinkedHashMap<String, InnerPoint>(); NodeList pointTagList = edgeTag.getElementsByTagName(PersistenceManager.POINT); for(int i=0; i<pointTagList.getLength(); i++){ InnerPoint point = new InnerPoint(); Element pointTag = (Element)pointTagList.item(i); String id = pointTag.getAttribute(PersistenceManager.ID); /* id of property nodes must be a negative value */ try{ if(Integer.parseInt(id) >= 0) throw new IOException(); }catch(NumberFormatException nfe){ throw new IOException(nfe); } pointsId.put(id, point); if(pointTag.getElementsByTagName(PersistenceManager.POSITION).item(0) == null) throw new IOException(); Element pointPositionTag = (Element)pointTag.getElementsByTagName(PersistenceManager.POSITION).item(0); double dx = 0,dy = 0; try{ dx = Double.parseDouble(pointPositionTag.getAttribute(PersistenceManager.X)); dy = Double.parseDouble(pointPositionTag.getAttribute(PersistenceManager.Y)); }catch(NumberFormatException nfe){ throw new IOException(); } point.translate(new Point2D.Double(), dx, dy); } /* remove the master inner point eventually created by connect */ /* we're going to replace it with the one in the XML file */ points.clear(); /* re do the cycle when all the points id have been Map-ped */ for(int i=0; i<pointTagList.getLength(); i++){ Element pointTag = (Element)pointTagList.item(i); InnerPoint point = pointsId.get(pointTag.getAttribute(PersistenceManager.ID)); if(pointTag.getElementsByTagName(PersistenceManager.NEIGHBOURS).item(0) == null) throw new IOException(); Element pointNeighboursTag = (Element)pointTag.getElementsByTagName(PersistenceManager.NEIGHBOURS).item(0); String pointNeighboursTagContent = pointNeighboursTag.getTextContent(); String[] neighboursId = pointNeighboursTagContent.split(" "); for(String neighbourId : neighboursId){ GraphElement ge = nodesId.get(neighbourId); if(ge == null) // it ain't a node ge = pointsId.get(neighbourId); if(ge == null) throw new IOException(); point.neighbours.add(ge); } points.add(point); } } @Override public Point2D getConnectionPoint(Direction d){return null;} public int getMinAttachedNodes(){ return minAttachedNodes; } public int getMaxAttachedNodes(){ return maxAttachedNodes; } public LineStyle getStyle(){ return style; } protected Point2D downPoint; private List<Node> nodes; /* list containing the vertex of the edge which are not nodes */ protected List<InnerPoint> points; protected InnerPoint newInnerPoint; protected InnerPoint masterInnerPoint; protected boolean newPointCreated; private int minAttachedNodes; private int maxAttachedNodes; private LineStyle style; private static final double MAX_DIST = 3; private static final Color POINT_COLOR = Color.GRAY; public static final String NO_ARROW_STRING = ResourceBundle.getBundle(EditorFrame.class.getName()).getString("no_arrow_string"); protected static class InnerPoint implements GraphElement{ public InnerPoint(){ bounds = new Rectangle2D.Double(0,0,DIM,DIM); neighbours = new LinkedList<GraphElement>(); } @Override public void startMove(Point2D p){} @Override public void stopMove(){} @Override public void draw(Graphics2D g2){ Color oldColor = g2.getColor(); g2.setColor(POINT_COLOR); g2.fill(bounds); g2.setColor(oldColor); } @Override public boolean contains(Point2D p){ return bounds.contains(p); } @Override public Point2D getConnectionPoint(Direction d){ return new Point2D.Double(bounds.getCenterX(),bounds.getCenterY()); } @Override public void translate(Point2D p, double dx, double dy){ bounds.setFrame(bounds.getX() + dx, bounds.getY() + dy, bounds.getWidth(), bounds.getHeight()); } @Override public Rectangle2D getBounds(){ return (Rectangle2D)bounds.clone(); } public List<GraphElement> getNeighbours(){ return neighbours; } public boolean hasNeighbour(GraphElement neighbour){ return neighbours.contains(neighbour); } @Override public String toString(){ return "EdgePoint: "+bounds.getCenterX()+"-"+bounds.getCenterY(); } private Rectangle2D bounds; private List<GraphElement> neighbours; private static final int DIM = 5; } public static class PointRepresentation { public PointRepresentation(int size){ xs = new double[size]; ys = new double[size]; adjMatrix = new BitSet[size]; for(int i=0; i<size; i++){ adjMatrix[i] = new BitSet(size); } } public double xs[]; public double ys[]; public BitSet adjMatrix[]; public int nodeStart; // the index of the beginning of the nodes in the adjMatrix } public PointRepresentation getPointRepresentation(){ PointRepresentation pr = new PointRepresentation(points.size()+nodes.size()); if(points.isEmpty()){ // two ended edge pr.xs[0] = nodes.get(0).getBounds().getCenterX(); pr.ys[0] = nodes.get(0).getBounds().getCenterY(); pr.xs[1] = nodes.get(1).getBounds().getCenterX(); pr.ys[1] = nodes.get(1).getBounds().getCenterY(); // we only need one edge, else it would be painted twice pr.adjMatrix[0].set(1); pr.nodeStart = 0; }else{ //[ point 1, point 2, point 3, ... , point n, node, 1 node 2, ... , node n ] int pSize = points.size(); pr.nodeStart = pSize; // the first node starts after the points for(int i=0; i<pSize;i++){ pr.xs[i] = points.get(i).getBounds().getCenterX(); pr.ys[i] = points.get(i).getBounds().getCenterY(); for(GraphElement ge : points.get(i).neighbours){ if(ge instanceof InnerPoint) pr.adjMatrix[i].set(points.indexOf(ge)); else //Node pr.adjMatrix[i].set(pSize+nodes.indexOf(ge)); } } /* set the coordinates of the nodes, no adj matrix needed as the inner points are enough */ for(int i=0 ; i<nodes.size(); i++){ pr.xs[pSize+i] = nodes.get(i).getBounds().getCenterX(); pr.ys[pSize+i] = nodes.get(i).getBounds().getCenterY(); } } return pr; } }