Homework 2: Object Oriented Graphics

05-830, User Interface Software, Fall, 2004

Assigned: Sep 7
Due: Oct 5

In this assignment, you are going to create an object-oriented graphics system that provides a retained-object model to automatically handle redrawing. You will create a set of Java classes out of which graphical objects can be constructed. Your classes will be responsible for handling refresh when an area of the screen is damaged and needs to be redrawn.

All the interface and Java files you need are in hw2-files.zip.

Graphical Objects

You will provide the following graphical object classes:

These classes should have the following required constructors:

public Rect (int x, int y,
             int width, int height, 
             Color color, 
             int lineThickness);

public FilledRect (int x, int y,
                    int width, int height, 
                    Color color);

public Line (int x1, int y1,
             int x2, int y2, 
             Color color,
             int lineThickness);

public Icon (Image image, 
             int x, int y);

public Text (String text,
             int x, int y,
             Font font,
             Color color);

Your graphical objects should be drawn using the Java 2D graphics model. Each object corresponds to a method of java.awt.Graphics2D. Rect corresponds to drawRect(), FilledRect to fillRect(), Line to drawLine(), Icon to drawImage(), and Text to drawString().

For Rect, FilledRect, and Icon, (x,y) is the top-left corner. For Line, (x1,y1) and (x2,y2) are the endpoints of the line. For Text, (x,y) is the position of the first character's baseline

Each class should have a getParam() and setParam() method for each parameter in its constructor. For example, Rect should have the following methods:

public int getX ();
public int getY ();
public int getWidth ();
public int getHeight ();
public Color getColor ();
public int getLineThickness ();

public void setX (int newX);
public void setY (int newY);
public void setWidth (int newWidth);
public void setHeight (int newHeight);
public void setColor (Color newColor);
public void setLineThickness (int newLineThickness);

All your graphical object classes must implement the GraphicalObject interface:

public interface GraphicalObject {
     public void draw (Graphics2D graphics, Shape clipShape);
     public Rectangle getBoundingBox ();
     public void moveTo (int x, int y);
     public Group getGroup ();
     public void setGroup (Group group);
     public boolean contains (int x, int y);
     public void setAffineTransform(AffineTransform af);
     public AffineTransform getAffineTransform();


draw() draws the graphical object on a drawing surface, clipped to the specified clipping shape (usually a rectangle -- unless you implement rotations, discussed below), which will be in the coordinate system of the group that the graphical object is in (see below). (The clipping shape is in the coordinate system of the group's container).

getBoundingBox() returns the smallest rectangle that contains all the pixels drawn by the graphical object. Your graphical objects will have to accurately compute their bounding boxes, taking into account line thickness and the drawing behavior of Java2D. For Text objects, you might use java.awt.FontMetrics to find the bounding box. The bounding box should be the smallest box such that if one were to draw a white filled rectangle using that box, all the pixels would be erased. Or if I set a clip rectangle to that box and drew the object, none of the object would get clipped. Note that the Rectangle returned by getBoundingBox() is java.awt.Rectangle, which is different from the Rect graphical object you have to implement.

The methods getBoundingBox, contains, and moveTo all use the coordinate system of the parent of the graphical object (the group that this object is in). That is, they are in the same coordinates of the X and Y for a rectangle.

Setting the width and height of text or image objects can be defined to be a no-op. Note that the getX and getY and setX and setY for text objects will be the baseline position, but of course, the x and y in the Rectangle returned by getBoundingBox must be for the outside of the text object. However, calling MoveTo on a text object should always move the top left corner to the specified position passed to MoveTo (therefore, mytext.moveTo(10,10) and mytext.setY(10) will not go to the same Y value).

Note that by default, drawing a filled rectangle and an outline rectangle in Java2D using the same values for width and height actually draws different size rectangles, which I think is weird. Please have the width and height parameters of both Rect and FilledRect draw the same size object. In particular, if x is 0 and the width is 3, then pixels 0, 1, and 2 should be drawn by both.

moveTo() moves the graphical object so that the top-left corner of its bounding box is at (x,y). You have to determine how this affects the coordinates of the underlying graphical object. Calling moveTo() on a Line object should move both endpoints so that the line has the same angle and length after the move.

getGroup() and setGroup() get and set the group to which the graphical object belongs. If the object doesn't belong to a group, getGroup() returns null. Groups are described in more detail next.

setAffineTransform is used to tell the object the transform that should be used to draw the object itself, and getAffineTransform returns the transform that was previously set. The object should assume the identity transform if setAffineTransform is not called. This will be useful for both ScaleGroup and RotatedGroup.


You will also provide the following grouping classes:

These classes have the following required constructors:

public SimpleGroup (int x,
                    int y
                    int width,
                    int height);

public LayoutGroup (int x,
                    int y,
                    int width,
                    int height,
                    int layout,
                    int offset);

public ScaledGroup (int x,
                    int y,
                    int width,
                    int height,
                    double scaleX, 
                    double scaleY);

Each group class should provide setParam() and getParam() methods for each parameter in its constructor.

A group defines a new coordinate system for its children. The coordinates of the children are interpreted relative to the group's origin (the x, y point passed to the group constructor). Children should be clipped to the bounding box of the group.

All grouping classes should implement the Group interface:

public interface Group extends GraphicalObject {
    public void addChild (GraphicalObject child);
    public void removeChild (GraphicalObject child);
    public void resizeChild (GraphicalObject child);
    public void bringChildToFront (GraphicalObject child);
    public void resizeToChildren ();
    public void damage (Rectangle damagedArea);
    public java.util.List getChildren ();
    public Point parentToChild (Point pt);
    public Point childToParent (Point pt);

    public static final int HORIZONTAL = 1;
    public static final int VERTICAL = 2;

Notice that the Group interface extends GraphicalObject, so your group classes must implement the GraphicalObject methods too.

addChild() and removeChild() add and remove graphical objects from the group. These methods should call setParent() on the child. If one attempts to add an object to more than one group, this should raise an exception.

resizeChild() notifies the group that the bounding box of one of its children has changed.

bringChildToFront() changes the display order of the children so that the specified child is drawn in front of other children. The default drawing order draws the last child added in front.

resizeToChildren() changes the group's width and height to fit around its children as tightly as possible. The children should not be repositioned , so children at negative (x,y) positions will still lie outside the group's bounding box after this method is called.

damage() collects rectangles that need to be redrawn. When a graphical object changes, it should call the damage() method of the group it belongs to, passing its old bounding box and its new bounding box if it has moved. If a group object is part of another group object, it should propagate the damaged rectangle up to its parent group. The next time draw() is called on the group, it should call draw() on all children that intersect the damaged region. 

The damage rectangle passed up from a group should use coordinates with respect to the group's container.

When draw() is called on the group, it should call draw() on all children. Note that it will have to transform the coordinates of the rectangle to make sure they are correct for the objects inside the group.

LayoutGroup has two extra parameters that determine how its children are positioned. The layout parameter has the following possible values, defined as constants in the Group interface. HORIZONTAL means that children are placed side by side from left to right, with top edges aligned. VERTICAL means that children are arranged top to bottom, with left edges aligned. The offset parameter specifies how much space to put between each child. Offsets may be negative, in which case adjacent children should overlap. The offset is not used before the first child, which should always be placed at position (0,0). Children should be laid out in drawing order, so bringChildToFront() should make the specified child the last object in the layout.

Extra credit is given for implementing other kinds of automatic layout; see below.

ScaledGroup draws its children scaled in x and y. Scale factors less than 1.0 shrink the children, and scale factors greater than 1.0 enlarge them. Scaling affects both positions and size, so two children of a ScaledGroup with scaleX=0.5 are not only half as wide but also half as far apart (in the x direction, at least). Be sure to take scale factors into account when you compute bounding boxes and damage rectangles.

Note that groups are transparent, so the parts of a group in which there are no graphical objects should show whatever was there before the group was drawn.

getChildren() returns a list of the group's children. List refers to java.util.List, which is an interface implemented by several java.util classes, including Vector, LinkedList, and ArrayList. The children should be listed in display order, so the last child in the list is the frontmost. The caller should treat the return value of getChildren() as immutable.

parentToChild() and childToParent() translate between the group's coordinate system and its parent's coordinate system. parentToChild() takes a point in the parent coordinate system and maps it down to the group's coordinate system. For example, if the group is located at (5,10) in its parent, then parentToChild(Point(5,10)) should return Point(0,0). Similarly, childToParent() maps a point in the group's coordinate system up to the parent coordinate system. Most groups will implement these methods as simple translations, but ScaledGroups should take scaling into account as well. (Note: since Point represents integer coordinates, you'll lose some precision if you put a ScaledGroup inside another ScaledGroup. If accurate scaling were important to our interactors, we'd want to use floating-point coordinates.) Example implementations of parentToChild() and childToParent() can be found in the reference implementation for Homework 2.

Other Notes

Do not change the GraphicalObject or Group interfaces. Your code should work with GraphicalObjects and Groups written by other developers. The test driver used for grading will include GraphicalObject and Group implementations that your code has never seen before.

Be careful with ungrouped objects. Your graphical objects should not throw any exceptions if they are used without being added to a group.

When working with java.awt.Image, which you'll need to do to implement Icon, you'll see that some methods take an ImageObserver argument. ImageObserver is used for images that are incrementally loaded over the network. Our test drivers will ensure that images are fully loaded before passing them to your Icon class, so you don't need to provide an ImageObserver. Just pass null to any methods that require an ImageObserver.

Extra Credit

For extra credit, you can implement more graphical objects, groups, and layouts than the minimum requirement. Here are some ideas. You may have others.

Graphical objects:




You will be provided with source code for the GraphicalObject and Group interfaces. You will also receive source code for some test programs. These programs have main() methods, so you can run them directly. The tests we provide are far from complete, so you should certainly write your own. We will use these test programs and others to grade your project. You can modify the test programs for your own use, but your code shouldn't depend on any changes you make.

If you implement additional graphical objects, groups, or layouts, please provide a custom test program for each one that demonstrates its features.


All the files can be found in this ZIP archive:


Back to Homework Overview
Back to 05-830 main page