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: }