view 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
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{

	/**
	 * 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;
	}

}