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

Homework 2: Object Oriented Graphics

18% of grade. Thursday, Jan. 26, 2017 - Tuesday, Feb. 14, 2017 at 1:30pm (before class)

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-swing.zip. This is set up as an Eclipse project. I prefer if you use Eclipse (in particular, Eclipse Neon.2 Release (4.6.2), available from here.)

If you have questions about this homework, it is best to post them on the class Piazza page, and then I will answer the questions for everyone.

Graphical Objects

You will provide the following graphical object classes:

These classes should have the following required constructors:

public OutlineRect (int x, int y,
            		 int width, int height, 
             		 java.awt.Color color, 
             		 int lineThickness);

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

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

public Icon (java.awt.Image image, 
             int x, int y);

public Text (Graphics2D graphics, //a graphics object is needed to calculate the size of the string in Java
	          String text, 
             int x, int y,
             java.awt.Font font, // font-size specified as part of the font object in Swing
             java.awt.Color color);

For the java.awt.Image passed to Icon, there is a helper routine in TestFrame called loadImageFully() which loads the image file by name to create the java.awt.Image object.

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

Your graphical objects should be drawn using the Java 2D graphics model. Each object corresponds to a method of the drawing packages: in java.awt.Graphics2D, the OutlineRect corresponds to drawRect(), FilledRect to fillRect(), etc.

For OutlineRect, FilledRect, and Icon, the (x,y) point is the top-left corner of the of the object (so it is inclusive). 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, OutlineRect should have the following methods:

public int getX ();
public int getY ();
public int getWidth ();
public int getHeight ();
public java.awt.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 (java.awt.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 BoundaryRectangle 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); //implementing these is optional
     public AffineTransform getAffineTransform();        //implementing these is optional
}
where the graphics parameter is a java.awt.Graphics2D, and clipShape is a java.awt.Shape; and setAffineTransform and getAffineTransform use the java.awt.geom.AffineTransform.

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 the graphics package. For Text objects, you might use java.awt.FontMetrics to compute 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 BoundaryRectangle returned by getBoundingBox() is different from the OutlineRect graphical object you have to implement. Also note that it is designed based on a java.awt.Rectangle, in having a width and a height.

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. It is OK if contains() just checks if the point is inside the bounding box.

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 in awt, drawing a filled rectangle and an outline rectangle 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 OutlineRect 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.

Supporting affine transforms is optional (if you don't support them, just leave these methods with empty bodies). 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 these methods 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 (BoundaryRectangle damagedArea);
    public java.util.List<GraphicalObject> getChildren();
    public java.awt.Point parentToChild (java.awt.Point pt);
    public java.awt.Point childToParent (java.awt.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. removeChild() causes the child object to now be in no group.

resizeChild() notifies the group that the bounding box of one of its children has changed, and should be called by graphical objects when they change size and they are in a group.

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, and the first child added to the group is in the back.

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. This is a helper function for the users of your toolkit, and should not be called automatically.

The top-level window object, here "TestFrame" and its subclasses, like TestAllObjects, implements the group interface, and you should put one SimpleGroup object in the TestFrame group, and then add all your objects to that SimpleGroup object. The only objects that should be visible are ones that are in a group in the parent chain up to that SimpleGroup object. That is, if an object obj1 is added to a group whose parent (or parent's parent, or parent's parent's parent, etc.) is the top-level SimpleGroup object, then obj1 will be displayed, but if it is not in a group or if it is removed from its group, then it should not be displayed.

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 TestOutlineRect.java call redraw after each change.

You may want to refer to the lecture and to the required paper by Kosbie, et. al for more details about how drawing and redrawing should work.

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 both half as wide and 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 the rectangles for the damage area.

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. There are security issues with returning a list that the toolkit is using, so you must create a new list (e.g., a copy of your internal list). However, you should not copy the graphical objects themselves, since users of your toolkit will need to modify the graphical objects.

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 Important Notes

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:

Implement a method on GraphicalObject that determines if an (x,y) point hits the object (i.e., is part of the drawn part of the object). That is, gObj.hits(x,y) returns true if and only iff x,y is a pixel drawn by gObj. Easy for FilledRects, somewhat or quite tricky for all other kinds of 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

The zip file includes the source code for the GraphicalObject and Group interfaces, along with the source code for some test programs. These programs have main() methods in Swing 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 (that is, the original versions of the Test programs should still work).

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-swing.zip

Remember, there is a Piazza page for this class which may have answers to your questions.
(Last revision of this page: 1/24/2017)

Back to Homework Overview
Back to 05-830 main page