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