Mercurial > hg > accesspd
diff java/src/uk/ac/qmul/eecs/ccmi/simpletemplate/SimpleShapeNode.java @ 0:78b7fc5391a2
first import, outcome of NIME 2014 hackaton
author | Fiore Martin <f.martin@qmul.ac.uk> |
---|---|
date | Tue, 08 Jul 2014 16:28:59 +0100 |
parents | |
children |
line wrap: on
line diff
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/java/src/uk/ac/qmul/eecs/ccmi/simpletemplate/SimpleShapeNode.java Tue Jul 08 16:28:59 2014 +0100 @@ -0,0 +1,600 @@ +/* + 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 + Rectangle2D boundsBeforeReshape = getBounds(); + reshape(); + Rectangle2D boundsAfterReshape = getBounds(); + /* after renaming or setting properties the boundaries can change resulting in a slight shift of the * + * node centre from its original position. the next line is to place it back to the right position */ + Point2D start = new Point2D.Double(boundsAfterReshape.getCenterX(),boundsAfterReshape.getCenterY()); + translateImplementation(start, + boundsBeforeReshape.getCenterX() - boundsAfterReshape.getCenterX(), + boundsBeforeReshape.getCenterY() - boundsAfterReshape.getCenterY()); + } + 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; + + /** + * When properties are configure to appear outside, the values are represented as small nodes + * connected (with a straight line) to the node they belong to. This class represents such + * small nodes. The possible shapes are: triangle, rectangle, square, circle, ellipse and no shape in + * which case only the string with the property value and the line connecting it to the node is shown. + * + */ + 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; + } +}