Mercurial > hg > ccmieditor
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; } }