fiore@0: /*
fiore@0: CCmI Editor - A Collaborative Cross-Modal Diagram Editing Tool
fiore@0:
fiore@0: Copyright (C) 2011 Queen Mary University of London (http://ccmi.eecs.qmul.ac.uk/)
fiore@0:
fiore@0: This program is free software: you can redistribute it and/or modify
fiore@0: it under the terms of the GNU General Public License as published by
fiore@0: the Free Software Foundation, either version 3 of the License, or
fiore@0: (at your option) any later version.
fiore@0:
fiore@0: This program is distributed in the hope that it will be useful,
fiore@0: but WITHOUT ANY WARRANTY; without even the implied warranty of
fiore@0: MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
fiore@0: GNU General Public License for more details.
fiore@0:
fiore@0: You should have received a copy of the GNU General Public License
fiore@0: along with this program. If not, see .
fiore@0: */
fiore@0:
fiore@0: package uk.ac.qmul.eecs.ccmi.simpletemplate;
fiore@0:
fiore@0: import java.awt.Color;
fiore@0: import java.awt.Graphics2D;
fiore@0: import java.awt.Shape;
fiore@0: import java.awt.geom.Ellipse2D;
fiore@0: import java.awt.geom.Line2D;
fiore@0: import java.awt.geom.Path2D;
fiore@0: import java.awt.geom.Point2D;
fiore@0: import java.awt.geom.Rectangle2D;
fiore@0: import java.awt.geom.RectangularShape;
fiore@0: import java.io.IOException;
fiore@0: import java.util.ArrayList;
fiore@0: import java.util.LinkedHashMap;
fiore@0: import java.util.LinkedList;
fiore@0: import java.util.List;
fiore@0: import java.util.Map;
fiore@0: import java.util.Set;
fiore@0:
fiore@0: import org.w3c.dom.Document;
fiore@0: import org.w3c.dom.Element;
fiore@0: import org.w3c.dom.NodeList;
fiore@0:
fiore@0: import uk.ac.qmul.eecs.ccmi.diagrammodel.ElementChangedEvent;
fiore@0: import uk.ac.qmul.eecs.ccmi.diagrammodel.NodeProperties;
fiore@0: import uk.ac.qmul.eecs.ccmi.gui.Direction;
fiore@0: import uk.ac.qmul.eecs.ccmi.gui.Node;
fiore@0: import uk.ac.qmul.eecs.ccmi.gui.persistence.PersistenceManager;
fiore@0: import uk.ac.qmul.eecs.ccmi.utils.Pair;
fiore@0:
fiore@0: /**
fiore@0: *
fiore@0: * A diagram node that can be represented visually as a simple shape such as
fiore@0: * a rectangle, square, circle, ellipse or triangle.
fiore@0: *
fiore@0: */
fiore@0: @SuppressWarnings("serial")
fiore@0: public abstract class SimpleShapeNode extends Node {
fiore@0:
fiore@0: public static SimpleShapeNode getInstance(ShapeType shapeType, String typeName, NodeProperties properties){
fiore@0: switch(shapeType){
fiore@0: case Rectangle :
fiore@0: return new RectangularNode(typeName, properties);
fiore@0: case Square :
fiore@0: return new SquareNode(typeName, properties);
fiore@0: case Circle :
fiore@0: return new CircleNode(typeName, properties);
fiore@0: case Ellipse :
fiore@0: return new EllipticalNode(typeName, properties);
fiore@0: case Triangle :
fiore@0: return new TriangularNode(typeName, properties);
fiore@0: }
fiore@0: return null;
fiore@0: }
fiore@0:
fiore@0: protected SimpleShapeNode(String typeName, NodeProperties properties){
fiore@0: super(typeName, properties);
fiore@0: dataDisplayBounds = (Rectangle2D.Double)getMinBounds();
fiore@0: /* Initialise the data structures for displaying the properties inside and outside */
fiore@0: propertyNodesMap = new LinkedHashMap>();
fiore@0: int numInsideProperties = 0;
fiore@0: for(String type : getProperties().getTypes()){
fiore@0: if(((PropertyView)getProperties().getView(type)).getPosition() == Position.Inside)
fiore@0: numInsideProperties++;
fiore@0: else
fiore@0: propertyNodesMap.put(type, new LinkedList());
fiore@0: }
fiore@0: propertyLabels = new MultiLineString[numInsideProperties];
fiore@0: nameLabel = new MultiLineString();
fiore@0: }
fiore@0:
fiore@0: @Override
fiore@0: protected void notifyChange(ElementChangedEvent evt){
fiore@4: if(!evt.getChangeType().equals("translate")&&!evt.getChangeType().equals("stop_move")){ //don't reshape for just moving
fiore@4: Rectangle2D boundsBeforeReshape = getBounds();
fiore@4: reshape();
fiore@4: Rectangle2D boundsAfterReshape = getBounds();
fiore@4: /* after renaming or setting properties the boundaries can change resulting in a slight shift of the *
fiore@4: * node centre from its original position. the next line is to place it back to the right position */
fiore@4: Point2D start = new Point2D.Double(boundsAfterReshape.getCenterX(),boundsAfterReshape.getCenterY());
fiore@4: translateImplementation(start,
fiore@5: boundsBeforeReshape.getCenterX() - boundsAfterReshape.getCenterX(),
fiore@5: boundsBeforeReshape.getCenterY() - boundsAfterReshape.getCenterY());
fiore@4: }
fiore@0: super.notifyChange(evt);
fiore@0: }
fiore@0:
fiore@0: @Override
fiore@0: public void setId(long id){
fiore@0: super.setId(id);
fiore@0: /* when they are given an id nodes change name into "new node " *
fiore@0: * where is the actual type of the node and is the given id *
fiore@0: * therefore a reshape is necessary to display the new name */
fiore@0: Rectangle2D boundsBeforeReshape = getBounds();
fiore@0: reshape();
fiore@0: /* the reshape might change the bounds, so the shape is translated so that the top-left *
fiore@0: * point is at the same position as before just to keep it more consistent */
fiore@0: Rectangle2D boundsAfterReshape = getBounds();
fiore@0: translateImplementation(
fiore@5: new Point2D.Double(),
fiore@5: boundsBeforeReshape.getX() - boundsAfterReshape.getX(),
fiore@5: boundsBeforeReshape.getY() - boundsAfterReshape.getY()
fiore@4: );
fiore@0: }
fiore@0:
fiore@0: @Override
fiore@0: public boolean contains(Point2D p) {
fiore@0: if (getShape().contains(p))
fiore@0: return true;
fiore@0: for(List pnList : propertyNodesMap.values())
fiore@0: for(PropertyNode pn : pnList)
fiore@0: if(pn.contains(p))
fiore@0: return true;
fiore@0: return false;
fiore@0: }
fiore@0:
fiore@0: protected void reshape(){
fiore@0: Pair, List> splitPropertyTypes = splitPropertyTypes();
fiore@0: /* properties displayed internally */
fiore@0: reshapeInnerProperties(splitPropertyTypes.first);
fiore@0: /* properties displayed externally */
fiore@0: reshapeOuterProperties(splitPropertyTypes.second);
fiore@0: }
fiore@0:
fiore@0: protected Pair, List> splitPropertyTypes(){
fiore@0: List types = getProperties().getTypes();
fiore@0: ArrayList insidePropertyTypes = new ArrayList(types.size());
fiore@0: ArrayList outsidePropertyTypes = new ArrayList(types.size());
fiore@0: for(String type : types){
fiore@0: if(((PropertyView)getProperties().getView(type)).getPosition() == Position.Inside)
fiore@0: insidePropertyTypes.add(type);
fiore@0: else
fiore@0: outsidePropertyTypes.add(type);
fiore@0: }
fiore@0:
fiore@0: return new Pair, List> (insidePropertyTypes,outsidePropertyTypes);
fiore@0: }
fiore@0:
fiore@0: protected void reshapeOuterProperties(List outsidePropertyTypes){
fiore@0: for(String type : outsidePropertyTypes){
fiore@0: List propertyNodes = propertyNodesMap.get(type);
fiore@0: List propertyValues = getProperties().getValues(type);
fiore@0: int diff = propertyNodes.size()-propertyValues.size();
fiore@0: if(diff > 0) // properties have been removed
fiore@0: for(int i=0; i < diff; i++)
fiore@0: propertyNodes.remove(propertyNodes.size() - 1);
fiore@0: else if(diff < 0){ // properties have been added. We need more properties node.
fiore@0: for(int i=0; i < -diff; i++){
fiore@0: PropertyNode propertyNode = new PropertyNode(((PropertyView)getProperties().getView(type)).getShapeType());
fiore@0: Rectangle2D bounds = getBounds();
fiore@0: double x = bounds.getCenterX() - bounds.getWidth()/2 - PROP_NODE_DIST;
fiore@0: double y = bounds.getCenterX() - PROP_NODE_DIST * i;
fiore@0: propertyNode.translate(x, y);
fiore@0: propertyNodes.add(propertyNode);
fiore@0: }
fiore@0: }
fiore@0: /* set the text on all the property nodes */
fiore@0: int i = 0;
fiore@0: for(String text : propertyValues){
fiore@0: NodeProperties.Modifiers modifiers = getProperties().getModifiers(type);
fiore@0: Set viewIndexes = modifiers.getIndexes(i);
fiore@0: ModifierView[] views = new ModifierView[viewIndexes.size()];
fiore@0: int j =0;
fiore@0: for(Integer I : viewIndexes){
fiore@0: views[j] = (ModifierView) getProperties().getModifiers(type).getView(modifiers.getTypes().get(I));
fiore@0: j++;
fiore@0: }
fiore@0: propertyNodes.get(i).setText(text,views);
fiore@0: i++;
fiore@0: }
fiore@0: }
fiore@0: }
fiore@0:
fiore@0: protected void reshapeInnerProperties(List insidePropertyTypes){
fiore@0: /* set the bounds for each multiline string and the resulting bound of the node */
fiore@0: nameLabel = new MultiLineString();
fiore@0: nameLabel.setText(getName().isEmpty() ? " " : getName());
fiore@0: nameLabel.setBold(true);
fiore@0: Rectangle2D r = nameLabel.getBounds();
fiore@0:
fiore@0: for(int i=0; i pnList : propertyNodesMap.values())
fiore@0: for(PropertyNode pn : pnList){
fiore@0: pn.draw(g2);
fiore@0: Direction d = new Direction( getBounds().getCenterX() - pn.getCenter().getX(), getBounds().getCenterY() - pn.getCenter().getY());
fiore@0: g2.draw(new Line2D.Double(pn.getConnectionPoint(d), getConnectionPoint(d.turn(180))));
fiore@0: }
fiore@0: /* draw visual cue for bookmarks and notes */
fiore@0: super.draw(g2);
fiore@0: }
fiore@0:
fiore@0: protected Rectangle2D getMinBounds(){
fiore@0: return (Rectangle2D)minBounds.clone();
fiore@0: }
fiore@0:
fiore@0: public abstract ShapeType getShapeType();
fiore@0:
fiore@0: @Override
fiore@0: public void encode(Document doc, Element parent){
fiore@0: super.encode(doc, parent);
fiore@0: if(getProperties().isEmpty())
fiore@0: return;
fiore@0: NodeList propTagList = doc.getElementsByTagName(PersistenceManager.PROPERTY);
fiore@0:
fiore@0: /* scan all the PROPERTY tags to add the position tag */
fiore@0: for(int i = 0 ; i< propTagList.getLength(); i++){
fiore@0: Element propertyTag = (Element)propTagList.item(i);
fiore@0: Element typeTag = (Element)propertyTag.getElementsByTagName(PersistenceManager.TYPE).item(0);
fiore@0: String type = typeTag.getTextContent();
fiore@0:
fiore@0: /* a property of another node, continue */
fiore@0: if(!getProperties().getTypes().contains(type))
fiore@0: continue;
fiore@0:
fiore@0: if(((PropertyView)getProperties().getView(type)).getPosition() == SimpleShapeNode.Position.Inside)
fiore@0: continue;
fiore@0:
fiore@0: List values = getProperties().getValues(type);
fiore@0: if(values.isEmpty())
fiore@0: continue;
fiore@0:
fiore@0: NodeList elementTagList = propertyTag.getElementsByTagName(PersistenceManager.ELEMENT);
fiore@0: List pnList = propertyNodesMap.get(type);
fiore@0: for(int j=0; j insidePropertyTypes = new ArrayList(getProperties().getTypes().size());
fiore@0: ArrayList outsidePropertyTypes = new ArrayList(getProperties().getTypes().size());
fiore@0: for(String type : getProperties().getTypes()){
fiore@0: if(((PropertyView)getProperties().getView(type)).getPosition() == SimpleShapeNode.Position.Inside)
fiore@0: insidePropertyTypes.add(type);
fiore@0: else
fiore@0: outsidePropertyTypes.add(type);
fiore@0: }
fiore@0:
fiore@0: /* set the multi-line string bounds for the properties which are displayed internally */
fiore@0: reshapeInnerProperties(insidePropertyTypes);
fiore@0:
fiore@0: /* scan all the PROPERTY tags to decode the position tag of the properties which are displayed externally */
fiore@0: for(int i = 0 ; i< propTagList.getLength(); i++){
fiore@0: Element propertyTag = (Element)propTagList.item(i);
fiore@0: if(propertyTag.getElementsByTagName(PersistenceManager.TYPE).item(0) == null)
fiore@0: throw new IOException();
fiore@0: Element typeTag = (Element)propertyTag.getElementsByTagName(PersistenceManager.TYPE).item(0);
fiore@0:
fiore@0: String type = typeTag.getTextContent();
fiore@0: /* (check on whether type exists in the node type definition is done in super.decode */
fiore@0:
fiore@0: if(((PropertyView)getProperties().getView(type)).getPosition() == SimpleShapeNode.Position.Inside)
fiore@0: continue;
fiore@0: /* this will create external nodes and assign them their position */
fiore@0: NodeList elementTagList = propertyTag.getElementsByTagName(PersistenceManager.ELEMENT);
fiore@0: List pnList = new LinkedList();
fiore@0: for(int j=0; j pnList : propertyNodesMap.values())
fiore@0: for(PropertyNode pn : pnList)
fiore@0: pn.translate(dx, dy);
fiore@0: }
fiore@0:
fiore@0: @Override
fiore@0: public Object clone(){
fiore@0: SimpleShapeNode n = (SimpleShapeNode)super.clone();
fiore@0: n.propertyLabels = new MultiLineString[propertyLabels.length];
fiore@0: n.nameLabel = new MultiLineString();
fiore@0: n.propertyNodesMap = new LinkedHashMap>();
fiore@0: for(String s : propertyNodesMap.keySet())
fiore@0: n.propertyNodesMap.put(s, new LinkedList());
fiore@0: n.dataDisplayBounds = (Rectangle2D.Double)dataDisplayBounds.clone();
fiore@0: return n;
fiore@0: }
fiore@0:
fiore@0: protected boolean anyInsideProperties(){
fiore@0: boolean propInside = false;
fiore@0: for(String type : getProperties().getTypes()){
fiore@0: if(((PropertyView)getProperties().getView(type)).getPosition() == Position.Inside){
fiore@0: if(!getProperties().getValues(type).isEmpty()){
fiore@0: propInside = true;
fiore@0: break;
fiore@0: }
fiore@0: }
fiore@0: }
fiore@0: return propInside;
fiore@0: }
fiore@0:
fiore@0: protected Rectangle2D.Double dataDisplayBounds;
fiore@0: protected double boundsGap;
fiore@0: protected boolean drawPropertySeparators = true;
fiore@0: protected MultiLineString[] propertyLabels;
fiore@0: protected MultiLineString nameLabel;
fiore@0: protected Map> propertyNodesMap;
fiore@0:
fiore@0: public static enum ShapeType {Circle, Ellipse, Rectangle, Square, Triangle, Transparent};
fiore@0: public static enum Position {Inside, Outside};
fiore@0:
fiore@0: private static final int DEFAULT_WIDTH = 100;
fiore@0: private static final int DEFAULT_HEIGHT = 60;
fiore@0: private static final Rectangle2D.Double minBounds = new Rectangle2D.Double(0,0,DEFAULT_WIDTH,DEFAULT_HEIGHT);
fiore@0: private static final int PROP_NODE_DIST = 50;
fiore@0:
fiore@6: /**
fiore@6: * When properties are configure to appear outside, the values are represented as small nodes
fiore@6: * connected (with a straight line) to the node they belong to. This class represents such
fiore@6: * small nodes. The possible shapes are: triangle, rectangle, square, circle, ellipse and no shape in
fiore@6: * which case only the string with the property value and the line connecting it to the node is shown.
fiore@6: *
fiore@6: */
fiore@0: protected static class PropertyNode{
fiore@0: public PropertyNode(ShapeType aShape){
fiore@0: /* add a little padding in the shape holding the label */
fiore@0: label = new MultiLineString(){
fiore@0: public Rectangle2D getBounds(){
fiore@0: Rectangle2D bounds = super.getBounds();
fiore@0: if(bounds.getWidth() != 0 || bounds.getHeight() != 0){
fiore@0: bounds.setFrame(
fiore@0: bounds.getX(),
fiore@0: bounds.getY(),
fiore@0: bounds.getWidth() + PADDING,
fiore@0: bounds.getHeight() + PADDING);
fiore@0: }
fiore@0: return bounds;
fiore@0: }
fiore@0: };
fiore@0: label.setJustification(MultiLineString.CENTER);
fiore@0: shapeType = aShape;
fiore@0: shape = label.getBounds();
fiore@0: }
fiore@0:
fiore@0: public void setText(String text, ModifierView[] views){
fiore@0: label.setText(text,views);
fiore@0:
fiore@0: switch(shapeType){
fiore@0: case Circle :
fiore@0: Rectangle2D circleBounds = EllipticalNode.getOutBounds(label.getBounds());
fiore@0: shape = new Ellipse2D.Double(
fiore@0: circleBounds.getX(),
fiore@0: circleBounds.getY(),
fiore@0: Math.max(circleBounds.getWidth(),circleBounds.getHeight()),
fiore@0: Math.max(circleBounds.getWidth(),circleBounds.getHeight())
fiore@0: );
fiore@0: break;
fiore@0: case Ellipse :
fiore@0: Rectangle2D ellipseBounds = EllipticalNode.getOutBounds(label.getBounds());
fiore@0: shape = new Ellipse2D.Double(
fiore@0: ellipseBounds.getX(),
fiore@0: ellipseBounds.getY(),
fiore@0: ellipseBounds.getWidth(),
fiore@0: ellipseBounds.getHeight()
fiore@0: );
fiore@0: break;
fiore@0: case Triangle :
fiore@0: shape = TriangularNode.getOutShape(label.getBounds());
fiore@0: break;
fiore@0: default : // Rectangle, Square and Transparent
fiore@0: shape = label.getBounds();;
fiore@0: break;
fiore@0: }
fiore@0:
fiore@0: /* a new shape, placed at (0,0) has been created as a result of set text, therefore *
fiore@0: * we must put it back where the old shape was, since the translation is performed *
fiore@0: * by adding the translate args to x and y, x and y must first be set to 0 */
fiore@0: double currentX = x;
fiore@0: double currentY = y;
fiore@0: x = 0;
fiore@0: y = 0;
fiore@0: translate(currentX,currentY);
fiore@0: }
fiore@0:
fiore@0: public void draw(Graphics2D g){
fiore@0: Color oldColor = g.getColor();
fiore@0: if(shapeType != ShapeType.Transparent){
fiore@0: g.translate(SHADOW_GAP, SHADOW_GAP);
fiore@0: g.setColor(SHADOW_COLOR);
fiore@0: g.fill(shape);
fiore@0: g.translate(-SHADOW_GAP, -SHADOW_GAP);
fiore@0:
fiore@0: g.setColor(g.getBackground());
fiore@0: g.fill(shape);
fiore@0: g.setColor(Color.BLACK);
fiore@0: g.draw(shape);
fiore@0: }
fiore@0:
fiore@0: label.draw(g, shape.getBounds2D());
fiore@0: g.setColor(oldColor);
fiore@0: }
fiore@0:
fiore@0: public void translate(double dx, double dy){
fiore@0: x += dx;
fiore@0: y += dy;
fiore@0:
fiore@0: if(shape instanceof Path2D){ //it's a triangle
fiore@0: Rectangle2D labelBounds = label.getBounds();
fiore@0: labelBounds.setFrame(
fiore@0: x,
fiore@0: y,
fiore@0: labelBounds.getWidth(),
fiore@0: labelBounds.getHeight()
fiore@0: );
fiore@0: shape = TriangularNode.getOutShape(labelBounds);
fiore@0: }else{
fiore@0: Rectangle2D bounds = shape.getBounds2D();
fiore@0: ((RectangularShape)shape).setFrame(
fiore@0: x,
fiore@0: y,
fiore@0: bounds.getWidth(),
fiore@0: bounds.getHeight()
fiore@0: );
fiore@0: }
fiore@0: }
fiore@0:
fiore@0: public Point2D getConnectionPoint(Direction d) {
fiore@0: switch(shapeType){
fiore@0: case Circle :
fiore@0: case Ellipse :
fiore@0: return EllipticalNode.calculateConnectionPoint(d, shape.getBounds2D());
fiore@0: case Triangle :
fiore@0: return TriangularNode.calculateConnectionPoint(d, shape.getBounds2D());
fiore@0: default :
fiore@0: return RectangularNode.calculateConnectionPoint(d, shape.getBounds2D());
fiore@0: }
fiore@0: }
fiore@0:
fiore@0: public boolean contains(Point2D p){
fiore@0: return shape.contains(p);
fiore@0: }
fiore@0:
fiore@0: public Point2D getCenter(){
fiore@0: Rectangle2D bounds = shape.getBounds2D() ;
fiore@0: return new Point2D.Double(bounds.getCenterX(), bounds.getCenterY());
fiore@0: }
fiore@0:
fiore@0: double getX(){
fiore@0: return x;
fiore@0: }
fiore@0:
fiore@0: double getY(){
fiore@0: return y;
fiore@0: }
fiore@0:
fiore@0: private static final int PADDING = 5;
fiore@0: private MultiLineString label;
fiore@0: private ShapeType shapeType;
fiore@0: private Shape shape;
fiore@0: private double x;
fiore@0: private double y;
fiore@0: }
fiore@0: }