PatchSet 5114 Date: 2004/08/29 09:37:53 Author: dalibor Branch: HEAD Tag: (none) Log: Resynced with GNU Classpath. 2004-08-29 Dalibor Topic <[EMAIL PROTECTED]>
* libraries/javalib/java/awt/geom/Area.java: Resynced with GNU Classpath. 2004-08-27 Sven de Marothy <[EMAIL PROTECTED]> * java/awt/geom/Area.java Implemented. Members: ChangeLog:1.2670->1.2671 libraries/javalib/java/awt/geom/Area.java:1.1->1.2 Index: kaffe/ChangeLog diff -u kaffe/ChangeLog:1.2670 kaffe/ChangeLog:1.2671 --- kaffe/ChangeLog:1.2670 Sat Aug 28 15:29:53 2004 +++ kaffe/ChangeLog Sun Aug 29 09:37:53 2004 @@ -1,3 +1,13 @@ +2004-08-29 Dalibor Topic <[EMAIL PROTECTED]> + + * libraries/javalib/java/awt/geom/Area.java: + Resynced with GNU Classpath. + + 2004-08-27 Sven de Marothy <[EMAIL PROTECTED]> + + * java/awt/geom/Area.java + Implemented. + 2004-08-28 Alexander Boettcher <[EMAIL PROTECTED]> * kaffe/kaffevm/debug.c Index: kaffe/libraries/javalib/java/awt/geom/Area.java diff -u kaffe/libraries/javalib/java/awt/geom/Area.java:1.1 kaffe/libraries/javalib/java/awt/geom/Area.java:1.2 --- kaffe/libraries/javalib/java/awt/geom/Area.java:1.1 Wed Nov 6 11:52:24 2002 +++ kaffe/libraries/javalib/java/awt/geom/Area.java Sun Aug 29 09:37:56 2004 @@ -1,5 +1,5 @@ /* Area.java -- represents a shape built by constructive area geometry - Copyright (C) 2002 Free Software Foundation + Copyright (C) 2002, 2004 Free Software Foundation This file is part of GNU Classpath. @@ -35,74 +35,636 @@ obligated to do so. If you do not wish to do so, delete this exception statement from your version. */ - package java.awt.geom; import java.awt.Rectangle; import java.awt.Shape; +import java.util.Vector; + /** - * STUBS ONLY - * XXX Implement and document. + * The Area class represents any area for the purpose of + * Constructive Area Geometry (CAG) manipulations. CAG manipulations + * work as an area-wise form of boolean logic, where the basic operations are: + * <P><li>Add (in boolean algebra: A <B>or</B> B)<BR> + * <li>Subtract (in boolean algebra: A <B>and</B> (<B>not</B> B) )<BR> + * <li>Intersect (in boolean algebra: A <B>and</B> B)<BR> + * <li>Exclusive Or <BR> + * <img src="doc-files/Area-1.png" width="342" height="302" + * alt="Illustration of CAG operations" /><BR> + * Above is an illustration of the CAG operations on two ring shapes.<P> + * + * The contains and intersects() methods are also more accurate than the + * specification of #Shape requires.<P> + * + * Please note that constructing an Area can be slow + * (Self-intersection resolving is proportional to the square of + * the number of segments).<P> + * @see #add(Area) + * @see #subtract(Area) + * @see #intersect(Area) + * @see #exclusiveOr(Area) + * + * @author Sven de Marothy ([EMAIL PROTECTED]) + * + * @since 1.2 + * @status Works, but could be faster and more reliable. */ public class Area implements Shape, Cloneable { + /** + * General numerical precision + */ + private final double EPSILON = 1E-11; + + /** + * recursive subdivision epsilon - (see getRecursionDepth) + */ + private final double RS_EPSILON = 1E-13; + + /** + * Snap distance - points within this distance are considered equal + */ + private final double PE_EPSILON = 1E-11; + + /** + * Segment vectors containing solid areas and holes + */ + private Vector solids; + + /** + * Segment vectors containing solid areas and holes + */ + private Vector holes; + + /** + * Vector (temporary) storing curve-curve intersections + */ + private Vector cc_intersections; + + /** + * Winding rule WIND_NON_ZERO used, after construction, + * this is irrelevant. + */ + private int windingRule; + + /** + * Constructs an empty Area + */ public Area() { + solids = new Vector(); + holes = new Vector(); } + + /** + * Constructs an Area from any given Shape. <P> + * + * If the Shape is self-intersecting, the created Area will consist + * of non-self-intersecting subpaths, and any inner paths which + * are found redundant in accordance with the Shape's winding rule + * will not be included. + */ public Area(Shape s) { + this(); + + Vector p = makeSegment(s); + + // empty path + if (p == null) + return; + + // delete empty paths + for (int i = 0; i < p.size(); i++) + if (((Segment) p.elementAt(i)).getSignedArea() == 0.0) + p.remove(i--); + + /* + * Resolve self intersecting paths into non-intersecting + * solids and holes. + * Algorithm is as follows: + * 1: Create nodes at all self intersections + * 2: Put all segments into a list + * 3: Grab a segment, follow it, change direction at each node, + * removing segments from the list in the process + * 4: Repeat (3) until no segments remain in the list + * 5: Remove redundant paths and sort into solids and holes + */ + Vector paths = new Vector(); + Segment v; + + for (int i = 0; i < p.size(); i++) + { + Segment path = (Segment) p.elementAt(i); + createNodesSelf(path); + } + + if (p.size() > 1) + { + for (int i = 0; i < p.size() - 1; i++) + for (int j = i + 1; j < p.size(); j++) + { + Segment path1 = (Segment) p.elementAt(i); + Segment path2 = (Segment) p.elementAt(j); + createNodes(path1, path2); + } + } + + // we have intersecting points. + Vector segments = new Vector(); + + for (int i = 0; i < p.size(); i++) + { + Segment path = v = (Segment) p.elementAt(i); + do + { + segments.add(v); + v = v.next; + } + while (v != path); + } + + paths = weilerAtherton(segments); + deleteRedundantPaths(paths); } - public void add(Area a) + + /** + * Performs an add (union) operation on this area with another Area.<BR> + * @param area - the area to be unioned with this one + */ + public void add(Area area) { - // XXX Implement. - throw new Error("not implemented"); + if (equals(area)) + return; + if (area.isEmpty()) + return; + + Area B = (Area) area.clone(); + + Vector pathA = new Vector(); + Vector pathB = new Vector(); + pathA.addAll(solids); + pathA.addAll(holes); + pathB.addAll(B.solids); + pathB.addAll(B.holes); + + int nNodes = 0; + + for (int i = 0; i < pathA.size(); i++) + { + Segment a = (Segment) pathA.elementAt(i); + for (int j = 0; j < pathB.size(); j++) + { + Segment b = (Segment) pathB.elementAt(j); + nNodes += createNodes(a, b); + } + } + + Vector paths = new Vector(); + Segment v; + + // we have intersecting points. + Vector segments = new Vector(); + + // In a union operation, we keep all + // segments of A oustide B and all B outside A + for (int i = 0; i < pathA.size(); i++) + { + v = (Segment) pathA.elementAt(i); + Segment path = v; + do + { + if (v.isSegmentOutside(area)) + segments.add(v); + v = v.next; + } + while (v != path); + } + + for (int i = 0; i < pathB.size(); i++) + { + v = (Segment) pathB.elementAt(i); + Segment path = v; + do + { + if (v.isSegmentOutside(this)) + segments.add(v); + v = v.next; + } + while (v != path); + } + + paths = weilerAtherton(segments); + deleteRedundantPaths(paths); } - public void subtract(Area a) + + /** + * Performs a subtraction operation on this Area.<BR> + * @param area - the area to be subtracted from this area. + */ + public void subtract(Area area) { - // XXX Implement. - throw new Error("not implemented"); + if (isEmpty() || area.isEmpty()) + return; + + if (equals(area)) + { + reset(); + return; + } + + Vector pathA = new Vector(); + Area B = (Area) area.clone(); + pathA.addAll(solids); + pathA.addAll(holes); + + // reverse the directions of B paths. + setDirection(B.holes, true); + setDirection(B.solids, false); + + Vector pathB = new Vector(); + pathB.addAll(B.solids); + pathB.addAll(B.holes); + + int nNodes = 0; + + // create nodes + for (int i = 0; i < pathA.size(); i++) + { + Segment a = (Segment) pathA.elementAt(i); + for (int j = 0; j < pathB.size(); j++) + { + Segment b = (Segment) pathB.elementAt(j); + nNodes += createNodes(a, b); + } + } + + Vector paths = new Vector(); + + // we have intersecting points. + Vector segments = new Vector(); + + // In a subtraction operation, we keep all + // segments of A oustide B and all B within A + // We outsideness-test only one segment in each path + // and the segments before and after any node + for (int i = 0; i < pathA.size(); i++) + { + Segment v = (Segment) pathA.elementAt(i); + Segment path = v; + if (v.isSegmentOutside(area) && v.node == null) + segments.add(v); + boolean node = false; + do + { + if ((v.node != null || node)) + { + node = (v.node != null); + if (v.isSegmentOutside(area)) + segments.add(v); + } + v = v.next; + } + while (v != path); + } + + for (int i = 0; i < pathB.size(); i++) + { + Segment v = (Segment) pathB.elementAt(i); + Segment path = v; + if (! v.isSegmentOutside(this) && v.node == null) + segments.add(v); + v = v.next; + boolean node = false; + do + { + if ((v.node != null || node)) + { + node = (v.node != null); + if (! v.isSegmentOutside(this)) + segments.add(v); + } + v = v.next; + } + while (v != path); + } + + paths = weilerAtherton(segments); + deleteRedundantPaths(paths); } - public void intersect(Area a) + + /** + * Performs an intersection operation on this Area.<BR> + * @param area - the area to be intersected with this area. + */ + public void intersect(Area area) { - // XXX Implement. - throw new Error("not implemented"); + if (isEmpty() || area.isEmpty()) + { + reset(); + return; + } + if (equals(area)) + return; + + Vector pathA = new Vector(); + Area B = (Area) area.clone(); + pathA.addAll(solids); + pathA.addAll(holes); + + Vector pathB = new Vector(); + pathB.addAll(B.solids); + pathB.addAll(B.holes); + + int nNodes = 0; + + // create nodes + for (int i = 0; i < pathA.size(); i++) + { + Segment a = (Segment) pathA.elementAt(i); + for (int j = 0; j < pathB.size(); j++) + { + Segment b = (Segment) pathB.elementAt(j); + nNodes += createNodes(a, b); + } + } + + Vector paths = new Vector(); + + // we have intersecting points. + Vector segments = new Vector(); + + // In an intersection operation, we keep all + // segments of A within B and all B within A + // (The rest must be redundant) + // We outsideness-test only one segment in each path + // and the segments before and after any node + for (int i = 0; i < pathA.size(); i++) + { + Segment v = (Segment) pathA.elementAt(i); + Segment path = v; + if (! v.isSegmentOutside(area) && v.node == null) + segments.add(v); + boolean node = false; + do + { + if ((v.node != null || node)) + { + node = (v.node != null); + if (! v.isSegmentOutside(area)) + segments.add(v); + } + v = v.next; + } + while (v != path); + } + + for (int i = 0; i < pathB.size(); i++) + { + Segment v = (Segment) pathB.elementAt(i); + Segment path = v; + if (! v.isSegmentOutside(this) && v.node == null) + segments.add(v); + v = v.next; + boolean node = false; + do + { + if ((v.node != null || node)) + { + node = (v.node != null); + if (! v.isSegmentOutside(this)) + segments.add(v); + } + v = v.next; + } + while (v != path); + } + + paths = weilerAtherton(segments); + deleteRedundantPaths(paths); } - public void exclusiveOr(Area a) + + /** + * Performs an exclusive-or operation on this Area.<BR> + * @param area - the area to be XORed with this area. + */ + public void exclusiveOr(Area area) { - // XXX Implement. - throw new Error("not implemented"); + if (area.isEmpty()) + return; + + if (isEmpty()) + { + Area B = (Area) area.clone(); + solids = B.solids; + holes = B.holes; + return; + } + if (equals(area)) + { + reset(); + return; + } + + Vector pathA = new Vector(); + + Area B = (Area) area.clone(); + Vector pathB = new Vector(); + pathA.addAll(solids); + pathA.addAll(holes); + + // reverse the directions of B paths. + setDirection(B.holes, true); + setDirection(B.solids, false); + pathB.addAll(B.solids); + pathB.addAll(B.holes); + + int nNodes = 0; + + for (int i = 0; i < pathA.size(); i++) + { + Segment a = (Segment) pathA.elementAt(i); + for (int j = 0; j < pathB.size(); j++) + { + Segment b = (Segment) pathB.elementAt(j); + nNodes += createNodes(a, b); + } + } + + Vector paths = new Vector(); + Segment v; + + // we have intersecting points. + Vector segments = new Vector(); + + // In an XOR operation, we operate on all segments + for (int i = 0; i < pathA.size(); i++) + { + v = (Segment) pathA.elementAt(i); + Segment path = v; + do + { + segments.add(v); + v = v.next; + } + while (v != path); + } + + for (int i = 0; i < pathB.size(); i++) + { + v = (Segment) pathB.elementAt(i); + Segment path = v; + do + { + segments.add(v); + v = v.next; + } + while (v != path); + } + + paths = weilerAtherton(segments); + deleteRedundantPaths(paths); } + + /** + * Clears the Area object, creating an empty area. + */ public void reset() { - // XXX Implement. - throw new Error("not implemented"); + solids = new Vector(); + holes = new Vector(); } + + /** + * Returns whether this area encloses any area. + * @return true if the object encloses any area. + */ public boolean isEmpty() { - // XXX Implement. - throw new Error("not implemented"); + if (solids.size() == 0) + return true; + + double totalArea = 0; + for (int i = 0; i < solids.size(); i++) + totalArea += Math.abs(((Segment) solids.elementAt(i)).getSignedArea()); + for (int i = 0; i < holes.size(); i++) + totalArea -= Math.abs(((Segment) holes.elementAt(i)).getSignedArea()); + if (totalArea <= EPSILON) + return true; + + return false; } + + /** + * Determines whether the Area consists entirely of line segments + * @return true if the Area lines-only, false otherwise + */ public boolean isPolygonal() { - // XXX Implement. - throw new Error("not implemented"); + for (int i = 0; i < holes.size(); i++) + if (! ((Segment) holes.elementAt(i)).isPolygonal()) + return false; + for (int i = 0; i < solids.size(); i++) + if (! ((Segment) solids.elementAt(i)).isPolygonal()) + return false; + return true; } + + /** + * Determines if the Area is rectangular.<P> + * + * This is strictly qualified. An area is considered rectangular if:<BR> + * <li>It consists of a single polygonal path.<BR> + * <li>It is oriented parallel/perpendicular to the xy axis<BR> + * <li>It must be exactly rectangular, i.e. small errors induced by + * transformations may cause a false result, although the area is + * visibly rectangular.<P> + * @return true if the above criteria are met, false otherwise + */ public boolean isRectangular() { - // XXX Implement. - throw new Error("not implemented"); + if (holes.size() != 0 || solids.size() != 1) + return false; + + Segment path = (Segment) solids.elementAt(0); + if (! path.isPolygonal()) + return false; + + int nCorners = 0; + Segment s = path; + do + { + Segment s2 = s.next; + double d1 = (s.P2.getX() - s.P1.getX())*(s2.P2.getX() - s2.P1.getX())/ + ((s.P1.distance(s.P2)) * (s2.P1.distance(s2.P2))); + double d2 = (s.P2.getY() - s.P1.getY())*(s2.P2.getY() - s2.P1.getY())/ + ((s.P1.distance(s.P2)) * (s2.P1.distance(s2.P2))); + double dotproduct = d1 + d2; + + // For some reason, only rectangles on the XY axis count. + if (d1 != 0 && d2 != 0) + return false; + + if (Math.abs(dotproduct) == 0) // 90 degree angle + nCorners++; + else if ((Math.abs(1.0 - dotproduct) > 0)) // 0 degree angle? + return false; // if not, return false + + s = s.next; + } + while (s != path); + + return nCorners == 4; } + + /** + * Returns whether the Area consists of more than one simple + * (non self-intersecting) subpath. + * + * @return true if the Area consists of none or one simple subpath, + * false otherwise. + */ public boolean isSingular() { - // XXX Implement. - throw new Error("not implemented"); + return (holes.size() == 0 && solids.size() <= 1); } + + /** + * Returns the bounding box of the Area.<P> Unlike the CubicCurve2D and + * QuadraticCurve2D classes, this method will return the tightest possible + * bounding box, evaluating the extreme points of each curved segment.<P> + * @return the bounding box + */ public Rectangle2D getBounds2D() { - // XXX Implement. - throw new Error("not implemented"); + if (solids.size() == 0) + return new Rectangle2D.Double(0.0, 0.0, 0.0, 0.0); + + double xmin; + double xmax; + double ymin; + double ymax; + xmin = xmax = ((Segment) solids.elementAt(0)).P1.getX(); + ymin = ymax = ((Segment) solids.elementAt(0)).P1.getY(); + + for (int path = 0; path < solids.size(); path++) + { + Rectangle2D r = ((Segment) solids.elementAt(path)).getPathBounds(); + xmin = Math.min(r.getMinX(), xmin); + ymin = Math.min(r.getMinY(), ymin); + xmax = Math.max(r.getMaxX(), xmax); + ymax = Math.max(r.getMaxY(), ymax); + } + + return (new Rectangle2D.Double(xmin, ymin, (xmax - xmin), (ymax - ymin))); } + + /** + * Returns the bounds of this object in Rectangle format. + * Please note that this may lead to loss of precision. + * @see #getBounds2D() + */ public Rectangle getBounds() { return getBounds2D().getBounds(); @@ -118,66 +680,2576 @@ { try { - return super.clone(); + Area clone = new Area(); + for (int i = 0; i < solids.size(); i++) + clone.solids.add(((Segment) solids.elementAt(i)).cloneSegmentList()); + for (int i = 0; i < holes.size(); i++) + clone.holes.add(((Segment) holes.elementAt(i)).cloneSegmentList()); + return clone; } catch (CloneNotSupportedException e) { - throw (Error) new InternalError().initCause(e); // Impossible + throw (Error) new InternalError().initCause(e); // Impossible } } - public boolean equals(Area a) + /** + * Compares two Areas. + * + * @return true if the areas are equal. False otherwise. + */ + public boolean equals(Area area) { - // XXX Implement. - throw new Error("not implemented"); + if (! getBounds2D().equals(area.getBounds2D())) + return false; + + if (solids.size() != area.solids.size() + || holes.size() != area.holes.size()) + return false; + + Vector pathA = new Vector(); + pathA.addAll(solids); + pathA.addAll(holes); + Vector pathB = new Vector(); + pathB.addAll(area.solids); + pathB.addAll(area.holes); + + int nPaths = pathA.size(); + boolean[][] match = new boolean[2][nPaths]; + + for (int i = 0; i < nPaths; i++) + { + for (int j = 0; j < nPaths; j++) + { + Segment p1 = (Segment) pathA.elementAt(i); + Segment p2 = (Segment) pathB.elementAt(j); + if (! match[0][i] && ! match[1][j]) + if (p1.pathEquals(p2)) + match[0][i] = match[1][j] = true; + } + } + + boolean result = true; + for (int i = 0; i < nPaths; i++) + result = result && match[0][i] && match[1][i]; + return result; } + /** + * Transforms this area by the AffineTransform at + */ public void transform(AffineTransform at) { - // XXX Implement. - throw new Error("not implemented"); + for (int i = 0; i < solids.size(); i++) + ((Segment) solids.elementAt(i)).transformSegmentList(at); + for (int i = 0; i < holes.size(); i++) + ((Segment) holes.elementAt(i)).transformSegmentList(at); + + // Note that the orientation is not invariant under inversion + if ((at.getType() & AffineTransform.TYPE_FLIP) != 0) + { + setDirection(holes, false); + setDirection(solids, true); + } } + + /** + * Returns a new Area equal to this one, transformed + * by the AffineTransform at + * @return the transformed area + */ public Area createTransformedArea(AffineTransform at) { Area a = (Area) clone(); a.transform(at); return a; } + + /** + * Determines if the point (x,y) is contained within this Area. + * + * @return true if the point is contained, false otherwise. + */ public boolean contains(double x, double y) { - // XXX Implement. - throw new Error("not implemented"); + int n = 0; + for (int i = 0; i < solids.size(); i++) + if (((Segment) solids.elementAt(i)).contains(x, y)) + n++; + + for (int i = 0; i < holes.size(); i++) + if (((Segment) holes.elementAt(i)).contains(x, y)) + n--; + + return (n != 0); } + + /** + * Determines if the Point2D p is contained within this Area. + * + * @return true if the point is contained, false otherwise. + */ public boolean contains(Point2D p) { return contains(p.getX(), p.getY()); } + + /** + * Determines if the rectangle specified by (x,y) as the upper-left + * and with width w and height h is completely contained within this Area, + * returns false otherwise.<P> + * + * This method should always produce the correct results, unlike for other + * classes in geom. + * @return true if the rectangle is considered contained + */ public boolean contains(double x, double y, double w, double h) { - // XXX Implement. - throw new Error("not implemented"); + LineSegment[] l = new LineSegment[4]; + l[0] = new LineSegment(x, y, x + w, y); + l[1] = new LineSegment(x, y + h, x + w, y + h); + l[2] = new LineSegment(x, y, x, y + h); + l[3] = new LineSegment(x + w, y, x + w, y + h); + + // Since every segment in the area must a contour + // between inside/outside segments, ANY intersection + // will mean the rectangle is not entirely contained. + for (int i = 0; i < 4; i++) + { + for (int path = 0; path < solids.size(); path++) + { + Segment v; + Segment start; + start = v = (Segment) solids.elementAt(path); + do + { + if (l[i].hasIntersections(v)) + return false; + v = v.next; + } + while (v != start); + } + for (int path = 0; path < holes.size(); path++) + { + Segment v; + Segment start; + start = v = (Segment) holes.elementAt(path); + do + { + if (l[i].hasIntersections(v)) + return false; + v = v.next; + } + while (v != start); + } + } + + // Is any point inside? + if (! contains(x, y)) + return false; + + // Final hoop: Is the rectangle non-intersecting and inside, + // but encloses a hole? + Rectangle2D r = new Rectangle2D.Double(x, y, w, h); + for (int path = 0; path < holes.size(); path++) + if (! ((Segment) holes.elementAt(path)).isSegmentOutside(r)) + return false; + + return true; } + + /** + * Determines if the Rectangle2D specified by r is completely contained + * within this Area, returns false otherwise.<P> + * + * This method should always produce the correct results, unlike for other + * classes in geom. + * @return true if the rectangle is considered contained + */ public boolean contains(Rectangle2D r) { return contains(r.getX(), r.getY(), r.getWidth(), r.getHeight()); } + /** + * Determines if the rectangle specified by (x,y) as the upper-left + * and with width w and height h intersects any part of this Area. + * @return true if the rectangle intersects the area, false otherwise. + */ public boolean intersects(double x, double y, double w, double h) { - // XXX Implement. - throw new Error("not implemented"); + if (solids.size() == 0) + return false; + + LineSegment[] l = new LineSegment[4]; + l[0] = new LineSegment(x, y, x + w, y); + l[1] = new LineSegment(x, y + h, x + w, y + h); + l[2] = new LineSegment(x, y, x, y + h); + l[3] = new LineSegment(x + w, y, x + w, y + h); + + // Return true on any intersection + for (int i = 0; i < 4; i++) + { + for (int path = 0; path < solids.size(); path++) + { + Segment v; + Segment start; + start = v = (Segment) solids.elementAt(path); + do + { + if (l[i].hasIntersections(v)) + return true; + v = v.next; + } + while (v != start); + } + for (int path = 0; path < holes.size(); path++) + { + Segment v; + Segment start; + start = v = (Segment) holes.elementAt(path); + do + { + if (l[i].hasIntersections(v)) + return true; + v = v.next; + } + while (v != start); + } + } + + // Non-intersecting, Is any point inside? + if (contains(x, y)) + return true; + + // What if the rectangle encloses the whole shape? + Point2D p = ((Segment) solids.elementAt(0)).getMidPoint(); + if ((new Rectangle2D.Double(x, y, w, h)).contains(p)) + return true; + return false; } + + /** + * Determines if the Rectangle2D specified by r intersects any + * part of this Area. + * @return true if the rectangle intersects the area, false otherwise. + */ public boolean intersects(Rectangle2D r) { return intersects(r.getX(), r.getY(), r.getWidth(), r.getHeight()); } + + /** + * Returns a PathIterator object defining the contour of this Area, + * transformed by at. + */ public PathIterator getPathIterator(AffineTransform at) { - // XXX Implement. - throw new Error("not implemented"); + return (new AreaIterator(at)); } *** Patch too long, truncated *** _______________________________________________ kaffe mailing list [EMAIL PROTECTED] http://kaffe.org/cgi-bin/mailman/listinfo/kaffe