COMP 14 Prasun Dewan1 13. Graphics In this chapter, you will learn how to create and animate graphics. Graphics is not conceptually complicated. There are low-level details you would need to master if you were to implement graphics using Java libraries. Fortunately, ObjectEditor can hide you from these details. So you will put on its training wheels again, and take them off later in a couple of chapters when you learn details of Java toolkit libraries. This way you can separate the concepts from the details, as you did when creating textual user-interfaces. The examples you will see in this chapter require the use of two predefined Java types, Vector and Enumeration. Vector provides a powerful built-in facility for defining dynamic collections. Once you learn how to use it, you will probably never use an array again. A Vector is much like the dynamic collections you saw earlier such as history, database, and set – it provides many of the operations offered by these collections plus some additional ones. It is built on top of the type Enumeration, which is like the CharEnumeration type you saw earlier - instead of enumerating characters, it can enumerate arbitrary objects. Therefore, learning how these two types work will be trivial – in fact, you can probably easily implement most aspects of them at this point. The only subtlety has to do with casting values retrieved from them to the right type. Moreover, they will solidify your understanding of the basic idea of dynamic collections and enumerations. Square with Text Figure 1 is an example of a graphics-based user interface. It displays a square with a text box centered in it. The square can be moved and resized, but the text box cannot be edited directly. If we move or resize the square, the text box is repositioned to stay centered in the rectangle.2 Copyright Prasun Dewan, 2000. To move the rectangle, you can press the select radio button and drag it with the left mouse button pressed. If the Immediate toggle button is pressed, then the text box moves as the rectangle is dragged. Otherwise, it moves after the user releases the left mouse button. 1 2 1 Figure 1 Text Stays Centered as Square Moves The user-interface, as described above, looks trivial. When we think of implementing it, however, it seems much more complicated, as we must consider several details such as how the square and text box should be drawn on the screen and how the move and resize commands should be implemented, worrying about how mouse dragging events should be intercepted and translated into position and size changes. Fortunately, ObjectEditor allows us to ignore these details, because built into it is a graphics editor that displays several geometric shapes such as points, lines, squares, ovals, and text boxes, and provides commands to move and resize them. All we have to worry about is what shapes we want displayed on the screen, and what their sizes and positions should be. Pixels and Coordinate System In order to specify sizes and positions, we need to understand the coordinate system available to us. As it turns out, the coordinate system familiar to us from Mathematics cannot be used directly. In the Mathematics coordinate system, we can specify continuous values along the X and Y dimensions, locating a point with arbitrary precision. As we saw earlier, computer numbers cannot model real numbers precisely. More important, the computer cannot draw at arbitrary locations on the screen. It can draw only at a subset of the infinite points on the screen called the screen pixels. 2 (0,0) (3,2) X pixels Y Figure 2 Java Coordinate System These are the points the hardware drawing gun colors to draw on the screen. The pixels are equally spaced in both the X and Y directions. The pixel spacing or pixel unit determines the resolution of the screen – the higher the resolution, the smaller the pixel unit. If the pixel spacing is p (in some unit of length such as millimeters) on a screen with width W and height H, then the resolution of the screen is W/p by H/p. The coordinates of a point are given in pixel units. Thus, the coordinate (x, y) indicates a pixel that is x pixel units from the origin in the X direction and y pixel units from the origin in the Y direction. Since graphics coordinates refer to discrete rather than continuous points on the screen, they are specified as int values. Because they are specified in pixel units, the actual location of a point, (x,y) depends on the resolution of the screen. Thus, if on a particular screen, the point (256, 174), is at a location 10cm from the Y axis and 8 cm from the X axis, then on a screen of the same size with twice the resolution, it will be 20cm from the Y axis and 16 cm from the X axis. Each window is associated with its own coordinate system so that we can create shapes independently in different windows. The origin of the coordinate system is the upper left corner of the window- X values increase from left to right; and Y values increase from up to down and not down to up as we are used to from Mathematics. The reason for not following the Mathematics convention is that it is usual to think of the upper left corner of a window as its position and specify positive coordinates, relative to this corner, for points within the window. Specifying Shapes With this coordinate system, we can describe arbitrary two-dimensional shapes. In general, a shape can be fairly complex, and the means for specifying thus, can also get arbitrary complex. If we restrict ourselves to lines, rectangles, and ovals, however, we can use the following simple mechanism for specifying a shape. 3 x2, y2 x1, y1 h2 h1 w1 w2 x3, y3 h3 w3 Figure 3 Specifying Shapes Using Rectangular Bounds A rectangle described by the tuple (x, y, w, h), where (x, y) are the coordinates of its upper left corner, and w and h are its width and height, respectively. The pair (w, h) is called the size of the rectangle. An oval or line is described by specifying the rectangle that bounds it. Thus, the rectangle, line, an oval of Figure 3 are described the tuples (x1,y1,w1,h1), (x2,y2,w2,h2), and (x3, y3, w3, h3) respectively. Both the width and height can be negative, in which case, (x,y) is not truly the upper left corner of the rectangle. The above tuple defines a rectangle in which one corner it at position (x, y) and the diagonally opposite corner is at position (x+w, y+h). Graphical Constraints We are now ready to define the user-interface of Figure 1. Though it looks very different from textual userinterfaces seen so far, in fact, it is very similar to the spreadsheet user-interfaces. Compare it with the BMI spreadsheet. Just as the BMI spreadsheet defines a couple of editable items, the height and weight, and a read-only item, the BMI; this user-interface also defines an editable item, the square, and a read-only item, the text. Moreover, just as the spreadsheet defines constraints among the displayed items, automatically updating the BMI when the height or weight is edited, this user-interface automatically updates the text box when the square is edited. The only difference is that, in this user-interface, constraints are defined among graphical rather than textual elements. In fact, we can represent graphical elements as read-only or editable properties of an object and use getter and setter methods to implement the constraints among them, leaving the display and editing of these properties to ObjectEditor, as we did for textual elements. Figures 3 and 4, the implementation of the example user-interface, show how. Figure 3 gives the interface, SquareWithText, of the object representing the square with centered text. The square is described by the class, shapes.RectangleModel, while the text is described by the class, shapes.TextModel. Neither of these classes is part of the standard Java library; they are defined in the library shapes.jar we included in the class path. The reason for adding the suffix, “Model”, to their names will be clear when we study the Model/View/Controller framework in the next chapter. We use the class TextModel instead of String to describe the text, because unlike the latter, it describes text that is displayed in a potentially movable and resizable textbox in a graphics window instead of a static text field in a form-based ObjectEditor window we have seen in the previous chapters. Similarly, we use the 4 class RectangleModel instead of the Java class Rectangle to describe a rectangle that is represented graphically rather than textually as a list of form items displaying its properties. import shapes.RectangleModel; import shapes.TextModel; public interface SquareWithText { public RectangleModel getSquare(); public void setSquare(RectangleModel newVal); public TextModel getText (); } Figure 4 Interface SquareWithText Thus, the getter and setter methods, getSquare() and setSquare(), describe the editable square shape, while the getter method, getText(), describes the read-only text displayed in the graphical window. import shapes.RectangleModel; import shapes.TextModel; public class ASquareWithText implements SquareWithText { static final int INIT_X = 10; static final int INIT_Y = 10; static final int INIT_SIDE_LENGTH = 100; static final String TEXT_STRING= “hello”; RectangleModel square; TextModel text; public ASquareWithText() { square = new RectangleModel (INIT_X, INIT_Y, INIT_SIDE_LENGTH, INIT_SIDE_LENGTH); text = new TextModel(TEXT_STRING); placeText(); } void placeText() { text.setCenter(square.getCenter()); } public RectangleModel getSquare () { return square; } public void setSquare (RectangleModel newVal) { square = newVal; placeText(); } public TextModel getText () { return text; } } Figure 5 Graphical Constraints Figure 4 gives the class, ASquareWithText, implementing this interface. It declares separate instance variables for the two properties. The getter methods simply return the values of these variables. The setter method for the square, called when the user edits the square, sets the value of the square instance variable to the edited value, and calls the method placeText(); which in turn, uses the getCenter() and setCenter() methods defined by the two shapes to center the text in the edited square. Finally, the constructor assigns initial values to the two instance variables. As we can guess from the code, the constructor for RectangleModel , like the Rectangle constructor we saw earlier, takes as arguments bounds of the rectangle (x, y, width, height); and the constructor of TextModel takes as an argument the 5 string to be displayed in the text box. A textbox automatically computes its bounds from the string it contains, which can be changed later, as discussed below. Thus, the implementation of this example is fairly straightforward, and more important, follows the pattern of the code we saw in Chapter 3. The only new feature we learned here the classes representing objects displayed graphically. Some AWT and ObjectEditor Graphics Classes The example above shows only some of the graphics classes we can use. Some of the graphics classes are provided by the package java.awt, which is part of the Java distribution. Some, like RectangleModel, are provided by the package shapes, which is part of the ObjectEditor distribution. We will refer to the former as AWT classes and the latter as ObjectEditor classes. Let us first consider some of the AWT graphics classes. The class Rectangle describes the rectangular bounds of a shape. It provides a constructor that takes the four integers as arguments and creates a new instance representing the these bounds: Rectangle r = new Rectangle(x,y,width,height); It also allows us to retrieve and change components of these bounds. The methods getLocation() and setLocation()can be used to retrieve and set the location of the upper left (north west) corner of the rectangle, while the methods, getSize() and setSize() can be used to change the size of the rectangle. A location is represented by an instance of the class Point, which provides a constructor that takes the x and y coordinates of the location as arguments and constructs a new instance representing the point: r.setLocation(new Point(x, y)); Similarly a size is represented by an instance of Dimension, which provides a constructor that takes the width and height of the rectangle as arguments and constructs a new instance representing the size: r.setSize(new Dimension(width, height)); Unfortunately, these classes do not provide getter and setter methods for setting all components. The class Point provides public access to the two coordinates by defining the public variables, x and y. Similarly, Dimension defines the public variables, width and height, and Rectangle defines the public variables, x, y, width and height. The x and y variables of a Rectangle provide access to the coordinates of its upper left corner, that is, its location.3 Suppose we execute the following code: Rectangle r = new Rectangle(0, 5, 50, 100); r.x = 10; r.width = 150; Dimension d = r.getSize(); Point p = r.getLocation(); Then the following four expressions evaluate as shown below: p.x 10 p.y 5 d.width 150 d.height 100 Another useful AWT class is Color, which provides the following static public named constants, black, red, pink, orange, yellow, green, magneta, cyan, and blue, for representing different colors. Again, the designers of this class violated one of our conventions – the constants are named using lowercase letters rather than upper case letters. 3 Exposing components as public variables does not make it possible, for instance, to change the Cartesian representation of a Point to a Polar one. 6 The ObjectEditor graphics classes are built on top of these AWT classes. Besides TextModel and RectangleModel, three other classes are provided by ObjectEditor, PointModel, LineModel and OvalModel, for specifying points, lines and ovals that are represented graphically. Like the constructor of RectangleModel, these types offer constructors that take as arguments the rectangular bounds of the shape to be created. For instance, the expression: new LineModel(5, 10, 30, 40) creates a line between the upper left corner and lower right corner of a rectangle whose upper left corner is (5, 10) and whose width and height are 30 and 40 respectively; and the expression: new OvalModel(5, 10, 30, 40) inscribes an ellipse in the same rectangle. Once we have created one of these shapes, we can retrieve and set the components of its bounds using the methods getLocation(), setLocation(), getSize(), and setSize(), which work the same way as the corresponding methods provided by Rectangle. These methods are sufficient to retrieve and manipulate any aspect of the bounds of the shape. However, using them may require some calculations on our part. For instance, if we wish to move the center of a shape to the center of another shape, we would have to calculate the center of the latter from its upper left corner, and then calculate the upper left corner of the former from its center. To spare us from such calculations, the shape classes provide several additional convenience methods such as getCenter() and setCenter() we saw above to access various aspects of the shape. The following are some of the methods provided by all of the ObjecEditor shape classes: getX(), setX(), getY(), setY(), getWidth(), setWidth(), getHeight(), setHeight(), which get and set int values. getCenter(), setCenter(), getNWCorner(), setNWCorner(), getNECorner(), setNECorner(), getSWCorner(), setSWCorner(), getSECorner(), setSECorner(), which get and set instances of (AWT) Point representing the center and four corners of the bounding rectangle. isFilled() and setFilled() to set a boolean property indicating if the shape should be filled when drawing it. getColor() and setColor() to get and set instances of Color representing the color of the shape. When you try and invoke an operation on an instance of one of these classes, the J++ editor will show you a menu of all available operations. The functionality of the operations will, typically, be clear from their names, parameter types, and return types. As you have probably noticed, the menu displayed by the J++ editor is very useful. Unfortunately, in the process of displaying the menu, at least on my computer, it sometimes crashes, giving the following uninformative error message, saying there is a pure virtual call: When this happens, you will lose all changes you have made! Therefore, be sure to save changes frequently if this ever happens to you. Also, to get around this problem, you will have to make sure that J++ does not 7 try to display the menu. Sometimes saving a file disables menu generation. If this does not work, type right to left. Assume we are trying to instantiate one of the shape model classes: new RectangleModel (INIT_X, INIT_Y, INIT_SIDE_LENGTH, INIT_SIDE_LENGTH); After we type new, J++ tries to give a menu of all the available constructors . To prevent it from crashing, type the constructor, with any parameters, first: RectangleModel (INIT_X, INIT_Y, INIT_SIDE_LENGTH, INIT_SIDE_LENGTH); and then enter the keyword new. Since the constructor is already is entered, it does not try to create the menu. It is important to distinguish between corresponding classes provided by ObjectEditor and AWT – RectangleModel and Rectangle, and PointModel and Point. The main difference is that ObjectEditor does not display instances of Point and Rectangle as shapes on the screen, while it does so for RectangleModel, PointModel, and the other shapes classes it defines. The following figure illustrates this, showing how an instance of Rectangle and a corresponding instance of RectangleModel are displayed by ObjectEditor.4 Demonstrating Details of Graphics Classes The following example demonstrates details of several of the AWT and ObjectEditor graphics classes. It creates an object with five graphical components: a rectangle, line, textbox, oval, and point with the following constraints: The bounds of the oval and rectangle are the same. The width of the rectangle, oval, line and textbox are the same. The center of the point is at the south west corner of the bounding box of the oval and rectangle. The northwest corner of the line is at the south west corner of this bounding box. The empty boolean property of a Rectangle returns true if the width or height of a rectangle is equal to or less than zero. ObjectEditor interprets zero and negaive values for width and height in the manner described in the section of Specifying Shapes. 4 8 The center of the text model is at the center of the bounding box. The object also allows the x coordinate and height of the bounding box to be changed, as shown in the figure below. The user- interfaces discussed previously in this chapter have all had only graphical elements, while the ones we saw earlier had only textual elements. ObjectEditor allows us to create user-interfaces that have both textual and graphical elements, as illustrated by this figure. ObjectEditor displays all properties of an object that are instances of its shape classes in the graphics window and all other properties in the text window. We will refer to these two kinds of properties as graphical and textual properties. If there is nothing to be displayed in either window, ObjectEditor does create the window. The interface of the object displayed in the window above is given below. import shapes.RectangleModel; import shapes.OvalModel; import shapes.LineModel; import shapes.PointModel; import shapes.TextModel; public interface ShapesDemo { public RectangleModel getRectangleModel(); public OvalModel getOvalModel (); public LineModel getLineModel (); public TextModel getTextModel (); 9 public PointModel getPointModel (); public int getX(); public void setX(int newVal); public int getHeight(); public void setHeight(int newVal); } All five graphical properties are associated with only getter methods. They are not associated with setter methods because they cannot be edited directly by the user in the graphics window. The other two properties are editable and are therefore associated with both setter and getter methods. The implementation of this interface is long, but straightforward. import shapes.PointModel; import shapes.TextModel; import java.awt.Rectangle; import java.awt.Point; import java.awt.Color; import java.awt.Dimension; public class AShapesDemo implements ShapesDemo { public static int INIT_X = 0; public static int INIT_Y = 0; public static int INIT_WIDTH = 100; public static int INIT_HEIGHT = 50; int x = INIT_X; // x coordinate of (NW corner of) rectangle and oval int y = INIT_Y; // y coordinate of rectangle and oval int width = INIT_WIDTH; // width of rectangle, oval, line and text int height = INIT_HEIGHT; // height of rectangle, oval and line Point location = new Point(x,y); // location of rectangle and oval //size of rectangle, oval, and line Dimension size = new Dimension(width, height); // bounds of rectangle and oval Rectangle bounds = new Rectangle(location, size); RectangleModel rectangleModel = new RectangleModel(); OvalModel ovalModel = new OvalModel(); LineModel lineModel = new LineModel(); TextModel textModel = new TextModel("Shapes Demo"); PointModel pointModel = new PointModel(0,0); public AShapesDemo() { ovalModel.setFilled(true); ovalModel.setColor(Color.gray); constrainShapes(); } void constrainShaopes() { location.x = x; location.y = y; bounds.setLocation(location); size.width = width; size.height = height; bounds.setSize(size); rectangleModel.setBounds(bounds); ovalModel.setBounds(bounds); lineModel.setSize(size); lineModel.setNWCorner(ovalModel.getSWCorner()); pointModel.setCenter(ovalModel.getSECorner()); textModel.setWidth(width); 10 textModel.setCenter(ovalModel.getCenter()); } public int getX() { return x; } public void setX(int newVal) { x = newVal; updateShapes(); } public int getHeight() { return height; } public void setHeight(int newVal) { height = newVal; updateShapes(); } public RectangleModel getRectangleModel () { return rectangleModel; public OvalModel getOvalModel () {return ovalModel; } public LineModel getLineModel () {return lineModel;} public TextModel getTextModel () {return textModel;} public PointModel getPointModel () {return pointModel;} } } The class creates instance variables for the seven properties. It also creates several instance variables such as width and size for maintaining the constraints of the application and for demonstrating how values of the AWT graphical classes are manipulated. Most of the variables are initialized completely while declaring them. The only one that is not is ovalModel, whose filled property is needs to be initialized. Therefore, the constructor of the class does this additional initialization. The method constrainShapes() implements the constraints of the application, calling several of the convenience methods provided by the ObjectEditor shape classes. This method is called by (a)the constructor to ensure that the initial configuration obeys the constraints, and (b) each of the setter methods, to ensure that the constraints are maintained after changes to the variables defining the constraints. This example demonstrates all of the AWT and ObjectEditor classes we have introduced in this chapter: Point, Dimension, Rectangle, Color, RectangleModel, OvalModel, LineModel, TextModel, and PointModel. As this example shows, we must import the AWT classes from the package java.awt and the ObjectEditor classes from the package shapes. In the examples we have seen so far, the number of graphics elements displayed on the screen was fixed – one square and one textbox. In the following example, we will see how to create a dynamic number of graphics elements. Exposing Dynamic Indexed Elements Suppose we wish to enter and view a series of points in some two-dimensional space. For instance, we wish to enter and view points on the trajectory of a rocket or the points in an exam curve. Figure 6 shows a userinterface that allows us to perform this task. 11 Figure 6 PointHistory It provides an operation, addElement(), for adding a new point whose x and y coordinates are arguments of the operation. Each added point is displayed in the ObjectEditor graphics window. As before, we would like to create an object responsible only for creating and manipulating the shapes, leaving the details of displaying and editing the shapes to ObjectEditor. To create a point on the screen, we can use the ObjectEditor class, PointModel, which, as we saw before, represents a point that is automatically displayed by ObjectEditor as a small, filled circle. In this example, we must create a dynamic number of points. We have seen in Chapter 12 how to create a dynamic collection of textual elements. We can create a dynamic number of graphics elements in much the same way. Let us, then, derive the interface of the object being edited in Figure 5. From the left window of Figure 5, we can directly derive one of the methods of this interface: public void addElement (int x, int y); If this object is not going to actually display the points, it must expose them to another object, in this case ObjectEditor, that performs this function. As we discussed in Chapter 12, it cannot use getter and setter methods to expose these elements, since they work only for objects with a static number of elements. To identify how a dynamic list of elements can be exposed, consider the StringHistory interface we defined to expose a dynamic collection of strings: interface StringHistory { public void addElement(String element); public String elementAt (int index); public int size(); } The methods elementAt() and size(), together, allow an external object to determine all the strings in the history. We can define the exact same method headers for this example, with the only difference that instead of strings we would expose values of type PointModel. Since this generalizes to indexable collections of arbitrary types of elements, ObjectEditor looks in an object for (a) a method named elementAt() taking a single int parameter, and (b) a parameter-less method named size() to determine if the object encapsulates an indexable dynamic collection; and uses these methods to access the elements of the collection. This is in the spirit of looking for methods whose names start with get to 12 determine and access the static properties of an object. In fact, we will refer to methods such as elementAt() and size() that retrieve dynamic components of a collection as getter methods, and methods such as addElement() that change the dynamic components of a collection as setter methods. Even though their names do not begin with get and set, they do perform the task of getting and setting components of an object. Thus, our interface becomes: import shapes.PointModel; public interface PointHistory { public void addElement (int x, int y); public PointModel elementAt (int index); public int size(); } We have named the interface PointHistory to because, like StringHistory, it also defines a history, allowing elements to be appended but not inserted in the middle or deleted. After implementing StringHistory, these three methods do not present any new implementation challenges. Instead of repeating the code we used in StringHistory, however, we will simply use Java vectors, which make the implementations of these methods trivial. Vectors Java vectors are instances of the class, java.util.Vector, which, like StringHistory and StringDatabase, defines variable-size collections. It defines the method: public final Object elementAt(int index) for returning the element at the specified index. The final keyword simply says that the method cannot be overridden in subclasses. Note that the type of an element is Object. This means that we can store arbitrary objects in a vector. It provides the method: public final void setElementAt(Object obj, int index) to set an element at a particular index. The constructor: public Vector() creates an empty vector. We can dynamically add elements to a vector using : public final void addElement(Object obj) Like addElement() in StringHistory, this method appends a new object to the end of the array. We can also insert an element in the middle of the array using: public final void insertElementAt(Object obj, int index) Once we have added an element, we may want to delete it. If we know its index, we can call: public final void removeElementAt(int index) This method removes the element at the specified index. If we know the element to remove, we can call: public final boolean removeElement(Object obj) This method is more complicated because the object may not exist or may have been added multiple times. It removes the first occurrence of the object, and returns false is there was no occurrence. Like StringDatabase, a vector provides a method determine the index of the first occurrenceof an object: public final int indexOf(Object obj) Finally, if we want to scan each element of the vector in succession, we can call: public final Enumeration elements() It returns an instance of the Java interface, java.util.Enumeration, representing the scanned elements. This interface defines the following methods: public boolean hasMoreElements(); public Object nextElement(); 13 Thus interface is much like the enumeration interfaces we have seen before such as CharEnumeration except that it enumerates arbitrary objects. As a result, it is appropriate for enumerating the elements of a vector, which, as mentioned above, can be arbitrary objects. There are several other methods defined by the class, but these will more than suffice for this course. The use of some of these methods is illustrated in the implementation of PointHistory given in Figure 6: import java.util.Vector; public class APointHistory implements PointHistory { Vector contents = new Vector(); public void addElement (int x, int y) { contents.addElement(new PointModel(x, y)); } public PointModel elementAt (int index) { return (PointModel) contents.elementAt(index); } public int size() { return contents.size(); } } Figure 7 Implementing a History using a Vector The class defines a vector variable, contents, for storing the points input by the user. The constructor initializes the variable to an empty vector. The elementAt(),addElement, and size() methods defined by this class call the corresponding methods on contents. The only tricky part has to do with the fact that elements of PointHistory are of type PointModel while elements of a vector are of type Object. Since the former is a subtype of the latter, the statement: contents.addElement(new PointModel(x, y)); is allowed because the type of the actual parameter, PointModel, is more specific than the expected type, Object. On the other hand, the statement: return contents.elementAt(index); is not allowed, since the type of the actual return value, Object, is more general than the expected type. 5 However, we do know that every element in a vector is a PointModel - the only way an element can get in the vector is by calling the addElement()method of PointHistory, which takes arguments of type PointHistory . Thus, we can safely cast the return value to the expected type, PointModel: return (PointModel) contents.elementAt(index); It would have been nice if it was possible to restrict the elements of a vector to a type specified by us, much as we can do for an array – however, that requires special language support not provided to programmerdefined types. Unlike an array, a vector is not part of the standard language but is provided by a library. Thus, we will often have to cast the types of values extracted from a vector based on the types of the values we put into it. In the above code, addElement(), adds a new instance of PointModel each time. What if, in the interest of saving space, we created a single PointModel: PointModel pointModel = new PointModel(); and made addElement() add this object always, after changing its position to the location of the new point: public void addElement (int x, int y) { pointModel.setX(x); pointModel.setY(y); 5 If these rules are still confusing, recall that we are happy to get a car that is different from the one we expected, as long as it has more features, and a subtype has more methods than its super types 14 contents.addElement(pointModel); } While this code is more efficient in that it creates a single PointModel, it does not work. The reason is that we end up adding a single point multiple times in the history, rather than multiple points! Just because we added a point at a different location in a vector does not mean Java created a new copy of it. You might be tempted to, instead, write the following code: public void addElement (int x, int y) { PointModel newPointModel = pointModel; newPointModel.setX(x); newPointModel.setY(y); contents.addElement(newPointModel); } This also does not work. The assignment statement: newPointModel = pointModel; does not create a new PointModel; it makes both variables point at the same object. We will study this issue in more depth later. It was mentioned here to prevent you from making subtle errors. In summary, the implementation of our second graphics example, like the first one, is not complicated once we understand how vectors work, how an object exposes its dynamic indexed elements, and that Java does not automatically copy objects for us. Animation The implementation gets trickier when we wish to animate the graphics. Consider an extension to the example above that provides a method, animate(), to trace the path defined by the points added in the history. More precisely, we wish to define a new point, colored white to distinguish it from the history points, which normally rests at some location, say (0, 0). When we call animate(), the point moves to the location of the first point we added to the history, then the second one, and so on (Figure 6). After reaching the last point, it returns to its normal resting position. (a) Animating Point Resting (b) Animating Point in Motion Figure 8 Animating the Trajectory defined by PointHistory To implement this extension, we must first create the animating point. This is not difficult, all we have to do is define a readonly point using a getter method: public PointModel getAnimatingPoint(); This point is readonly because the user does not directly manipulate it, only indirectly by calling animate().The implementation of this method is, of course, straightforward: 15 static final int ANIMATING_POINT_X = 0, ANIMATING_POINT_Y = 0; PointModel animatingPoint = new PointModel(ANIMATING_POINT_X, ANIMATING_POINT_Y); public PointModel getAnimatingPoint() { return animatingPoint; } The named constants define the normal resting position of the point. As their declaration shows, Java allows us to create and initialize multiple variables of the same type in a single declaration. Our next task is to color the point white. A Java library class, java.awt.Color, defines a wide range of colors, and all shapes (PointModel, RectangleModel, etc.) define a method, setColor(), to assign them one of these colors. Thus, we can write the following constructor to initialize the color of the animating point: static final Color ANIMATING_POINT_COLOR = Color.white; public APointHistory() { animatingPoint.setColor(ANIMATING_POINT_COLOR); } Now we come to the tricky part, the implementation of the method animate(). In this method, we must visit each point in the history in succession, and move the animating point to the location of the visited point. There are two ways to visit the history of points – use the vector method elementAt() to index contents or use the method elements()to enumerate contents, Because we are not accessing the elements at random indices, but instead, visiting them from the first to the last, let us enumerate the elements: public void animate () { Enumeration points = contents.elements(); while (points.hasMoreElements()) { animatingPoint.setCenter(((PointModel) points.nextElement()).getCenter()); } animatingPoint.setX(ANIMATING_POINT_X); animatingPoint.setY(ANIMATING_POINT_Y); } As in the first example, we are using the methods getCenter() and setCenter(), defined on all shapes, to move the shape. Once the animating point has visited all the points in the history, it returns to its resting position. Pausing Execution If we run animate(), it will not seem to do its job. The reason is that it executes the loop so quickly that the animating point will return to its resting position before we have noticed it has left, and thus not appear to move at all. In order to make its effects visible to the human eye; it must give us a chance to observe each of the locations it visited. The way to do this is to pause execution for some time after it visits each point. Java provides us with way to make a method pause or “sleep” for a period specified by us, as shown in the rewritten animation code shown in Figure 8: static final int ANIMATION_DELAY = 1000; public synchronized void animate () { Enumeration points = contents.elements(); while (points.hasMoreElements()) { animatingPoint.setCenter(((PointModel) points.nextElement()).getCenter()); try { Thread.sleep(ANIMATION_DELAY); } catch (InterruptedException e) { System.out.println("Animation interrupted"); } 16 } animatingPoint.setX(ANIMATING_POINT_X); animatingPoint.setY(ANIMATING_POINT_Y); } Figure 9 Animating the Trajectory The method, Thread.sleep(), makes animate() sleep for the number of milliseconds specified by its argument. We have specified a pause time of one second. There is nothing special about this interval – it could be smaller or larger – and it is best to experiment with different values of it before settling on one, or allow the user to set its value, as shown below. If we are interested in fixing the point velocity, then it depends on the distances between successive history points; and if we are interested in fixing the time to complete the animation, then it depends on the size of the history. A sleeping method may be woken up not only when its regular alarm goes off, but also because of some unexpected condition such as the user terminating the program. To signal an abnormal waking up, the method throws an InterruptedException. We have therefore enclosed the call to it in a try-catch block. Concurrency and Synchronization Notice the keywords, synchronized, in the header of animate(): public synchronized void animate () The reason for this keyword is subtle. For the animation to work, two methods must be active concurrently, the animate() method and a method called paint() in ObjectEditor that actually draws the animating point on the screen. If the two methods were executed serially, one after the other, then the drawing method would be executed before or after animate() is called, when the animating point is always at its resting position. Thus, it would never show any of the positions the point takes while animate() is executing! We will study later how we can start these methods concurrently in Java – for now we do not have to worry how this is done since ObjectEditor takes care of it for us, it being the object that calls both methods. When two methods are active concurrently, they can step on each other’s toes, leaving data they share in an inconsistent state. To understand the kind of problems that might occur, imagine two users trying concurrently editing a program without synchronizing with each other - they can change instance variables and methods independently, leaving the program in an inconsistent state! The keyword synchronized before a method tells Java to ensure that it does not interfere with concurrently executing methods. How Java ensures non-interference is beyond the scope of this course, and will be covered in-depth in your operating systems course. If we omit the keyword in a method declaration, ObjectEditor will not execute the method concurrently with other methods so that concurrency inconsistencies do not occur. We did not put this keyword in other methods because we were content with seeing their effects on the display after they finished execution. Thus, without this keyword, animate(), will not appear to work and will essentially have the same effect on the display as the previous version of the method, seemingly not moving the point. The only difference would be there would be long pause, equal to the sum of all the sleeps in the method, during which no other method can be invoked. As it turns out, this keyword cannot be used in a method declared in an interface. Thus, in the interface, PointHistory, we must declare the header of animate()as: public void animate() even though, in the implementation of the interface, we declare it as: public synchronized void animate () Normally, Java requires matching of all components of corresponding method headers in interfaces and the classes implementing it, but not in this case, probably to give the implementers of an interface the flexibility of deciding if they want to allow for concurrency and pay the cost of synchronization. 17 Summary Each window is associated with a coordinate system in which the origin is the top-left corner the window, x coordinates increase from left to right, and y coordinates from top to bottom. Computer-screen coordinates are specified as integer rather than real values because they address screen pixels, which are finite in number. A line, rectangle, or oval can be described by specifying its rectangular bounding box. Components of an object that are instances of certain predefined shape types are displayed graphically by ObjectEditor as the shapes they denote. Constraints can be defined among these components, and thus the associated screen images, much as they are defined among other kinds of components. A dynamic indexed collection should be made accessible to other objects using methods with standard names so that the code implementing the collection is easy to understand and the collection elements can be displayed by a generic tool such as ObjectEditor. Java provides a built-in type for defining a dynamic, indexed collection of arbitrary objects. Elements extracted from such a collection may need to be cast to the types of the elements put into the collection. Java also provides a built-in type for enumerating arbitrary objects, which also may need to be cast to more specific types. It is important to make a method pause after it creates the next animation image. A method that creates the next animation image must execute concurrently with a method that actually displays the image on the screen. A method that executes concurrently with one or more other methods should be declared as synchronized. Exercises 1. 2. 3. 4. 5. What is a screen pixel? Use rectangular bounds to describe a line between the points (50,50) and (25,25). Explain the convention we studied in this chapter to expose the elements of a dynamic indexed collection whose elements are of type T. Explain why a method that creates the next animation image must execute concurrently with a method that actually displays the image on the screen. Explain why a cast was needed in the animate() method shown in Figure 8. 6. In this problem, you will write a turtle object, shown in Figure 11 (a) that allows a user to move a turtle on the screen. 18 (a) Initial State (b) After Drawing Some Lines Figure 10 Turtle Graphics As shown in the figure, the “turtle” is represented by two adjacent, solid, circles, one for the body and another for the head. The head always points in the direction in which the turtle is pointing. The turtle can point in the north, south, east or west direction, and can move both forward and backward in the direction it is pointing. Initially, the turtle is pointing eastwards, as shown in the figure. As also shown in the figure, the turtle has three integer properties, StepSize, StepTime, and MoveDistance, and a boolean property, PenDown. In addition, it provides the following methods: 1. rotate(): Rotate the turtle 90 degrees in the anti-clockwise direction. 2. move(): Move the turtle the number of pixel units given by the MoveDistance property . Movement is in forward (backward) direction if argument is positive (negative). 3. animationMove(): Like move(), this operation moves the turtle the number of pixel units given by the MoveDistance property. While move() essentially makes the turtle take one 19 big step to its new position, this makes it take several small steps to its destination, animating the turtle as it moves. The size of the step, in pixels, is given by StepSize and the time to take a step is given by the property, StepTime. You are free to choose the sizes of the body and head, the size of the drawing window, and the initial position of the turtle. Once you have implemented these basic operations, allow users to draw lines by moving the turtle on the screen, telling the turtle to pick up or put down a pen as it moves (Figure 11(b)). The boolean property PenDown determines if the pen is up or down. Finally, implement an animation operation, spin(), that continuously rotates the turtle until the stop() operation is called. The operation should call rotate() once (that is rotate by 90 degrees) every step time. 20 21