05-830, Advanced User Interface Software, Spring, 2009

This is the OLD version of the course -- please see the Spring, 2013 version

Homework 2: Object Oriented Graphics

18% of grade. Wed, Jan 21, 2009 - Wed, Feb 18, 2009

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, which is set up as an Eclipse project.

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);
Based on Jeff Stylos's research (see reference), each class should also support a default constructor with no parameters, that sets all the fields to an appropriate default value, e.g., public Rect();

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 of the outside of the object. 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). Note that users of this API will not call draw -- it is only called by your toolkit itself. Users will just set properties, and your toolkit will automatically call draw at the appropriate times.

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 a 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. You will probably only need to use this method if you implement RotateGroup, below.

Groups

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);

Plus the default constructors, of course. 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<GraphicalObject> 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 setGroup() 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. Note that objects should not call draw when properties change -- draw will be called later, often with a collection of objects to be redrawn. (See the discussion in lecture about the tradeoffs of calling draw() on all children vs. calling draw() on only the children that intersect the damaged areas. In this assignment, we are just redrawing all children, but with an appropriate clipping rectangle.)

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.

The top-level call to draw() is the redraw call in the TestFrame.java file. Note that the test programs like TestRect.java call redraw after each change.

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 of GraphicalObjects, 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 toolkit, we'd want to use floating-point coordinates.).

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.

The test programs supplied to you should not need to be changed. If you seem to need to change them, your design is probably wrong.

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

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:

Groups:

Layouts:

It is not necessary to make the program thread-safe. In this homework, everything will be done in the same thread. However, extra points if you do make everything thread-safe so things can go in different threads, and if you provide a good test program that demonstrates it.

Resources

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.

Files:

All the files can be found in this ZIP archive, which we have arranged using an Eclipse project.

hw2-files.zip

(Last revision of this page: 2/10/2009)

Back to Homework Overview
Back to 05-830 main page