view java/src/uk/ac/qmul/eecs/ccmi/simpletemplate/SimpleShapeNode.java @ 0:9418ab7b7f3f

Initial import
author Fiore Martin <fiore@eecs.qmul.ac.uk>
date Fri, 16 Dec 2011 17:35:51 +0000
parents
children 2c67ac862920
line wrap: on
line source
/*  
 CCmI Editor - A Collaborative Cross-Modal Diagram Editing Tool
  
 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.simpletemplate;

import java.awt.Color;
import java.awt.Graphics2D;
import java.awt.Shape;
import java.awt.geom.Ellipse2D;
import java.awt.geom.Line2D;
import java.awt.geom.Path2D;
import java.awt.geom.Point2D;
import java.awt.geom.Rectangle2D;
import java.awt.geom.RectangularShape;
import java.io.IOException;
import java.util.ArrayList;
import java.util.LinkedHashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
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.ElementChangedEvent;
import uk.ac.qmul.eecs.ccmi.diagrammodel.NodeProperties;
import uk.ac.qmul.eecs.ccmi.gui.Direction;
import uk.ac.qmul.eecs.ccmi.gui.Node;
import uk.ac.qmul.eecs.ccmi.gui.persistence.PersistenceManager;
import uk.ac.qmul.eecs.ccmi.utils.Pair;

/**
 * 
 * A diagram node that can be represented visually as a simple shape such as 
 * a rectangle, square, circle, ellipse or triangle.
 *
 */
@SuppressWarnings("serial")
public abstract class SimpleShapeNode extends Node {
	
	public static SimpleShapeNode getInstance(ShapeType shapeType, String typeName, NodeProperties properties){
		switch(shapeType){
		case Rectangle :
			return new RectangularNode(typeName, properties);
		case Square : 
			return new SquareNode(typeName, properties);
		case Circle :
			return new CircleNode(typeName, properties);
		case Ellipse :
			return new EllipticalNode(typeName, properties);
		case Triangle :
			return new TriangularNode(typeName, properties);
		}
		return null;
	}
	
	protected SimpleShapeNode(String typeName, NodeProperties properties){
		super(typeName, properties);
		dataDisplayBounds = (Rectangle2D.Double)getMinBounds();
		/* Initialise the data structures for displaying the properties inside and outside */
		propertyNodesMap = new LinkedHashMap<String,List<PropertyNode>>(); 
		int numInsideProperties = 0;
		for(String type : getProperties().getTypes()){
			if(((PropertyView)getProperties().getView(type)).getPosition() == Position.Inside)
				numInsideProperties++;
			else
				propertyNodesMap.put(type, new LinkedList<PropertyNode>());
		}
		propertyLabels = new MultiLineString[numInsideProperties];
		nameLabel = new MultiLineString();
	}
	
	@Override
	protected void notifyChange(ElementChangedEvent evt){
		if(!evt.getChangeType().equals("translate")&&!evt.getChangeType().equals("stop_move")) //don't reshape for just moving 
			reshape();  
		super.notifyChange(evt);
	}
	
	@Override
	public void setId(long id){
		super.setId(id);
		/* when they are given an id nodes change name into "new <type> node <id>" *
		 * where <type> is the actual type of the node and <id> is the given id    *
		 * therefore a reshape is necessary to display the new name                */
		Rectangle2D boundsBeforeReshape = getBounds();
		reshape();
		/* the reshape might change the bounds, so the shape is translated so that the top-left  *
		 * point is at the same position as before just to keep it more consistent               */
		Rectangle2D boundsAfterReshape = getBounds();
		translateImplementation(
				new Point2D.Double(),
				boundsBeforeReshape.getX() - boundsAfterReshape.getX(),
				boundsBeforeReshape.getY() - boundsAfterReshape.getY()
			);
	}
	
	@Override
	public boolean contains(Point2D p) {
		if (getShape().contains(p))
			return true;
		for(List<PropertyNode> pnList : propertyNodesMap.values())
			for(PropertyNode pn : pnList) 
				if(pn.contains(p))
					return true;
		return false;
	}
	
	protected void reshape(){
		Pair<List<String>, List<String>> splitPropertyTypes = splitPropertyTypes();
		/* properties displayed internally */
		reshapeInnerProperties(splitPropertyTypes.first);
		/* properties displayed externally */ 
		reshapeOuterProperties(splitPropertyTypes.second);	
	}
	
	protected Pair<List<String>, List<String>> splitPropertyTypes(){
		List<String> types = getProperties().getTypes();
		ArrayList<String> insidePropertyTypes = new ArrayList<String>(types.size());
		ArrayList<String> outsidePropertyTypes = new ArrayList<String>(types.size());
		for(String type : types){
			if(((PropertyView)getProperties().getView(type)).getPosition() == Position.Inside)
				insidePropertyTypes.add(type);
			else
				outsidePropertyTypes.add(type);
		}
		
		return new Pair<List<String>, List<String>> (insidePropertyTypes,outsidePropertyTypes);
	}
	
	protected void reshapeOuterProperties(List<String> outsidePropertyTypes){
		for(String type : outsidePropertyTypes){
			List<PropertyNode> propertyNodes = propertyNodesMap.get(type);
			List<String> propertyValues = getProperties().getValues(type);
			int diff = propertyNodes.size()-propertyValues.size();
			if(diff > 0) // properties have been removed
				for(int i=0; i < diff; i++)
					propertyNodes.remove(propertyNodes.size() - 1);
			else if(diff < 0){ // properties have been added. We need more properties node.
				for(int i=0; i < -diff; i++){
					PropertyNode propertyNode = new PropertyNode(((PropertyView)getProperties().getView(type)).getShapeType());
					Rectangle2D bounds = getBounds();
					double x = bounds.getCenterX() - bounds.getWidth()/2 - PROP_NODE_DIST;
					double y = bounds.getCenterX() - PROP_NODE_DIST * i;
					propertyNode.translate(x, y);
					propertyNodes.add(propertyNode);
				}
			}
			/* set the text on all the property nodes */
			int i = 0;
			for(String text : propertyValues){
				NodeProperties.Modifiers modifiers = getProperties().getModifiers(type);
				Set<Integer> viewIndexes = modifiers.getIndexes(i);
				ModifierView[] views = new ModifierView[viewIndexes.size()]; 
				int j =0;
				for(Integer I : viewIndexes){
					views[j] = (ModifierView) getProperties().getModifiers(type).getView(modifiers.getTypes().get(I));
					j++;
				}
				propertyNodes.get(i).setText(text,views);
				i++;
			}
		}
	}
	
	protected void reshapeInnerProperties(List<String> insidePropertyTypes){
		/* set the bounds for each multiline string and the resulting bound of the node */
		nameLabel = new MultiLineString();
		nameLabel.setText(getName().isEmpty() ? " " : getName());
		nameLabel.setBold(true);		
		Rectangle2D r = nameLabel.getBounds();
		
		for(int i=0; i<insidePropertyTypes.size();i++){
			propertyLabels[i] = new MultiLineString();
			String propertyType = insidePropertyTypes.get(i);
	    	if(getProperties().getValues(propertyType).size() == 0){
	    		propertyLabels[i].setText(" ");
	    	}else{
	    		propertyLabels[i].setJustification(MultiLineString.LEFT);
	    		String[] array = new String[getProperties().getValues(propertyType).size()];
	    		propertyLabels[i].setText(getProperties().getValues(propertyType).toArray(array), getProperties().getModifiers(propertyType));
	    	}
	    	r.add(new Rectangle2D.Double(r.getX(),r.getMaxY(),propertyLabels[i].getBounds().getWidth(),propertyLabels[i].getBounds().getHeight()));
    	}
		/* set a gap to uniformly distribute the extra space among property rectangles to reach the minimum bound's height */
		boundsGap = 0;
		Rectangle2D.Double minBounds = (Rectangle2D.Double)getMinBounds();
		if(r.getHeight() < minBounds.height){
			boundsGap = minBounds.height - r.getHeight();
			boundsGap /= insidePropertyTypes.size();
		}
		r.add(minBounds); //make sure it's at least as big as the minimum bounds 
		dataDisplayBounds.setFrame(new Rectangle2D.Double(dataDisplayBounds.x,dataDisplayBounds.y,r.getWidth(),r.getHeight()));
	}
	
	/**
	  Draw the node from the Shape with shadow
	  @param g2 the graphics context
	*/
	@Override
	public void draw(Graphics2D g2){
		/* draw the external shape */
		Shape shape = getShape();
			if (shape == null) return;
		Color oldColor = g2.getColor();
		g2.translate(SHADOW_GAP, SHADOW_GAP);      
		g2.setColor(SHADOW_COLOR);
		g2.fill(shape);
		g2.translate(-SHADOW_GAP, -SHADOW_GAP);
		g2.setColor(g2.getBackground());
		g2.fill(shape);
		g2.setColor(Color.BLACK);
		g2.draw(shape);      
		g2.setColor(oldColor);
	
		/* if there ain't any property to display inside, then display the name in the middle of the data Display bounds */
		if(!anyInsideProperties()){
	    	nameLabel.draw(g2, dataDisplayBounds);
		}else{
	    	/* draw name */
	    	Rectangle2D currentBounds = new Rectangle2D.Double(
	    			dataDisplayBounds.x,
	    			dataDisplayBounds.y,
	    			dataDisplayBounds.getWidth(),
	    			nameLabel.getBounds().getHeight());
	    	if(drawPropertySeparators){
	    		Shape oldClip = g2.getClip();
	    		g2.setClip(getShape());
	    		g2.draw(new Rectangle2D.Double(
	    				getBounds().getX(),
		    			dataDisplayBounds.y,
		    			getBounds().getWidth(),
		    			nameLabel.getBounds().getHeight())
	    				);
	    		g2.setClip(oldClip);
	    	}
	    	nameLabel.draw(g2, currentBounds);
	    	
	    	/* draw internal properties */
	    	Rectangle2D previousBounds;
	    	for(int i=0;i<propertyLabels.length;i++){
	    		previousBounds = currentBounds;
	    		currentBounds = new Rectangle2D.Double(
	    				previousBounds.getX(),
	    				previousBounds.getMaxY(),
	    				dataDisplayBounds.getWidth(),
	    				propertyLabels[i].getBounds().getHeight()+boundsGap);
	    		if(drawPropertySeparators){
		    		Shape oldClip = g2.getClip();
		    		g2.setClip(getShape());
	    			g2.draw(new Rectangle2D.Double(
		    				getBounds().getX(),
			    			currentBounds.getY(),
			    			getBounds().getWidth(),
			    			currentBounds.getHeight())
		    				);
	    			g2.setClip(oldClip);
	    		}
	    		propertyLabels[i].draw(g2, currentBounds);
	    	}
	    }
	    	
		/* draw external properties */
		for(List<PropertyNode> pnList : propertyNodesMap.values())
			for(PropertyNode pn : pnList){
				pn.draw(g2);
				Direction d = new Direction( getBounds().getCenterX() - pn.getCenter().getX(), getBounds().getCenterY() - pn.getCenter().getY());
				g2.draw(new Line2D.Double(pn.getConnectionPoint(d), getConnectionPoint(d.turn(180))));
			}
		/* draw visual cue for bookmarks and notes */
		super.draw(g2);
	}
	
	protected Rectangle2D getMinBounds(){
		return (Rectangle2D)minBounds.clone();
	}
	
	public abstract  ShapeType getShapeType();

	@Override
	public void encode(Document doc, Element parent){
		super.encode(doc, parent);
		if(getProperties().isEmpty())
			 return;
		NodeList propTagList = doc.getElementsByTagName(PersistenceManager.PROPERTY);
		
		/* scan all the PROPERTY tags to add the position tag */
		for(int i = 0 ; i< propTagList.getLength(); i++){
			Element propertyTag = (Element)propTagList.item(i);
			Element typeTag = (Element)propertyTag.getElementsByTagName(PersistenceManager.TYPE).item(0);
			String type = typeTag.getTextContent();
			
			/* a property of another node, continue */
			if(!getProperties().getTypes().contains(type))
				continue;
			
			if(((PropertyView)getProperties().getView(type)).getPosition() == SimpleShapeNode.Position.Inside)
				continue;
			
			List<String> values = getProperties().getValues(type);
			if(values.isEmpty())
				continue;
			
			NodeList elementTagList = propertyTag.getElementsByTagName(PersistenceManager.ELEMENT);
			List<PropertyNode> pnList = propertyNodesMap.get(type);
			for(int j=0; j<elementTagList.getLength();j++){
				Element elementTag = (Element)elementTagList.item(j);
				Element positionTag = doc.createElement(SimpleShapePrototypePersistenceDelegate.POSITION);	
				positionTag.setAttribute(PersistenceManager.X, String.valueOf(pnList.get(j).getX()));
				positionTag.setAttribute(PersistenceManager.Y, String.valueOf(pnList.get(j).getY()));
				elementTag.appendChild(positionTag);
			}
		}
	}
	
	@Override
	public void decode(Document doc, Element nodeTag) throws IOException{
		super.decode(doc, nodeTag);
		
		NodeList propTagList = nodeTag.getElementsByTagName(PersistenceManager.PROPERTY);

		/* split the property types into internal and external, properties have been set by super.decodeXMLInstance  */
		ArrayList<String> insidePropertyTypes = new ArrayList<String>(getProperties().getTypes().size());
		ArrayList<String> outsidePropertyTypes = new ArrayList<String>(getProperties().getTypes().size());
		for(String type : getProperties().getTypes()){
			if(((PropertyView)getProperties().getView(type)).getPosition() == SimpleShapeNode.Position.Inside)
				insidePropertyTypes.add(type);
			else
				outsidePropertyTypes.add(type);
		}
		
		/* set the multi-line string bounds for the properties which are displayed internally */		
		reshapeInnerProperties(insidePropertyTypes);
		
		/* scan all the PROPERTY tags to decode the position tag of the properties which are displayed externally */
		for(int i = 0 ; i< propTagList.getLength(); i++){
			Element propertyTag = (Element)propTagList.item(i);
			if(propertyTag.getElementsByTagName(PersistenceManager.TYPE).item(0) == null)
				throw new IOException();
			Element typeTag = (Element)propertyTag.getElementsByTagName(PersistenceManager.TYPE).item(0);

			String type = typeTag.getTextContent();
			/* (check on whether type exists in the node type definition is done in super.decode */
			
			if(((PropertyView)getProperties().getView(type)).getPosition() == SimpleShapeNode.Position.Inside)
				continue;
			/* this will create external nodes and assign them their position */
			NodeList elementTagList = propertyTag.getElementsByTagName(PersistenceManager.ELEMENT);
			List<PropertyNode> pnList = new LinkedList<PropertyNode>();
			for(int j=0; j<elementTagList.getLength();j++){
				Element elementTag  = (Element)elementTagList.item(j);
				if(elementTag.getElementsByTagName(SimpleShapePrototypePersistenceDelegate.POSITION).item(0) == null ||
						elementTag.getElementsByTagName(PersistenceManager.VALUE).item(0) == null)
					throw new IOException();
				Element positionTag = (Element)elementTag.getElementsByTagName(SimpleShapePrototypePersistenceDelegate.POSITION).item(0);				
				Element valueTag = (Element)elementTag.getElementsByTagName(PersistenceManager.VALUE).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();
				}
				PropertyNode pn = new PropertyNode(((PropertyView)getProperties().getView(type)).getShapeType());
				pn.translate(dx, dy);
				pn.setText(valueTag.getTextContent(),null);
				pnList.add(pn);
			}
			propertyNodesMap.put(type, pnList);
			/* this will apply the modifier format to the properties */
			reshapeOuterProperties(outsidePropertyTypes);
		}
	}
	
	@Override
	protected void translateImplementation(Point2D p, double dx, double dy){
		dataDisplayBounds.setFrame(dataDisplayBounds.getX() + dx,
				dataDisplayBounds.getY() + dy, 
				dataDisplayBounds.getWidth(), 
				dataDisplayBounds.getHeight());
		/* translate all the external property nodes  */
		for(List<PropertyNode> pnList : propertyNodesMap.values())
			for(PropertyNode pn : pnList)
				pn.translate(dx, dy);
	}
	
	@Override
	public Object clone(){
		SimpleShapeNode n = (SimpleShapeNode)super.clone();
		n.propertyLabels = new MultiLineString[propertyLabels.length];
		n.nameLabel = new MultiLineString();
		n.propertyNodesMap = new LinkedHashMap<String, List<PropertyNode>>();
		for(String s : propertyNodesMap.keySet())
			n.propertyNodesMap.put(s, new LinkedList<PropertyNode>());
		n.dataDisplayBounds = (Rectangle2D.Double)dataDisplayBounds.clone();
		return n;
	}
	
	protected boolean anyInsideProperties(){
		boolean propInside = false;
		for(String type : getProperties().getTypes()){
	    	if(((PropertyView)getProperties().getView(type)).getPosition() == Position.Inside){
	    		if(!getProperties().getValues(type).isEmpty()){
	    			propInside = true;
	    			break;
	    		}
	    	}
	    }
		return propInside;
	}
	
	protected Rectangle2D.Double dataDisplayBounds;
	protected double boundsGap; 
	protected boolean drawPropertySeparators = true;
	protected MultiLineString[] propertyLabels;
	protected MultiLineString nameLabel;
	protected Map<String,List<PropertyNode>> propertyNodesMap;
	
	public static enum ShapeType  {Circle, Ellipse, Rectangle, Square, Triangle, Transparent};
	public static enum Position  {Inside, Outside};
	
	private static final int DEFAULT_WIDTH = 100;
    private static final int DEFAULT_HEIGHT = 60;
	private static final Rectangle2D.Double minBounds = new Rectangle2D.Double(0,0,DEFAULT_WIDTH,DEFAULT_HEIGHT);
	private static final int PROP_NODE_DIST = 50;
	
	protected static class PropertyNode{
		public PropertyNode(ShapeType aShape){
			/* add a little padding in the shape holding the label */
			label = new MultiLineString(){
				 public Rectangle2D getBounds(){
					 Rectangle2D bounds = super.getBounds();
					 if(bounds.getWidth() != 0 || bounds.getHeight() != 0){
						 bounds.setFrame(
								 bounds.getX(),
								 bounds.getY(), 
								 bounds.getWidth() + PADDING, 
								 bounds.getHeight() + PADDING);
					 }
					 return bounds;
				 }
			};
			label.setJustification(MultiLineString.CENTER);
			shapeType = aShape;
			shape  = label.getBounds();
		}
		
		public void setText(String text, ModifierView[] views){
			label.setText(text,views);
			
			switch(shapeType){
			case Circle :
				Rectangle2D circleBounds = EllipticalNode.getOutBounds(label.getBounds());	
				shape = new Ellipse2D.Double(
						circleBounds.getX(),
						circleBounds.getY(),
						Math.max(circleBounds.getWidth(),circleBounds.getHeight()),
						Math.max(circleBounds.getWidth(),circleBounds.getHeight())
					);
				break;
			case Ellipse :
				Rectangle2D ellipseBounds = EllipticalNode.getOutBounds(label.getBounds());
				shape = new Ellipse2D.Double(
							ellipseBounds.getX(),
							ellipseBounds.getY(),
							ellipseBounds.getWidth(),
							ellipseBounds.getHeight()
						);
				break;
			case Triangle : 
				shape = TriangularNode.getOutShape(label.getBounds());
				break;
			default : // Rectangle, Square and Transparent 
				shape = label.getBounds();;
				break;
			}
			
			/* a new shape, placed at (0,0) has been created as a result of set text, therefore  *
			 * we must put it back where the old shape was, since the translation is performed   * 
			 * by adding the translate args to x and y, x and y must first be set to 0 			 */
			double currentX = x;
			double currentY = y;
			x = 0;
			y = 0;
			translate(currentX,currentY);
		}
		
		public void draw(Graphics2D g){
			Color oldColor = g.getColor();
			if(shapeType != ShapeType.Transparent){
				g.translate(SHADOW_GAP, SHADOW_GAP);      
				g.setColor(SHADOW_COLOR);
				g.fill(shape);
				g.translate(-SHADOW_GAP, -SHADOW_GAP);
				
				g.setColor(g.getBackground());
				g.fill(shape);
				g.setColor(Color.BLACK);
				g.draw(shape);
			}
			
			label.draw(g, shape.getBounds2D());		
			g.setColor(oldColor);
		}
		
		public void translate(double dx, double dy){
			x += dx;
			y += dy;
			
			if(shape instanceof Path2D){ //it's a triangle
				Rectangle2D labelBounds = label.getBounds();
				labelBounds.setFrame(
						x,
						y,
						labelBounds.getWidth(),
						labelBounds.getHeight()
						);
				shape = TriangularNode.getOutShape(labelBounds);
			}else{
				Rectangle2D bounds = shape.getBounds2D();
				((RectangularShape)shape).setFrame(
						x,
						y,
						bounds.getWidth(),
						bounds.getHeight()
				);
			}
		}
		
		public Point2D getConnectionPoint(Direction d) {
			switch(shapeType){
			case Circle :
			case Ellipse :
				return EllipticalNode.calculateConnectionPoint(d, shape.getBounds2D());
			case Triangle :
				return TriangularNode.calculateConnectionPoint(d, shape.getBounds2D());
			default :
				return RectangularNode.calculateConnectionPoint(d, shape.getBounds2D());
			}
		}
		
		public boolean contains(Point2D p){
			return shape.contains(p);
		}
		
		public Point2D getCenter(){
			Rectangle2D bounds =  shape.getBounds2D() ;
			return new Point2D.Double(bounds.getCenterX(), bounds.getCenterY());
		}
		
		double getX(){
			return x;
		}
		
		double getY(){
			return y;
		}
		
		private static final int PADDING = 5;
		private MultiLineString label;
		private ShapeType shapeType;
		private Shape shape;
		private double x;
		private double y;
	}
}