001: /*
002: * @(#)AttributeMap 1.0 03-JUL-04
003: *
004: * Copyright (c) 2001-2005 Gaudenz Alder
005: *
006: * See LICENSE file in distribution for licensing details of this source file
007: */
008: package org.jgraph.graph;
009:
010: import java.awt.geom.Point2D;
011: import java.awt.geom.Rectangle2D;
012: import java.io.IOException;
013: import java.io.ObjectInputStream;
014: import java.io.ObjectOutputStream;
015: import java.io.Serializable;
016: import java.util.ArrayList;
017: import java.util.Collection;
018: import java.util.HashSet;
019: import java.util.Hashtable;
020: import java.util.Iterator;
021: import java.util.List;
022: import java.util.Map;
023: import java.util.Set;
024:
025: /**
026: * A map specifically for the storage of attributes of graph cells. The main
027: * advantage of the AttributeMap is that it allows to override cell view
028: * behaviour for scaling, translation, diffing, and cloning on a per instance
029: * basis without having to change the GraphConstants class
030: */
031: public class AttributeMap extends Hashtable implements Cloneable {
032:
033: /**
034: * Shared empty attribute map to return instead of null in applyMap.
035: */
036: public static transient AttributeMap emptyAttributeMap = new AttributeMap(
037: 0) {
038: public Object clone() {
039: return this ;
040: }
041: };
042:
043: /**
044: * Creates a new attribute map with an initial capacity of 8.
045: */
046: public AttributeMap() {
047: super (8);
048: }
049:
050: /**
051: * Creates a new attribute map with the specified initial capacity
052: *
053: * @param initialCapacity
054: * the initial capacity of the new map
055: */
056: public AttributeMap(int initialCapacity) {
057: super (initialCapacity);
058: }
059:
060: /**
061: * Constructs a new, empty hashtable with the specified initial capacity and
062: * the specified load factor.
063: *
064: * @param initialCapacity
065: * the initial capacity of the hashtable.
066: * @param loadCapacity
067: * the load factor of the hashtable.
068: */
069: public AttributeMap(int initialCapacity, float loadCapacity) {
070: super (initialCapacity, loadCapacity);
071: }
072:
073: /**
074: * Constructs a new AttributeMap with the same mappings as the given Map.
075: *
076: * @param map
077: * the input map to copy
078: */
079: public AttributeMap(Map map) {
080: super (map);
081: }
082:
083: /**
084: * Creates a point of suitable type for this attribute map
085: *
086: * @return a new point
087: */
088: public Point2D createPoint() {
089: return new SerializablePoint2D();
090: }
091:
092: /**
093: * Creates a point of suitable type for this attribute map with the same
094: * values as the point passed in
095: *
096: * @param p
097: * the point whose values the new point are to be based on
098: * @return a new copy of the point passed in
099: */
100: public Point2D createPoint(Point2D p) {
101: if (p != null) {
102: return createPoint(p.getX(), p.getY());
103: }
104: return null;
105: }
106:
107: /**
108: * Creates a point of suitable type for this attribute map with the same
109: * values as those passed in
110: *
111: * @param x
112: * the x-coordinate position of the new point
113: * @param y
114: * the y-coordinate position of the new point
115: * @return a new point at the coordinates passed in
116: */
117: public Point2D createPoint(double x, double y) {
118: return new SerializablePoint2D(x, y);
119: }
120:
121: /**
122: * Creates a rectangle of suitable type for this attribute map
123: *
124: * @return a new rectangle
125: */
126: public Rectangle2D createRect() {
127: return new SerializableRectangle2D();
128: }
129:
130: /**
131: * Creates a rectangle of suitable type for this attribute map with the same
132: * values as those passed in
133: *
134: * @param x
135: * the x-coordinate position of the new rectangle
136: * @param y
137: * the y-coordinate position of the new rectangle
138: * @param w
139: * the width of the new rectangle
140: * @param h
141: * the height of the new rectangle
142: * @return a new rectangle at the coordinates and of the dimensions passed
143: * in
144: */
145: public Rectangle2D createRect(double x, double y, double w, double h) {
146: return new SerializableRectangle2D(x, y, w, h);
147: }
148:
149: /**
150: * Creates a rectangle of suitable type for this attribute map at the
151: * position of the point passed in
152: *
153: * @param pt
154: * the position of the new rectangle
155: * @return a new rectangle the specified coordinates of zero size
156: */
157: public Rectangle2D createRect(Point2D pt) {
158: return createRect(pt, 0);
159: }
160:
161: /**
162: * Creates a rectangle of suitable type for this attribute map at the
163: * position of the point passed in with lengths <code>size</code>
164: *
165: * @param pt
166: * the position of the new rectangle
167: * @param size
168: * the length of both sides of the rectangle
169: * @return a new rectangle the specified position and dimensions
170: */
171: public Rectangle2D createRect(Point2D pt, double size) {
172: if (pt != null) {
173: return createRect(pt.getX(), pt.getY(), size, size);
174: }
175: return null;
176: }
177:
178: /**
179: * Clones the rectangle passed in
180: *
181: * @param rect
182: * the rectangle to clone
183: *
184: * @return a copy of the rectangle passed in
185: */
186: public Rectangle2D createRect(Rectangle2D rect) {
187: if (rect != null) {
188: return createRect(rect.getX(), rect.getY(),
189: rect.getWidth(), rect.getHeight());
190: }
191: return null;
192: }
193:
194: /**
195: * Creates a rectangle of suitable type for this attribute map
196: *
197: * @param x
198: * the x-coordinate position of the new rectangle
199: * @param y
200: * the y-coordinate position of the new rectangle
201: * @param w
202: * the width of the new rectangle
203: * @param h
204: * the height of the new rectangle
205: * @param grow1
206: * the amount both dimensions are to be increased by and the
207: * position coorindates of the rectangle are to be decreased by
208: * @param grow2
209: * the additional amount by which both dimensions are to be
210: * increased by
211: * @return a new rectangle at the coordinates and of the dimensions passed
212: * in
213: */
214: public Rectangle2D createRect(double x, double y, double w,
215: double h, double grow1, double grow2) {
216: return createRect(x - grow1, y - grow1, w + grow1 + grow2, h
217: + grow1 + grow2);
218: }
219:
220: /**
221: * Creates a clone of the rectangle passed in and manipulates it by
222: * <code>grow1</code> and <code>grow2</code>
223: *
224: * @param grow1
225: * the amount both dimensions are to be increased by and the
226: * position coorindates of the rectangle are to be decreased by
227: * @param grow2
228: * the additional amount by which both dimensions are to be
229: * increased by
230: * @return a new rectangle at the coordinates and of the dimensions passed
231: * in
232: */
233: public Rectangle2D createRect(Rectangle2D rect, double grow1,
234: double grow2) {
235: if (rect != null) {
236: return createRect(rect.getX(), rect.getY(),
237: rect.getWidth(), rect.getHeight(), grow1, grow2);
238: }
239: return null;
240: }
241:
242: /**
243: * Apply the <code>change</code> to this views attributes.
244: * <code>change</code> must be a <code>Map</code> previously obtained
245: * from this object.
246: *
247: * @param change
248: * the change to apply
249: * @return a map that may be used to undo the change to target.
250: */
251: public AttributeMap applyMap(Map change) {
252: AttributeMap undo = new AttributeMap();
253: if (change != null) {
254: // Handle Remove All
255: if (GraphConstants.isRemoveAll(change)) {
256: undo.putAll(this );
257: clear();
258: }
259: // Handle Remove Individual
260: Object[] remove = GraphConstants
261: .getRemoveAttributes(change);
262: if (remove != null) {
263: // don't store command
264: for (int i = 0; i < remove.length; i++) {
265: Object oldValue = remove(remove[i]);
266: if (oldValue != null)
267: undo.put(remove[i], oldValue);
268: }
269: }
270: // Attributes that were empty are added to removeattibutes.
271: // Performance and transient memory peak are reduced by lazily
272: // instantiating the set.
273: Set removeAttributes = null;
274: Iterator it = change.entrySet().iterator();
275: while (it.hasNext()) {
276: Map.Entry entry = (Map.Entry) it.next();
277: Object key = entry.getKey();
278: if (!key.equals(GraphConstants.REMOVEALL)
279: && !key.equals(GraphConstants.REMOVEATTRIBUTES)
280: && !key.equals(GraphConstants.VALUE)) {
281: Object oldValue = applyValue(key, entry.getValue());
282: if (oldValue == null) {
283: if (removeAttributes == null) {
284: removeAttributes = new HashSet();
285: }
286: removeAttributes.add(key);
287: } else {
288: undo.put(key, oldValue);
289: }
290: }
291: }
292: if (removeAttributes != null && !removeAttributes.isEmpty()) {
293: GraphConstants.setRemoveAttributes(undo,
294: removeAttributes.toArray());
295: }
296: }
297: return undo;
298: }
299:
300: /**
301: * Apply the <code>key</code> to <code>value</code>
302: *
303: * @param key
304: * the map key whose value is to be altered
305: * @param value
306: * the new value to be applied to the specified key
307: * @return the old value.
308: */
309: public Object applyValue(Object key, Object value) {
310: // In all other cases we put the new value into the
311: // map. If we encounter a list (of points) or rectangle
312: // these will be cloned before insertion. Cloning includes
313: // replacing the rectangle/points with serializable objects.
314: if (value instanceof Rectangle2D)
315: value = createRect((Rectangle2D) value);
316: if (value instanceof Point2D)
317: value = createPoint((Point2D) value);
318: if (value instanceof Point2D[])
319: value = clonePoints((Point2D[]) value);
320: if (value instanceof List) // FIXME: PointList interface?
321: value = clonePoints((List) value);
322: return put(key, value);
323: }
324:
325: /**
326: * Returns a list where all instances of PortView are replaced by their
327: * correspnding Point instance.
328: *
329: * @param points
330: * the points to be cloned
331: * @return the cloned points
332: */
333: public Point2D[] clonePoints(Point2D[] points) {
334: List pts = clonePoints(points, true);
335: Point2D[] newPoints = new Point2D[pts.size()];
336: pts.toArray(newPoints);
337: return newPoints;
338: }
339:
340: /**
341: * Returns a list where all instances of PortView are replaced by their
342: * correspnding Point instance.
343: *
344: * @param points
345: * the points to be cloned
346: * @return the cloned points
347: */
348: public List clonePoints(List points) {
349: return clonePoints(points.toArray(), true);
350: }
351:
352: /**
353: * Returns a list where all instances of PortView are replaced by their
354: * correspnding Point instance.
355: */
356: public List clonePoints(Object[] points, boolean convertPortViews) {
357: // TODO: Change the list in-place?
358: ArrayList newList = new ArrayList(points.length);
359: for (int i = 0; i < points.length; i++) {
360: // Clone Point
361: Object point = points[i];
362: if (point instanceof PortView && convertPortViews)
363: point = createPoint(((PortView) point).getLocation());
364: else if (point instanceof Point2D)
365: point = createPoint((Point2D) point);
366: newList.add(point);
367: }
368: return newList;
369: }
370:
371: /**
372: * Translates the maps in <code>c</code> using
373: * <code>translate(Map, int, int)</code>.
374: */
375: public static void translate(Collection c, double dx, double dy) {
376: Iterator it = c.iterator();
377: while (it.hasNext()) {
378: Object map = it.next();
379: if (map instanceof AttributeMap)
380: ((AttributeMap) map).translate(dx, dy);
381: }
382: }
383:
384: /**
385: * Translates <code>map</code> by the given amount.
386: */
387: public void translate(double dx, double dy) {
388: // Translate Bounds
389: if (GraphConstants.isMoveable(this )) {
390: Rectangle2D bounds = GraphConstants.getBounds(this );
391: if (bounds != null) {
392: int moveableAxis = GraphConstants.getMoveableAxis(this );
393: if (moveableAxis == GraphConstants.X_AXIS)
394: dy = 0;
395: else if (moveableAxis == GraphConstants.Y_AXIS)
396: dx = 0;
397: bounds.setFrame(bounds.getX() + dx, bounds.getY() + dy,
398: bounds.getWidth(), bounds.getHeight());
399: }
400: // Translate Points
401: List points = GraphConstants.getPoints(this );
402: if (points != null) {
403: for (int i = 0; i < points.size(); i++) {
404: Object obj = points.get(i);
405: if (obj instanceof Point2D) {
406: Point2D pt = (Point2D) obj;
407: pt.setLocation(pt.getX() + dx, pt.getY() + dy);
408: }
409: }
410: }
411: }
412: }
413:
414: /**
415: * Scales <code>map</code> by the given amount.
416: */
417: public void scale(double sx, double sy, Point2D origin) {
418: // Scale Bounds
419: Rectangle2D bounds = GraphConstants.getBounds(this );
420: if (bounds != null) {
421: Point2D p = createPoint(bounds.getX(), bounds.getY());
422: Point2D loc = (Point2D) p.clone();
423: p.setLocation(origin.getX()
424: + Math.round((p.getX() - origin.getX()) * sx),
425: origin.getY()
426: + Math.round((p.getY() - origin.getY())
427: * sy));
428: if (!p.equals(loc)) // Scale Location
429: translate(p.getX() - loc.getX(), p.getY() - loc.getY());
430: int sizeableAxis = GraphConstants.getSizeableAxis(this );
431: if (sizeableAxis == GraphConstants.X_AXIS)
432: sy = 1;
433: else if (sizeableAxis == GraphConstants.Y_AXIS)
434: sx = 1;
435: double w = Math.max(1, Math.round(bounds.getWidth() * sx));
436: double h = Math.max(1, Math.round(bounds.getHeight() * sy));
437: // Scale Bounds
438: bounds.setFrame(bounds.getX(), bounds.getY(), w, h);
439: }
440: // Scale Points
441: List points = GraphConstants.getPoints(this );
442: if (points != null) {
443: Iterator it = points.iterator();
444: while (it.hasNext()) {
445: Object obj = it.next();
446: if (obj instanceof Point2D) {
447: // Scale Point
448: Point2D loc = (Point2D) obj;
449: Point2D p = (Point2D) loc.clone();
450: p.setLocation(origin.getX()
451: + Math.round((p.getX() - origin.getX())
452: * sx), origin.getY()
453: + Math.round((p.getY() - origin.getY())
454: * sy));
455: // Move Point
456: loc.setLocation(p);
457: }
458: }
459: }
460: }
461:
462: /**
463: * Returns a new map that contains all (key, value)-pairs of
464: * <code>newState</code> where either key is not used or value is
465: * different for key in <code>oldState</code>. In other words, this
466: * method removes the common entries from oldState and newState, and returns
467: * the "difference" between the two.
468: *
469: * This method never returns null.
470: */
471: public Map diff(Map newState) {
472: Map diff = new Hashtable();
473: Iterator it = newState.entrySet().iterator();
474: while (it.hasNext()) {
475: Map.Entry entry = (Map.Entry) it.next();
476: Object key = entry.getKey();
477: Object newValue = entry.getValue();
478: Object oldValue = get(key);
479: if (oldValue == null || !oldValue.equals(newValue))
480: diff.put(key, newValue);
481: }
482: return diff;
483: }
484:
485: /**
486: * Returns a clone of <code>map</code>, from keys to values. If the map
487: * contains bounds or points, these are cloned as well. References to
488: * <code>PortViews</code> are replaces by points. <br>
489: * <b>Note: </b> Extend this method to clone custom user objects.
490: */
491: public Object clone() {
492: // TODO, is cloning the hash table excessive?
493: return cloneEntries((AttributeMap) super .clone());
494: }
495:
496: /**
497: * Clones special object entried in the given map.
498: */
499: public AttributeMap cloneEntries(AttributeMap newMap) {
500: // Clone Bounds
501: Rectangle2D bounds = GraphConstants.getBounds(newMap);
502: if (bounds != null)
503: GraphConstants.setBounds(newMap, (Rectangle2D) (bounds
504: .clone()));
505: // Clone List Of Points
506: List points = GraphConstants.getPoints(newMap);
507: if (points != null)
508: GraphConstants.setPoints(newMap, clonePoints(points));
509: // Clone extra label positions
510: Point2D[] positions = GraphConstants
511: .getExtraLabelPositions(newMap);
512: if (positions != null)
513: GraphConstants.setExtraLabelPositions(newMap,
514: clonePoints(positions));
515: // Clone Edge Label
516: Point2D label = GraphConstants.getLabelPosition(newMap);
517: if (label != null)
518: GraphConstants.setLabelPosition(newMap, (Point2D) label
519: .clone());
520: return newMap;
521: }
522:
523: public static class SerializablePoint2D extends Point2D.Double
524: implements Serializable {
525:
526: public SerializablePoint2D() {
527: super ();
528: }
529:
530: public SerializablePoint2D(double x, double y) {
531: super (x, y);
532: }
533:
534: public void setX(double x) {
535: setLocation(x, getY());
536: }
537:
538: public void setY(double y) {
539: setLocation(getX(), y);
540: }
541:
542: private void writeObject(ObjectOutputStream out)
543: throws IOException {
544: out.defaultWriteObject();
545: out.writeObject(new java.lang.Double(getX()));
546: out.writeObject(new java.lang.Double(getY()));
547: }
548:
549: private void readObject(ObjectInputStream in)
550: throws IOException, ClassNotFoundException {
551: in.defaultReadObject();
552: java.lang.Double x = (java.lang.Double) in.readObject();
553: java.lang.Double y = (java.lang.Double) in.readObject();
554: setLocation(x.doubleValue(), y.doubleValue());
555: }
556:
557: }
558:
559: public static class SerializableRectangle2D extends
560: Rectangle2D.Double implements Serializable {
561:
562: public SerializableRectangle2D() {
563: super ();
564: }
565:
566: public SerializableRectangle2D(double x, double y,
567: double width, double height) {
568: super (x, y, width, height);
569: }
570:
571: public void setX(double x) {
572: setFrame(x, getY(), getWidth(), getHeight());
573: }
574:
575: public void setY(double y) {
576: setFrame(getX(), y, getWidth(), getHeight());
577: }
578:
579: public void setWidth(double width) {
580: setFrame(getX(), getY(), width, getHeight());
581: }
582:
583: public void setHeight(double height) {
584: setFrame(getX(), getY(), getWidth(), height);
585: }
586:
587: private void writeObject(ObjectOutputStream out)
588: throws IOException {
589: out.defaultWriteObject();
590: out.writeObject(new java.lang.Double(getX()));
591: out.writeObject(new java.lang.Double(getY()));
592: out.writeObject(new java.lang.Double(getWidth()));
593: out.writeObject(new java.lang.Double(getHeight()));
594: }
595:
596: private void readObject(ObjectInputStream in)
597: throws IOException, ClassNotFoundException {
598: in.defaultReadObject();
599: java.lang.Double x = (java.lang.Double) in.readObject();
600: java.lang.Double y = (java.lang.Double) in.readObject();
601: java.lang.Double width = (java.lang.Double) in.readObject();
602: java.lang.Double height = (java.lang.Double) in
603: .readObject();
604: setFrame(x.doubleValue(), y.doubleValue(), width
605: .doubleValue(), height.doubleValue());
606: }
607: }
608: }
|