Mercurial > hg > accesspd
diff java/src/uk/ac/qmul/eecs/ccmi/gui/Edge.java @ 0:78b7fc5391a2
first import, outcome of NIME 2014 hackaton
author | Fiore Martin <f.martin@qmul.ac.uk> |
---|---|
date | Tue, 08 Jul 2014 16:28:59 +0100 |
parents | |
children |
line wrap: on
line diff
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/java/src/uk/ac/qmul/eecs/ccmi/gui/Edge.java Tue Jul 08 16:28:59 2014 +0100 @@ -0,0 +1,864 @@ +/* + 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{ + + /** + * Creates a new Edge + * + * @param type the type of the edge. The type is just a string that the user assign to the edge. + * All the edges created via clonation from this edge will have the same type. + * @param availableEndDescriptions all the possible end description ends of this edge can be + * associated to. An end description is a text associated to a possible arrow head of an edge end. + * It's used to give awareness of arrow heads via speech. + * @param minAttachedNodes the minimum number of nodes that can be attached to this edge + * @param maxAttachedNodes the minimum number of nodes that can be attached to this edge + * @param style the style of the edge: whether it's solid, dotted or dashed + */ + 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>(); + } + + /* --- Methods inherited from DiagramEdge --- */ + @Override + public Node getNodeAt(int index){ + return nodes.get(index); + } + + @Override + public int getNodesNum(){ + return nodes.size(); + } + + @Override + public boolean removeNode(DiagramNode diagramNode, Object source){ + 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; + } + boolean nodeRemoved = nodes.remove(n); + /* for update in the haptic device */ + notifyChange(new ElementChangedEvent(this,n,"remove_node",source)); + return nodeRemoved; + } + } + + @Override + 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(),DiagramEventSource.NONE); + p.neighbours.addAll(nodeSet); + points.add(p); + } + this.nodes.addAll(nodeSet); + + if(!points.isEmpty())// points is empty when the edge has two nodes only + masterInnerPoint = points.get(0); + } + + @Override + public abstract void draw(Graphics2D g2); + + @Override + public void translate(Point2D p, double dx, double dy, Object source){ + for(InnerPoint ip : points) + ip.translate(p, dx, dy,source); + } + + /** + * To be called before {@code bend}, determines from {@code downPoint} whether + * a line is going to be break into two lines (with the creation of a new inner point) + * or if the bending is determined by an already existing inner point being translated + */ + @Override + public void startMove(Point2D downPoint,Object source){ + 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(),DiagramEventSource.NONE); + newPointCreated = true; + /* this methods checks for segments of the edge which are aligned and makes a unique edge out of them */ + } + } + + /** + * If this edge is made out of several lines and two or more of them becomes + * aligned then they are blended in one single line and the inner point that was + * at the joint is removed. + * + * @param source the source of the {@code stopMove} action + */ + @Override + public void stopMove(Object source){ + 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",source)); + } + + @Override + public abstract Rectangle2D getBounds(); + + @Override + public Point2D getConnectionPoint(Direction d){return null;} + + @Override + public boolean contains(Point2D aPoint){ + if(points.isEmpty()){ + return fatStrokeContains (nodes.get(0), nodes.get(1), aPoint); + } + for(InnerPoint p : points){ + for(GraphElement ge : p.neighbours){ + if(fatStrokeContains(p,ge,aPoint)) + return true; + } + } + return false; + } + + /** + * 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 neighbour only */ + InnerPoint neighbour = (InnerPoint)p.neighbours.get(0); + points.remove(p); + neighbour.neighbours.remove(p); + if(neighbour.neighbours.size() == 1) + removePoint(neighbour); + } + + /* checks if a point belongs to a shape with a margin of MAX_DIST*/ + private boolean fatStrokeContains(GraphElement ge1, GraphElement ge2, Point2D p){ + BasicStroke fatStroke = new BasicStroke((float) (2 * MAX_DIST)); + Line2D line = new Line2D.Double( + ge1.getBounds().getCenterX(), + ge1.getBounds().getCenterY(), + ge2.getBounds().getCenterX(), + ge2.getBounds().getCenterY()); + Shape fatPath = fatStroke.createStrokedShape(line); + return fatPath.contains(p); + } + + /** + * Returns a line connecting the centre of the graph elements passed as argument. + * + * @param start the first graph element + * @param end the second graph element + * + * @return a line connecting {@code start} and {@code end} + */ + 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))); + } + + /** + * Returns a list of the points where this edge and the nodes it connects come to a contact + * + * @return a list of points + */ + 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; + } + + /** + * Gets the stipple pattern of this edge line style + * + * @see LineStyle#getStipplePattern() + * + * @return an int representing the stipple pattern of this edge + */ + public int getStipplePattern(){ + return getStyle().getStipplePattern(); + } + + /** + * Bends one of the lines forming this edge. + * + * When an line is bent, if the location where the bending happens is a line then a new + * inner point is created and the line is broken into two sub lines. If the location is an + * already existing inner point, then the point is translated. + * + * @param p the starting point of the bending + * @param source the source of the bending action + */ + public void bend(Point2D p,Object source) { + boolean found = false; + if(points.isEmpty()){ + newInnerPoint.neighbours.addAll(nodes); + points.add(newInnerPoint); + newPointCreated = false; + }else if(newPointCreated){ + /* find the segment closest to where the new point lays */ + InnerPoint closestP1 = null; + GraphElement closestP2 = null; + double minDist = 0; + for(ListIterator<InnerPoint> pItr = points.listIterator(); pItr.hasNext(); ){ + 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 next = geItr.next(); + double dist = Line2D.ptSegDist( + ePoint.getBounds().getCenterX(), + ePoint.getBounds().getCenterY(), + next.getBounds().getCenterX(), + next.getBounds().getCenterY(), + downPoint.getX(), + downPoint.getY() + ); + + if(closestP1 == null || dist < minDist){ + closestP1 = ePoint; + closestP2 = next; + minDist = dist; + continue; + } + } + } + + if(closestP2 instanceof InnerPoint ){ + /* remove current edge point from the neighbour's neighbours */ + ((InnerPoint)closestP2).neighbours.remove(closestP1); + /* add the new inner point */ + ((InnerPoint)closestP2).neighbours.add(newInnerPoint); + } + /*remove old neighbour from edge inner point neighbours */ + closestP1.neighbours.remove(closestP2); + newInnerPoint.neighbours.add(closestP1); + newInnerPoint.neighbours.add(closestP2); + /* add the new node to the list of EdgeNodes of this edge */ + points.add(newInnerPoint); + closestP1.neighbours.add(newInnerPoint); + found = true; + newPointCreated = false; + } + newInnerPoint.translate(p, p.getX() - newInnerPoint.getBounds().getCenterX(), + p.getY() - newInnerPoint.getBounds().getCenterY(),DiagramEventSource.NONE); + notifyChange(new ElementChangedEvent(this,this,"bend",source)); + } + + /** + * Returns the line where the edge name will be painted. If the edge is only made out + * of a straight line then this will be returned. + * + * If the edge has been broken into several segments (by bending it) then the central + * line is returned. If the edge connects more than two nodes then a line (not necessarily + * matching the edge) that has the central point in its centre is returned. Note that a + * edge connecting more than two nodes is painted as a central point connected to all the nodes. + * + * @return the line where the name will be painted + */ + 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); + } + } + } + + /** + * Encodes all the relevant data of this object in XML format. + * + * @param doc an XML document + * @param parent the parent XML element, where tag about this edge will be inserted + * @param nodes a list of all nodes of the diagram + */ + 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()); + } + } + } + + + /** + * Decodes an edge from the XML representation. + * + * @see #encode(Document, Element, List) + * + * @param doc an XML document + * @param edgeTag the tag in the XML file related to this edge + * @param nodesId a map linking node ids in the XML file to the {@code Node} objects they represent + * @throws IOException if something goes wrong while reading the XML file + */ + public void decode(Document doc, Element edgeTag, Map<String,Node> nodesId) throws IOException{ + setName(edgeTag.getAttribute(PersistenceManager.NAME),DiagramEventSource.PERS); + 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),DiagramEventSource.PERS); + } + + 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,DiagramEventSource.PERS); + } + + /* 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); + if(i==0) + masterInnerPoint = point; + } + } + + /** + * Returns the minimum number of nodes that edge of this type can connect + * + * @return the minimum nodes for edges of this type + */ + public int getMinAttachedNodes(){ + return minAttachedNodes; + } + + /** + * Returns the maximum number of nodes that edge of this type can connect + * + * @return the maximum nodes for edges of this type + */ + public int getMaxAttachedNodes(){ + return maxAttachedNodes; + } + + /** + * Return the line style of this edge + * + * @return the line style of this edge + */ + public LineStyle getStyle(){ + return style; + } + + protected Point2D downPoint; + private List<Node> nodes; + + /* list containing the vertex of the edge which are not nodes */ + /** + * The list of the inner points of this edge + */ + protected List<InnerPoint> points; + /** + * For edges connecting more than two nodes, this is the central inner point where + * all the lines from the nodes join + */ + protected InnerPoint masterInnerPoint; + + private boolean newPointCreated; + private InnerPoint newInnerPoint; + private int minAttachedNodes; + private int maxAttachedNodes; + + private LineStyle style; + + + private static final double MAX_DIST = 5; + private static final Color POINT_COLOR = Color.GRAY; + /** + * The end description for an end that has no hand description set by the user + */ + public static final String NO_ENDDESCRIPTION_STRING = ResourceBundle.getBundle(EditorFrame.class.getName()).getString("no_arrow_string"); + + + /** + * When an edge's (straight) line is bent it breaks into two sub lines. At the point where this two + * sub lines join a square shaped point is painted. This class represent that point. Objects of this class + * are graph elements and the user can click on them and translate them along the graph. + * + */ + protected static class InnerPoint implements GraphElement{ + /** + * Creates a new inner point + */ + public InnerPoint(){ + bounds = new Rectangle2D.Double(0,0,DIM,DIM); + neighbours = new LinkedList<GraphElement>(); + } + + @Override + public void startMove(Point2D p, Object source){} + + @Override + public void stopMove(Object source){} + + @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, Object source){ + bounds.setFrame(bounds.getX() + dx, + bounds.getY() + dy, + bounds.getWidth(), + bounds.getHeight()); + } + + @Override + public Rectangle2D getBounds(){ + return (Rectangle2D)bounds.clone(); + } + + /** + * Neighbours are the graph elements (either nodes or other inner points) this inner point is + * directly connected to. Directly connected means there is a straight line from this node + * and the neighbour. + * + * @return a list of neighbours of this inner node + */ + public List<GraphElement> getNeighbours(){ + return neighbours; + } + + /** + * Returns true if this inner node and {@code ge} are neighbours. + * + * @see #getNeighbours() + * + * @param ge the graph element to be tested + * @return {@code true} if {@code ge} is a neighbour of this graph element, {@code false} otherwise + * + */ + public boolean hasNeighbour(GraphElement ge){ + return neighbours.contains(ge); + } + + @Override + public String toString(){ + return "EdgePoint: "+bounds.getCenterX()+"-"+bounds.getCenterY(); + } + + private Rectangle2D bounds; + private List<GraphElement> neighbours; + private static final int DIM = 7; + } + + /** + * A representation of this edge as a set of 2D points. + * Every node the edge connects and every inner point are represented as a pair with coordinates + * of their centre. Furthermore an adjacency matrix holds the information + * about which point is connected to which is. This representation of the edge is + * used in the haptic space, being more suitable for haptic devices. + * + */ + 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); + } + } + /** + * An array with all the x coordinate of the edge's points (nodes and inner points) + */ + public double xs[]; + /** + * An array with all the y coordinate of the edge's points (nodes and inner points) + */ + public double ys[]; + /** + * The adjacency matrix. If the i-th bit of {@code adjMatrix[j]} is set to {@code true} + * it means there is a direct line connecting the i-th point (coordinates {@code (xs[i],ys[i])} + * to the j-th point (coordinates {@code (xs[j],ys[j])}. Note that connection are represented only + * once to avoid double paintings by the haptic engine. So if the i-th bit of {@code adjMatrix[j]} + * is set to {@code true}, the j-th bit of {@code adjMatrix[i]} and information redundancy is avoided. + */ + public BitSet adjMatrix[]; + /** + * The index of the beginning of the nodes (after inner points) in {@code adjMatrix}. + * So if the edge has three nodes and two inner points. The inner points + * will be at {@code adjMatrix[0]} and {@code adjMatrix[1]} and the nodes at the following indexes. + * In this case {@code nodeStart} is equal to 2. + */ + public int nodeStart; + } + + /** + * Returns a new {@code PointRepresentation} of this edge + * + * @return a new {@code PointRepresentation} of this edge + */ + 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; + } + +} +