view java/src/uk/ac/qmul/eecs/ccmi/gui/Node.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 1c5af356bb99
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.Color;
import java.awt.Graphics2D;
import java.awt.Shape;
import java.awt.geom.Point2D;
import java.awt.geom.Rectangle2D;
import java.io.IOException;
import java.util.ArrayList;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Set;

import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.NodeList;

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.diagrammodel.NodeProperties;
import uk.ac.qmul.eecs.ccmi.diagrammodel.NodeProperties.Modifiers;
import uk.ac.qmul.eecs.ccmi.gui.persistence.PersistenceManager;

/**
 * An node in a graph. {@code Node} objects are used in a {@code GraphPanel} to render diagram nodes visually. 
 * {@code Node} objects are used in the tree representation of the diagram as well, as they're
 * subclasses of {@link DiagramNode}    
 * 
 */
@SuppressWarnings("serial")
public abstract class Node extends DiagramNode implements GraphElement{

	/**
	 * Constructor to be called by sub classes 
	 * 
	 * @param type the type of the new node. All nodes with this type will be 
	 * put under the same tree node in the tree representation 
	 * @param properties the properties of this node 
	 */
	public Node(String type, NodeProperties properties){
		super(type,properties);
		attachedEdges = new ArrayList<Edge>();
	}

	/* --- DiagramNode abstract methods implementation --- */
	@Override
	public int getEdgesNum(){
		return attachedEdges.size();
	}

	@Override
	public Edge getEdgeAt(int index){
		return attachedEdges.get(index);
	}
	
	@Override
	public boolean addEdge(DiagramEdge e){
		return attachedEdges.add((Edge)e);
	}

	@Override
	public boolean removeEdge(DiagramEdge e){
		return attachedEdges.remove((Edge)e);
	}

	@Override
	public Node getExternalNode(){
		return null; 
	}

	@Override
	public void setExternalNode(DiagramNode node){
		throw new UnsupportedOperationException(); 
	}

	@Override
	public  Node getInternalNodeAt(int i){
		throw new UnsupportedOperationException(); 
	}
	
	@Override
	public int getInternalNodesNum(){
		return 0;
	}

	@Override
	public void addInternalNode(DiagramNode node){
		throw new UnsupportedOperationException(); 
	}

	@Override
	public void removeInternalNode(DiagramNode node){
		throw new UnsupportedOperationException(); 
	}

	@Override
	public void stopMove(Object source){
		notifyChange(new ElementChangedEvent(this,this,"stop_move",source));
		/* edges can change as a result of nodes motion thus we call the method for all the edges
		 * of the node regardless what the mouse point is */
		for(int i = 0; i < getEdgesNum();i++){
			getEdgeAt(i).stopMove(source);
		}
	}

	@Override
	public void translate( Point2D p , double dx, double dy, Object source){
		translateImplementation( p, dx, dy);
		for(int i=0; i< getInternalNodesNum();i++){
			getInternalNodeAt(i).translate(p, dx, dy,source);
		}
		notifyChange(new ElementChangedEvent(this, this, "translate", source));
	}

	@Override
	protected void setNotes(String notes,Object source){
		this.notes = notes;
		notifyChange(new ElementChangedEvent(this,this,"notes",source));
	}
	
	/**
	 *  The actual implementation of {@code translate()}. The {@code translate} method
	 *  when called will, in turn, call this method, and then call all the registered
	 *  change listeners in order to notify them that the node has been translated.
	 *    
	 *	@param p the point we are translating from
	 *	@param dx the amount to translate in the x-direction
	 *	@param dy the amount to translate in the y-direction
	 */
	protected abstract void translateImplementation(Point2D p , double dx, double dy);
		
	/**
	 *	Tests whether the node contains a point.
	 *	@param aPoint the point to test
	 *	@return true if this node contains aPoint
	 */
	public abstract boolean contains(Point2D aPoint);

	@Override
	public abstract Rectangle2D getBounds();

	@Override
	public void startMove(Point2D p,Object source){
		/* useless, here just to comply with the GraphElement interface */
	}

	@Override
	public abstract Point2D getConnectionPoint(Direction d);

	@Override
	public void draw(Graphics2D g2){
		if(!"".equals(getNotes())){
			Rectangle2D bounds = getBounds();
			Color oldColor = g2.getColor();
			g2.setColor(GraphPanel.GRABBER_COLOR);
			g2.fill(new Rectangle2D.Double(bounds.getX() - MARKER_SIZE / 2, bounds.getY() - MARKER_SIZE / 2, MARKER_SIZE, MARKER_SIZE));
			g2.setColor(oldColor);
		}
	}

	/**
	 * Returns the geometric shape of this node  
	 * 
	 * @return the shape of this node
	 */
	public abstract Shape getShape();

	/**
	 * Encodes the internal data of this node (position, name, properties, modifiers) in XML format. 
	 * 
	 * The saved data can be retrieved and set back via {@code decode}. 
	 *  
	 * @param doc An XMl document
	 * @param parent the parent XML tag this node tag will be nested in  
	 */
	public void encode(Document doc, Element parent){
		parent.setAttribute(PersistenceManager.NAME,getName());

		Element positionTag = doc.createElement(PersistenceManager.POSITION);
		Rectangle2D bounds = getBounds();
		positionTag.setAttribute(PersistenceManager.X, String.valueOf(bounds.getX()));
		positionTag.setAttribute(PersistenceManager.Y, String.valueOf(bounds.getY()));
		parent.appendChild(positionTag);

		if(getProperties().isEmpty())
			return;

		Element propertiesTag = doc.createElement(PersistenceManager.PROPERTIES);
		parent.appendChild(propertiesTag);
		for(String type : getProperties().getTypes()){
			List<String> values = getProperties().getValues(type);
			if(values.isEmpty())
				continue;
			Element propertyTag = doc.createElement(PersistenceManager.PROPERTY);
			propertiesTag.appendChild(propertyTag);

			Element typeTag = doc.createElement(PersistenceManager.TYPE);
			typeTag.appendChild(doc.createTextNode(type));
			propertyTag.appendChild(typeTag);

			int index = 0;
			for(String value : values){
				Element elementTag = doc.createElement(PersistenceManager.ELEMENT);
				propertyTag.appendChild(elementTag);

				Element valueTag = doc.createElement(PersistenceManager.VALUE);
				valueTag.appendChild(doc.createTextNode(value));
				elementTag.appendChild(valueTag);


				Set<Integer> modifierIndexes = getProperties().getModifiers(type).getIndexes(index);
				if(!modifierIndexes.isEmpty()){
					Element modifiersTag = doc.createElement(PersistenceManager.MODIFIERS);
					StringBuilder builder = new StringBuilder();
					for(Integer i : modifierIndexes )
						builder.append(i).append(' ');
					builder.deleteCharAt(builder.length()-1);//remove last space
					modifiersTag.appendChild(doc.createTextNode(builder.toString()));
					elementTag.appendChild(modifiersTag);
				}
				index++;
			}
		}
	}

	/**
	 * Sets the internal data of this node (position, name, properties, modifiers) from an XML file
	 * node tag previously encoded via {@code encode}  
	 * 
	 * @param doc An XMl document
	 * @param nodeTag the XML {@code PersistenceManager.NODE } tag with data for this node
	 * @throws IOException if something goes wrong when reading the document. E.g. when the file is corrupted 
	 * 
	 * @see uk.ac.qmul.eecs.ccmi.gui.persistence
	 */
	public void decode(Document doc, Element nodeTag) throws IOException{
		setName(nodeTag.getAttribute(PersistenceManager.NAME),DiagramEventSource.PERS);
		try{
			setId(Integer.parseInt(nodeTag.getAttribute(PersistenceManager.ID)));
		}catch(NumberFormatException nfe){
			throw new IOException();
		}

		if(nodeTag.getElementsByTagName(PersistenceManager.POSITION).item(0) == null)
			throw new IOException();
		Element positionTag = (Element)nodeTag.getElementsByTagName(PersistenceManager.POSITION).item(0);
		double dx,dy;
		try{
			dx = Double.parseDouble(positionTag.getAttribute(PersistenceManager.X));
			dy = Double.parseDouble(positionTag.getAttribute(PersistenceManager.Y));
		}catch(NumberFormatException nfe){
			throw new IOException();
		}
		Rectangle2D bounds = getBounds();
		translate(new Point2D.Double(0,0), dx - bounds.getX(), dy - bounds.getY(),DiagramEventSource.PERS);

		NodeList propList = nodeTag.getElementsByTagName(PersistenceManager.PROPERTY);
		NodeProperties properties = getProperties();
		for(int j=0; j<propList.getLength();j++){
			Element propertyTag = (Element)propList.item(j);

			if(propertyTag.getElementsByTagName(PersistenceManager.TYPE).item(0) == null)
				throw new IOException();
			Element pTypeTag = (Element)propertyTag.getElementsByTagName(PersistenceManager.TYPE).item(0);
			String propertyType = pTypeTag.getTextContent();

			/* scan all the <Element> of the current <Property>*/
			NodeList elemValueList = propertyTag.getElementsByTagName(PersistenceManager.ELEMENT);
			for(int h=0; h<elemValueList.getLength(); h++){

				Element elemTag = (Element)elemValueList.item(h);
				/* get the <value> */
				if(elemTag.getElementsByTagName(PersistenceManager.VALUE).item(0) == null)
					throw new IOException();
				Element valueTag = (Element)elemTag.getElementsByTagName(PersistenceManager.VALUE).item(0);
				String value = valueTag.getTextContent();

				/* <modifiers>. need to go back on the prototypes because the content of <modifier> is a list */
				/* of int index pointing to the modifiers type, defined just in the prototypes                */
				Element prototypesTag = (Element)doc.getElementsByTagName(PersistenceManager.PROTOTYPES).item(0);
				Modifiers modifiers = null;
				try {
					modifiers = properties.getModifiers(propertyType);
				}catch(IllegalArgumentException iae){
					throw new IOException(iae);
				}
				if(!modifiers.isNull()){
					Element modifiersTag = (Element)((Element)elemValueList.item(h)).getElementsByTagName(PersistenceManager.MODIFIERS).item(0);
					if(modifiersTag != null){ //else there are no modifiers specified for this property value
						Set<Integer> indexesToAdd = new LinkedHashSet<Integer>();
						String indexesString = modifiersTag.getTextContent();
						String[] indexes = indexesString.split(" ");
						for(String s : indexes){
							try{
								int index = Integer.parseInt(s);
								NodeList templatePropList = prototypesTag.getElementsByTagName(PersistenceManager.PROPERTY);
								String modifiersType = null;
								/* look at the property prototypes to see which modifier the index is referring to.      *  
								 * The index is in fact the id attribute of the <Modifier> tag in the prototypes section */
								for(int k=0; k<templatePropList.getLength();k++){
									Element prototypePropTag = (Element)templatePropList.item(k);
									Element prototypePropTypeTag = (Element)prototypePropTag.getElementsByTagName(PersistenceManager.TYPE).item(0);
	
									if(propertyType.equals(prototypePropTypeTag.getTextContent())){
										NodeList proptotypeModifierList = prototypePropTag.getElementsByTagName(PersistenceManager.MODIFIER);
										for(int m = 0 ; m<proptotypeModifierList.getLength();m++){
											if(index == Integer.parseInt(((Element)proptotypeModifierList.item(m)).getAttribute(PersistenceManager.ID))){
												Element modifierTypeTag = (Element)((Element)proptotypeModifierList.item(m)).getElementsByTagName(PersistenceManager.TYPE).item(0);
												modifiersType = modifierTypeTag.getTextContent();
											}
										}
									}
								}
								if(modifiersType == null) // the index must point to a valid modifier's id
									throw new IOException();
								indexesToAdd.add(Integer.valueOf(modifiers.getTypes().indexOf(modifiersType)));
							}catch(NumberFormatException nfe){
								throw new IOException(nfe);
							}
						}
						addProperty(propertyType, value,DiagramEventSource.PERS);//whether propertyType actually exist in the prototypes has been already checked  
						setModifierIndexes(propertyType, h, indexesToAdd,DiagramEventSource.PERS);
					}else
						addProperty(propertyType, value,DiagramEventSource.PERS);
				}else
					addProperty(propertyType, value,DiagramEventSource.PERS);
			}
		}
	}
	
	@SuppressWarnings("unchecked")
	@Override
	public Object clone(){
		Node clone = (Node)super.clone();
		clone.attachedEdges = (ArrayList<Edge>) attachedEdges.clone(); 
		return clone;
	}

	/**
	 * An array of references to the edges attached to this node 
	 */
	protected ArrayList<Edge> attachedEdges;

	private final int MARKER_SIZE = 7;
	/**
	 * The shadow color of nodes
	 */
	protected static final Color SHADOW_COLOR = Color.LIGHT_GRAY;
	public static final int SHADOW_GAP = 2;

}