comparison 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
comparison
equal deleted inserted replaced
-1:000000000000 0:9418ab7b7f3f
1 /*
2 CCmI Editor - A Collaborative Cross-Modal Diagram Editing Tool
3
4 Copyright (C) 2011 Queen Mary University of London (http://ccmi.eecs.qmul.ac.uk/)
5
6 This program is free software: you can redistribute it and/or modify
7 it under the terms of the GNU General Public License as published by
8 the Free Software Foundation, either version 3 of the License, or
9 (at your option) any later version.
10
11 This program is distributed in the hope that it will be useful,
12 but WITHOUT ANY WARRANTY; without even the implied warranty of
13 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14 GNU General Public License for more details.
15
16 You should have received a copy of the GNU General Public License
17 along with this program. If not, see <http://www.gnu.org/licenses/>.
18 */
19
20 package uk.ac.qmul.eecs.ccmi.simpletemplate;
21
22 import java.awt.Color;
23 import java.awt.Graphics2D;
24 import java.awt.Shape;
25 import java.awt.geom.Ellipse2D;
26 import java.awt.geom.Line2D;
27 import java.awt.geom.Path2D;
28 import java.awt.geom.Point2D;
29 import java.awt.geom.Rectangle2D;
30 import java.awt.geom.RectangularShape;
31 import java.io.IOException;
32 import java.util.ArrayList;
33 import java.util.LinkedHashMap;
34 import java.util.LinkedList;
35 import java.util.List;
36 import java.util.Map;
37 import java.util.Set;
38
39 import org.w3c.dom.Document;
40 import org.w3c.dom.Element;
41 import org.w3c.dom.NodeList;
42
43 import uk.ac.qmul.eecs.ccmi.diagrammodel.ElementChangedEvent;
44 import uk.ac.qmul.eecs.ccmi.diagrammodel.NodeProperties;
45 import uk.ac.qmul.eecs.ccmi.gui.Direction;
46 import uk.ac.qmul.eecs.ccmi.gui.Node;
47 import uk.ac.qmul.eecs.ccmi.gui.persistence.PersistenceManager;
48 import uk.ac.qmul.eecs.ccmi.utils.Pair;
49
50 /**
51 *
52 * A diagram node that can be represented visually as a simple shape such as
53 * a rectangle, square, circle, ellipse or triangle.
54 *
55 */
56 @SuppressWarnings("serial")
57 public abstract class SimpleShapeNode extends Node {
58
59 public static SimpleShapeNode getInstance(ShapeType shapeType, String typeName, NodeProperties properties){
60 switch(shapeType){
61 case Rectangle :
62 return new RectangularNode(typeName, properties);
63 case Square :
64 return new SquareNode(typeName, properties);
65 case Circle :
66 return new CircleNode(typeName, properties);
67 case Ellipse :
68 return new EllipticalNode(typeName, properties);
69 case Triangle :
70 return new TriangularNode(typeName, properties);
71 }
72 return null;
73 }
74
75 protected SimpleShapeNode(String typeName, NodeProperties properties){
76 super(typeName, properties);
77 dataDisplayBounds = (Rectangle2D.Double)getMinBounds();
78 /* Initialise the data structures for displaying the properties inside and outside */
79 propertyNodesMap = new LinkedHashMap<String,List<PropertyNode>>();
80 int numInsideProperties = 0;
81 for(String type : getProperties().getTypes()){
82 if(((PropertyView)getProperties().getView(type)).getPosition() == Position.Inside)
83 numInsideProperties++;
84 else
85 propertyNodesMap.put(type, new LinkedList<PropertyNode>());
86 }
87 propertyLabels = new MultiLineString[numInsideProperties];
88 nameLabel = new MultiLineString();
89 }
90
91 @Override
92 protected void notifyChange(ElementChangedEvent evt){
93 if(!evt.getChangeType().equals("translate")&&!evt.getChangeType().equals("stop_move")) //don't reshape for just moving
94 reshape();
95 super.notifyChange(evt);
96 }
97
98 @Override
99 public void setId(long id){
100 super.setId(id);
101 /* when they are given an id nodes change name into "new <type> node <id>" *
102 * where <type> is the actual type of the node and <id> is the given id *
103 * therefore a reshape is necessary to display the new name */
104 Rectangle2D boundsBeforeReshape = getBounds();
105 reshape();
106 /* the reshape might change the bounds, so the shape is translated so that the top-left *
107 * point is at the same position as before just to keep it more consistent */
108 Rectangle2D boundsAfterReshape = getBounds();
109 translateImplementation(
110 new Point2D.Double(),
111 boundsBeforeReshape.getX() - boundsAfterReshape.getX(),
112 boundsBeforeReshape.getY() - boundsAfterReshape.getY()
113 );
114 }
115
116 @Override
117 public boolean contains(Point2D p) {
118 if (getShape().contains(p))
119 return true;
120 for(List<PropertyNode> pnList : propertyNodesMap.values())
121 for(PropertyNode pn : pnList)
122 if(pn.contains(p))
123 return true;
124 return false;
125 }
126
127 protected void reshape(){
128 Pair<List<String>, List<String>> splitPropertyTypes = splitPropertyTypes();
129 /* properties displayed internally */
130 reshapeInnerProperties(splitPropertyTypes.first);
131 /* properties displayed externally */
132 reshapeOuterProperties(splitPropertyTypes.second);
133 }
134
135 protected Pair<List<String>, List<String>> splitPropertyTypes(){
136 List<String> types = getProperties().getTypes();
137 ArrayList<String> insidePropertyTypes = new ArrayList<String>(types.size());
138 ArrayList<String> outsidePropertyTypes = new ArrayList<String>(types.size());
139 for(String type : types){
140 if(((PropertyView)getProperties().getView(type)).getPosition() == Position.Inside)
141 insidePropertyTypes.add(type);
142 else
143 outsidePropertyTypes.add(type);
144 }
145
146 return new Pair<List<String>, List<String>> (insidePropertyTypes,outsidePropertyTypes);
147 }
148
149 protected void reshapeOuterProperties(List<String> outsidePropertyTypes){
150 for(String type : outsidePropertyTypes){
151 List<PropertyNode> propertyNodes = propertyNodesMap.get(type);
152 List<String> propertyValues = getProperties().getValues(type);
153 int diff = propertyNodes.size()-propertyValues.size();
154 if(diff > 0) // properties have been removed
155 for(int i=0; i < diff; i++)
156 propertyNodes.remove(propertyNodes.size() - 1);
157 else if(diff < 0){ // properties have been added. We need more properties node.
158 for(int i=0; i < -diff; i++){
159 PropertyNode propertyNode = new PropertyNode(((PropertyView)getProperties().getView(type)).getShapeType());
160 Rectangle2D bounds = getBounds();
161 double x = bounds.getCenterX() - bounds.getWidth()/2 - PROP_NODE_DIST;
162 double y = bounds.getCenterX() - PROP_NODE_DIST * i;
163 propertyNode.translate(x, y);
164 propertyNodes.add(propertyNode);
165 }
166 }
167 /* set the text on all the property nodes */
168 int i = 0;
169 for(String text : propertyValues){
170 NodeProperties.Modifiers modifiers = getProperties().getModifiers(type);
171 Set<Integer> viewIndexes = modifiers.getIndexes(i);
172 ModifierView[] views = new ModifierView[viewIndexes.size()];
173 int j =0;
174 for(Integer I : viewIndexes){
175 views[j] = (ModifierView) getProperties().getModifiers(type).getView(modifiers.getTypes().get(I));
176 j++;
177 }
178 propertyNodes.get(i).setText(text,views);
179 i++;
180 }
181 }
182 }
183
184 protected void reshapeInnerProperties(List<String> insidePropertyTypes){
185 /* set the bounds for each multiline string and the resulting bound of the node */
186 nameLabel = new MultiLineString();
187 nameLabel.setText(getName().isEmpty() ? " " : getName());
188 nameLabel.setBold(true);
189 Rectangle2D r = nameLabel.getBounds();
190
191 for(int i=0; i<insidePropertyTypes.size();i++){
192 propertyLabels[i] = new MultiLineString();
193 String propertyType = insidePropertyTypes.get(i);
194 if(getProperties().getValues(propertyType).size() == 0){
195 propertyLabels[i].setText(" ");
196 }else{
197 propertyLabels[i].setJustification(MultiLineString.LEFT);
198 String[] array = new String[getProperties().getValues(propertyType).size()];
199 propertyLabels[i].setText(getProperties().getValues(propertyType).toArray(array), getProperties().getModifiers(propertyType));
200 }
201 r.add(new Rectangle2D.Double(r.getX(),r.getMaxY(),propertyLabels[i].getBounds().getWidth(),propertyLabels[i].getBounds().getHeight()));
202 }
203 /* set a gap to uniformly distribute the extra space among property rectangles to reach the minimum bound's height */
204 boundsGap = 0;
205 Rectangle2D.Double minBounds = (Rectangle2D.Double)getMinBounds();
206 if(r.getHeight() < minBounds.height){
207 boundsGap = minBounds.height - r.getHeight();
208 boundsGap /= insidePropertyTypes.size();
209 }
210 r.add(minBounds); //make sure it's at least as big as the minimum bounds
211 dataDisplayBounds.setFrame(new Rectangle2D.Double(dataDisplayBounds.x,dataDisplayBounds.y,r.getWidth(),r.getHeight()));
212 }
213
214 /**
215 Draw the node from the Shape with shadow
216 @param g2 the graphics context
217 */
218 @Override
219 public void draw(Graphics2D g2){
220 /* draw the external shape */
221 Shape shape = getShape();
222 if (shape == null) return;
223 Color oldColor = g2.getColor();
224 g2.translate(SHADOW_GAP, SHADOW_GAP);
225 g2.setColor(SHADOW_COLOR);
226 g2.fill(shape);
227 g2.translate(-SHADOW_GAP, -SHADOW_GAP);
228 g2.setColor(g2.getBackground());
229 g2.fill(shape);
230 g2.setColor(Color.BLACK);
231 g2.draw(shape);
232 g2.setColor(oldColor);
233
234 /* if there ain't any property to display inside, then display the name in the middle of the data Display bounds */
235 if(!anyInsideProperties()){
236 nameLabel.draw(g2, dataDisplayBounds);
237 }else{
238 /* draw name */
239 Rectangle2D currentBounds = new Rectangle2D.Double(
240 dataDisplayBounds.x,
241 dataDisplayBounds.y,
242 dataDisplayBounds.getWidth(),
243 nameLabel.getBounds().getHeight());
244 if(drawPropertySeparators){
245 Shape oldClip = g2.getClip();
246 g2.setClip(getShape());
247 g2.draw(new Rectangle2D.Double(
248 getBounds().getX(),
249 dataDisplayBounds.y,
250 getBounds().getWidth(),
251 nameLabel.getBounds().getHeight())
252 );
253 g2.setClip(oldClip);
254 }
255 nameLabel.draw(g2, currentBounds);
256
257 /* draw internal properties */
258 Rectangle2D previousBounds;
259 for(int i=0;i<propertyLabels.length;i++){
260 previousBounds = currentBounds;
261 currentBounds = new Rectangle2D.Double(
262 previousBounds.getX(),
263 previousBounds.getMaxY(),
264 dataDisplayBounds.getWidth(),
265 propertyLabels[i].getBounds().getHeight()+boundsGap);
266 if(drawPropertySeparators){
267 Shape oldClip = g2.getClip();
268 g2.setClip(getShape());
269 g2.draw(new Rectangle2D.Double(
270 getBounds().getX(),
271 currentBounds.getY(),
272 getBounds().getWidth(),
273 currentBounds.getHeight())
274 );
275 g2.setClip(oldClip);
276 }
277 propertyLabels[i].draw(g2, currentBounds);
278 }
279 }
280
281 /* draw external properties */
282 for(List<PropertyNode> pnList : propertyNodesMap.values())
283 for(PropertyNode pn : pnList){
284 pn.draw(g2);
285 Direction d = new Direction( getBounds().getCenterX() - pn.getCenter().getX(), getBounds().getCenterY() - pn.getCenter().getY());
286 g2.draw(new Line2D.Double(pn.getConnectionPoint(d), getConnectionPoint(d.turn(180))));
287 }
288 /* draw visual cue for bookmarks and notes */
289 super.draw(g2);
290 }
291
292 protected Rectangle2D getMinBounds(){
293 return (Rectangle2D)minBounds.clone();
294 }
295
296 public abstract ShapeType getShapeType();
297
298 @Override
299 public void encode(Document doc, Element parent){
300 super.encode(doc, parent);
301 if(getProperties().isEmpty())
302 return;
303 NodeList propTagList = doc.getElementsByTagName(PersistenceManager.PROPERTY);
304
305 /* scan all the PROPERTY tags to add the position tag */
306 for(int i = 0 ; i< propTagList.getLength(); i++){
307 Element propertyTag = (Element)propTagList.item(i);
308 Element typeTag = (Element)propertyTag.getElementsByTagName(PersistenceManager.TYPE).item(0);
309 String type = typeTag.getTextContent();
310
311 /* a property of another node, continue */
312 if(!getProperties().getTypes().contains(type))
313 continue;
314
315 if(((PropertyView)getProperties().getView(type)).getPosition() == SimpleShapeNode.Position.Inside)
316 continue;
317
318 List<String> values = getProperties().getValues(type);
319 if(values.isEmpty())
320 continue;
321
322 NodeList elementTagList = propertyTag.getElementsByTagName(PersistenceManager.ELEMENT);
323 List<PropertyNode> pnList = propertyNodesMap.get(type);
324 for(int j=0; j<elementTagList.getLength();j++){
325 Element elementTag = (Element)elementTagList.item(j);
326 Element positionTag = doc.createElement(SimpleShapePrototypePersistenceDelegate.POSITION);
327 positionTag.setAttribute(PersistenceManager.X, String.valueOf(pnList.get(j).getX()));
328 positionTag.setAttribute(PersistenceManager.Y, String.valueOf(pnList.get(j).getY()));
329 elementTag.appendChild(positionTag);
330 }
331 }
332 }
333
334 @Override
335 public void decode(Document doc, Element nodeTag) throws IOException{
336 super.decode(doc, nodeTag);
337
338 NodeList propTagList = nodeTag.getElementsByTagName(PersistenceManager.PROPERTY);
339
340 /* split the property types into internal and external, properties have been set by super.decodeXMLInstance */
341 ArrayList<String> insidePropertyTypes = new ArrayList<String>(getProperties().getTypes().size());
342 ArrayList<String> outsidePropertyTypes = new ArrayList<String>(getProperties().getTypes().size());
343 for(String type : getProperties().getTypes()){
344 if(((PropertyView)getProperties().getView(type)).getPosition() == SimpleShapeNode.Position.Inside)
345 insidePropertyTypes.add(type);
346 else
347 outsidePropertyTypes.add(type);
348 }
349
350 /* set the multi-line string bounds for the properties which are displayed internally */
351 reshapeInnerProperties(insidePropertyTypes);
352
353 /* scan all the PROPERTY tags to decode the position tag of the properties which are displayed externally */
354 for(int i = 0 ; i< propTagList.getLength(); i++){
355 Element propertyTag = (Element)propTagList.item(i);
356 if(propertyTag.getElementsByTagName(PersistenceManager.TYPE).item(0) == null)
357 throw new IOException();
358 Element typeTag = (Element)propertyTag.getElementsByTagName(PersistenceManager.TYPE).item(0);
359
360 String type = typeTag.getTextContent();
361 /* (check on whether type exists in the node type definition is done in super.decode */
362
363 if(((PropertyView)getProperties().getView(type)).getPosition() == SimpleShapeNode.Position.Inside)
364 continue;
365 /* this will create external nodes and assign them their position */
366 NodeList elementTagList = propertyTag.getElementsByTagName(PersistenceManager.ELEMENT);
367 List<PropertyNode> pnList = new LinkedList<PropertyNode>();
368 for(int j=0; j<elementTagList.getLength();j++){
369 Element elementTag = (Element)elementTagList.item(j);
370 if(elementTag.getElementsByTagName(SimpleShapePrototypePersistenceDelegate.POSITION).item(0) == null ||
371 elementTag.getElementsByTagName(PersistenceManager.VALUE).item(0) == null)
372 throw new IOException();
373 Element positionTag = (Element)elementTag.getElementsByTagName(SimpleShapePrototypePersistenceDelegate.POSITION).item(0);
374 Element valueTag = (Element)elementTag.getElementsByTagName(PersistenceManager.VALUE).item(0);
375 double dx,dy;
376 try{
377 dx = Double.parseDouble(positionTag.getAttribute(PersistenceManager.X));
378 dy = Double.parseDouble(positionTag.getAttribute(PersistenceManager.Y));
379 }catch(NumberFormatException nfe){
380 throw new IOException();
381 }
382 PropertyNode pn = new PropertyNode(((PropertyView)getProperties().getView(type)).getShapeType());
383 pn.translate(dx, dy);
384 pn.setText(valueTag.getTextContent(),null);
385 pnList.add(pn);
386 }
387 propertyNodesMap.put(type, pnList);
388 /* this will apply the modifier format to the properties */
389 reshapeOuterProperties(outsidePropertyTypes);
390 }
391 }
392
393 @Override
394 protected void translateImplementation(Point2D p, double dx, double dy){
395 dataDisplayBounds.setFrame(dataDisplayBounds.getX() + dx,
396 dataDisplayBounds.getY() + dy,
397 dataDisplayBounds.getWidth(),
398 dataDisplayBounds.getHeight());
399 /* translate all the external property nodes */
400 for(List<PropertyNode> pnList : propertyNodesMap.values())
401 for(PropertyNode pn : pnList)
402 pn.translate(dx, dy);
403 }
404
405 @Override
406 public Object clone(){
407 SimpleShapeNode n = (SimpleShapeNode)super.clone();
408 n.propertyLabels = new MultiLineString[propertyLabels.length];
409 n.nameLabel = new MultiLineString();
410 n.propertyNodesMap = new LinkedHashMap<String, List<PropertyNode>>();
411 for(String s : propertyNodesMap.keySet())
412 n.propertyNodesMap.put(s, new LinkedList<PropertyNode>());
413 n.dataDisplayBounds = (Rectangle2D.Double)dataDisplayBounds.clone();
414 return n;
415 }
416
417 protected boolean anyInsideProperties(){
418 boolean propInside = false;
419 for(String type : getProperties().getTypes()){
420 if(((PropertyView)getProperties().getView(type)).getPosition() == Position.Inside){
421 if(!getProperties().getValues(type).isEmpty()){
422 propInside = true;
423 break;
424 }
425 }
426 }
427 return propInside;
428 }
429
430 protected Rectangle2D.Double dataDisplayBounds;
431 protected double boundsGap;
432 protected boolean drawPropertySeparators = true;
433 protected MultiLineString[] propertyLabels;
434 protected MultiLineString nameLabel;
435 protected Map<String,List<PropertyNode>> propertyNodesMap;
436
437 public static enum ShapeType {Circle, Ellipse, Rectangle, Square, Triangle, Transparent};
438 public static enum Position {Inside, Outside};
439
440 private static final int DEFAULT_WIDTH = 100;
441 private static final int DEFAULT_HEIGHT = 60;
442 private static final Rectangle2D.Double minBounds = new Rectangle2D.Double(0,0,DEFAULT_WIDTH,DEFAULT_HEIGHT);
443 private static final int PROP_NODE_DIST = 50;
444
445 protected static class PropertyNode{
446 public PropertyNode(ShapeType aShape){
447 /* add a little padding in the shape holding the label */
448 label = new MultiLineString(){
449 public Rectangle2D getBounds(){
450 Rectangle2D bounds = super.getBounds();
451 if(bounds.getWidth() != 0 || bounds.getHeight() != 0){
452 bounds.setFrame(
453 bounds.getX(),
454 bounds.getY(),
455 bounds.getWidth() + PADDING,
456 bounds.getHeight() + PADDING);
457 }
458 return bounds;
459 }
460 };
461 label.setJustification(MultiLineString.CENTER);
462 shapeType = aShape;
463 shape = label.getBounds();
464 }
465
466 public void setText(String text, ModifierView[] views){
467 label.setText(text,views);
468
469 switch(shapeType){
470 case Circle :
471 Rectangle2D circleBounds = EllipticalNode.getOutBounds(label.getBounds());
472 shape = new Ellipse2D.Double(
473 circleBounds.getX(),
474 circleBounds.getY(),
475 Math.max(circleBounds.getWidth(),circleBounds.getHeight()),
476 Math.max(circleBounds.getWidth(),circleBounds.getHeight())
477 );
478 break;
479 case Ellipse :
480 Rectangle2D ellipseBounds = EllipticalNode.getOutBounds(label.getBounds());
481 shape = new Ellipse2D.Double(
482 ellipseBounds.getX(),
483 ellipseBounds.getY(),
484 ellipseBounds.getWidth(),
485 ellipseBounds.getHeight()
486 );
487 break;
488 case Triangle :
489 shape = TriangularNode.getOutShape(label.getBounds());
490 break;
491 default : // Rectangle, Square and Transparent
492 shape = label.getBounds();;
493 break;
494 }
495
496 /* a new shape, placed at (0,0) has been created as a result of set text, therefore *
497 * we must put it back where the old shape was, since the translation is performed *
498 * by adding the translate args to x and y, x and y must first be set to 0 */
499 double currentX = x;
500 double currentY = y;
501 x = 0;
502 y = 0;
503 translate(currentX,currentY);
504 }
505
506 public void draw(Graphics2D g){
507 Color oldColor = g.getColor();
508 if(shapeType != ShapeType.Transparent){
509 g.translate(SHADOW_GAP, SHADOW_GAP);
510 g.setColor(SHADOW_COLOR);
511 g.fill(shape);
512 g.translate(-SHADOW_GAP, -SHADOW_GAP);
513
514 g.setColor(g.getBackground());
515 g.fill(shape);
516 g.setColor(Color.BLACK);
517 g.draw(shape);
518 }
519
520 label.draw(g, shape.getBounds2D());
521 g.setColor(oldColor);
522 }
523
524 public void translate(double dx, double dy){
525 x += dx;
526 y += dy;
527
528 if(shape instanceof Path2D){ //it's a triangle
529 Rectangle2D labelBounds = label.getBounds();
530 labelBounds.setFrame(
531 x,
532 y,
533 labelBounds.getWidth(),
534 labelBounds.getHeight()
535 );
536 shape = TriangularNode.getOutShape(labelBounds);
537 }else{
538 Rectangle2D bounds = shape.getBounds2D();
539 ((RectangularShape)shape).setFrame(
540 x,
541 y,
542 bounds.getWidth(),
543 bounds.getHeight()
544 );
545 }
546 }
547
548 public Point2D getConnectionPoint(Direction d) {
549 switch(shapeType){
550 case Circle :
551 case Ellipse :
552 return EllipticalNode.calculateConnectionPoint(d, shape.getBounds2D());
553 case Triangle :
554 return TriangularNode.calculateConnectionPoint(d, shape.getBounds2D());
555 default :
556 return RectangularNode.calculateConnectionPoint(d, shape.getBounds2D());
557 }
558 }
559
560 public boolean contains(Point2D p){
561 return shape.contains(p);
562 }
563
564 public Point2D getCenter(){
565 Rectangle2D bounds = shape.getBounds2D() ;
566 return new Point2D.Double(bounds.getCenterX(), bounds.getCenterY());
567 }
568
569 double getX(){
570 return x;
571 }
572
573 double getY(){
574 return y;
575 }
576
577 private static final int PADDING = 5;
578 private MultiLineString label;
579 private ShapeType shapeType;
580 private Shape shape;
581 private double x;
582 private double y;
583 }
584 }