March 6, 2000
Java Programming, Lecture Notes # 310
by Richard G. Baldwin
In an earlier lesson, I explained that the Graphics2D class extends the Graphics class to provide more sophisticated control over geometry, coordinate transformations, color management, and text layout. Beginning with JDK 1.2, Graphics2D is the fundamental class for rendering two-dimensional shapes, text and images.
I also explained that without understanding the behavior of other classes and interfaces such as Shape, AffineTransform, GraphicsConfiguration, PathIterator, and Stroke, it is not possible to fully understand the inner workings of the Graphics2D class.
This and an earlier lesson are intended to give you the necessary understanding of the Shape interface and the PathIterator class.
As I reported in the earlier lesson, according to Sun:
“The Shape interface provides definitions for objects that represent some form of geometric shape. The Shape is described by a PathIterator object, which can express the outline of the Shape as well as a rule for determining how the outline divides the 2D plane into interior and exterior points. Each Shape object provides callbacks to get the bounding box of the geometry, determine whether points or rectangles lie partly or entirely within the interior of the Shape, and retrieve a PathIterator object that describes the trajectory path of the Shape outline.” |
The Shape interface provides four groups of overloaded methods that make it possible to perform the following tasks:
I discussed the first three capabilities in the earlier lesson. I told you that I was going to defer a discussion of the PathIterator to this lesson. This lesson is dedicated the PathIterator interface, but in order to understand the behavior of that class, it will be instructive to provide a brief discussion and illustration of the GeneralPath class. You might consider the PathIterator and the GeneralPath to be opposite sides of the same coin.
This is part of what Sun has to say about PathIterator.
The PathIterator interface provides the mechanism for objects that implement the Shape interface to return the geometry of their boundary by allowing a caller to retrieve the path of that boundary a segment at a time. |
In other words, PathIterator makes it possible for code in a program to obtain information about the geometric outline of an object that implements the Shape interface.
Here is part of what Sun has to say about GeneralPath.
“The GeneralPath class represents a geometric path constructed from straight lines, and quadratic and cubic (Bezier) curves. It can contain multiple subpaths.
The winding rule specifies how the interior of a path is determined. There are two types of winding rules: EVEN_ODD and NON_ZERO.
An EVEN_ODD winding rule...” |
I plan to discuss the GeneralPath class in much more detail in a subsequent lesson. In this lesson, I will simply show you how to create a geometric shape using GeneralPath. Then I will show you how to use PathIterator to analyze that shape and to replicate it with an offset.
Here is what Java Foundation Classes in a Nutshell, by David Flanagan, has to say about GeneralPath.
“This class represents an arbitrary path or shape that consists of any number of line segments and quadratic and cubic Bezier curves. After creating a GeneralPath object, you must define a current point by calling moveTo(). Once an initial current point is established, you can create the path by calling lineTo(), quadTo(), and curveTo(). These methods draw line segments, quadratic curves, and cubic curves from the current point to a new point (which becomes the new current point).
The shape defined by a GeneralPath need not be closed, although you may close it by calling the closePath() method, which appends a line segment between the current point and the initial point... The append() method allows you to add a Shape or PathIterator to a GeneralPath, optionally connecting it to the current point with a straight line.” |
Since my purpose in using GeneralPath in this lesson is to provide background for understanding PathIterator, I will keep it simple and deal only with the following methods of GeneralPath:
This is what Jonathan Knudsen has to say on the subject in his excellent book entitled Java 2D Graphics, from O’Reilly.
“Lurking behind the Shape interface, there’s a handy toolbox of shapes in the java.awt.geom package – rectangles, ellipses, and so on. I’ll discuss these soon. If you want to draw pentagons, decagons, stars, or something completely different, you’ll have to describe the path yourself using a java.awt.geom.GeneralPath. This class implements the Shape interface and allows you to build a path, segment by segment.” |
The bottom line is that a Shape object is constructed from segments consisting of moves (pen up), lines, curves, and an optional closure. GeneralPath can be used to construct such a Shape, one segment at a time.
The Shape object remembers how it was originally constructed in terms of the types of segments, their coordinate values, etc. The object can tell us how it was originally constructed. The PathIterator is used to extract that information from the Shape object.
The two following programs show how to construct a simple Shape consisting of moves, lines, and a closure, and how to extract the necessary information from the Shape to replicate it with an offset. I didn’t use Bezier curves. The topic of Bezier curves is sufficiently complex as to merit a lesson of its own. I will be discussing the use of Bezier curves in a subsequent lesson.
This first sample program illustrates the use of the GeneralPath class to construct a simple Shape object.
The program draws a four-inch by four-inch Frame on the screen. Then it translates the origin to the center of the Frame. It draws a pair of X and Y-axes centered on the origin.
This discussion of dimensions in inches on the screen depends on the method named getScreenResolution() returning the correct value. However, the getScreenResolution() method always seems to return 120 on my computer regardless of the actual screen resolution settings. |
Then the program uses GeneralPath to create a diamond Shape and draw it on the Frame. The vertices of the diamond shape are at plus and minus one-half inch on each of the axes. When viewed on the screen, the vertices are at the North, South, East, and West positions and the diamond is centered on the origin.
The program was tested using JDK 1.2.2 under WinNT Workstation 4.0.
As is usually the case, I will discuss this program in fragments. The controlling class and the constructor for the GUI class are essentially the same as you have seen in several previous lessons, so, I won’t bore you by repeating that discussion here. You can view this material in the complete listing of the program at the end of the lesson.
All of the interesting action takes place in the overridden paint() method, so I will begin the discussion there.
The beginning portions of the overridden paint() method should be familiar to you by now as well. So, I am simply going to let the comments in Figure 1 speak for themselves.
Now we have arrived at the interesting part. Figure 2 begins by instantiating a new object of the GeneralPath class.
Then it invokes the moveTo() method to establish the current point (as described above by Flanagan).
As a practical matter (see the caveat below), if you think of the creation and population of a GeneralPath object as analogous to creating a line drawing using a pen and paper, invocation of the moveTo() method corresponds to moving the drawing pen without the tip of the pen touching the paper.
Continuing with our pen and paper analogy, the program invokes the lineTo() method three times in succession to draw three straight lines on the paper. Given the coordinates shown in Figure 3, these lines form three sides of a diamond shape.
Finally, the program invokes the closePath() method to draw a single straight line from the current point back to the current point established by the original moveTo() method call. This line forms the fourth side of a diamond shape. See Figure 4 for this code fragment.
The problem with using the pen and paper analogy to describe the process is that the process implemented in the above fragments really doesn’t draw anything (not anything that a human can see, anyway). Rather, it creates and populates an object that implements the Shape interface that exist only in the computer’s memory.
This is a very important point that we must keep in mind. Creating a Shape object in the computer’s memory is not the same thing as rendering that object on a graphics display device. The object doesn’t become visible until it is rendered. However, the fact that it isn’t visible doesn’t mean that it doesn’t exist. That just means that it hasn’t been rendered.
The Shape object can be rendered onto an output device using the draw(Shape) method shown in Figure 5.
When the virtual object created and populated by the previous fragments is rendered, it will look like a diamond shape, centered on the origin, with the vertices at the North, South, East, and West positions. The shape of the object and its location is controlled by the coordinate information passed to the moveTo(), and lineTo() methods in the above fragments, and the attributes of the AffineTransform object currently associated with the Graphics2D object.
Now that we know how to create and populate a GeneralPath object, the next program will illustrate how to extract the necessary information from that object to replicate it.
The purpose of this program is to illustrate the use of the getPathIterator() method of the Shape interface, and the PathIterator object returned by that method. The program uses that object to learn all that there is to know about an object that implements the Shape interface.
The program creates and populates a GeneralPath object identical to that in the previous program.
Then the program invokes getPathIterator() on that GeneralPath object to obtain a PathIterator object that provides access to information about the GeneralPath object.
Then the program invokes various methods on the PathIterator object to extract the pertinent information about the GeneralPath object that it represents. This information is used to replicate the original object, offset by one inch in both directions. The segment information is also displayed on the command-line screen so that you can compare it with what you know to be true about the original object.
Then the program draws the new object in red.
I will discuss this program in fragments. This program is identical to the previous program down to the point where the original GeneralPath object has been created, populated, and displayed on a Frame object. Therefore, I won’t repeat the discussion of that material. You can view the code that accomplishes this in the complete listing of the program at the end of the lesson.
For continuity, this fragment picks up at the point in the overridden paint() method where the original object, referred to by thePath, is being rendered on the Frame. Once the following fragment has been executed, the original object has been rendered on the screen. See Figure 6 for the code fragment.
Figure 7 invokes the getPathIterator() method on the original object to get a PathIterator object that represents it.
Note that this program passes null to the getPathIterator() method. It is also possible to pass a reference to an AffineTransform object to the method. In that case, the PathIterator object will represent a transformed version of the target object with the nature of the transformation being determined by the AffineTransform object.
Since my objective here is to reproduce the original object with an offset in both the horizontal and vertical directions, I could have passed a translation transform object as a parameter. Then the PathIterator that is returned would represent the original object translated to a different location in the 2D space. However, I elected to perform the translation numerically when populating a new GeneralPath object instead. Therefore, I passed null as a parameter to getPathIterator().
The PathIterator object that is returned could contain segments consisting of Bezier curves. That may not be what you want. Another overloaded version of getPathIterator(AffineTransform at, double flatness) takes two parameters and returns a PathIterator object in which all Bezier curves have been replaced by a series of straight-line segments.
The flatness parameter defines how well the straight lines represent the curve. In particular, this parameter defines the maximum distance that any point on the curve can deviate from the straight line that represents the curve at that point. Thus, in general, the smaller the flatness parameter, the more straight line segments will be generated to represent the curve, and the better will be the straight line approximation of the curve.
Later on, I am going to need some place to store two kinds of information about the segments that represent the GeneralPath object. I will need to store the type of segment, which is of type int. I will also need to store coordinate information about the segment.
Figure 8 declares two local variables that will be used later to store this information.
I will be invoking the currentSegment() method on the PathIterator object to get information about the segment. This method returns the type, which I will store in theType.
I will pass the array named theData to the currentSegment() method. The method will populate the elements in the array with the coordinate information describing the segment. For the case where the segment can be a Bezier curve, the size of this array needs to be six elements, as described in the following information from Sun regarding the currentSegment() method (note that there is also a version of this method that deals in coordinate data of type double).
“Returns the coordinates and type of the current path segment in the iteration. The return value is the path segment type: SEG_MOVETO, SEG_LINETO, SEG_QUADTO, SEG_CUBICTO, or SEG_CLOSE.
A float array of length 6 must be passed in and can be used to store the coordinates of the point(s). Each point is stored as a pair of float x, y coordinates.
SEG_MOVETO and SEG_LINETO types returns one point, SEG_QUADTO returns two points, SEG_CUBICTO returns 3 points and SEG_CLOSE does not return any points.” |
Since I was the one who created the Shape object that this PathIterator object will represent, and since I know that it doesn’t contain any SEG_QUADTO or SEG_CUBICTO segments, I could have gotten by with a two-element array. However, to be more general, I used a six-element array.
My objective is to extract the necessary information from an existing Shape object to allow me to replicate that object with a one-inch offset in the horizontal and vertical dimensions. For that, I need a new GeneralPath object that I can populate with the information that I extract from the existing object.
Figure 9 instantiates, but does not populate a new GeneralPath object.
At the risk of causing total confusion, I am going to do something unusual here. In particular, I am going to show a code fragment that would ordinarily be a fairly long fragment, but I am going to delete some of code interior to the fragment to make it more manageable. I will explain the code that remains in the fragment after the deletion. Then I will come back in a subsequent fragment and explain the code that I deleted from the fragment.
You can refer to the original code, fully intact, in the complete listing of the program at the end of the lesson if this is confusing.
Figure 10 uses a while loop to iterate on, and extract information from the existing Shape object. This information is used to populate the new GeneralPath object inside a switch statement, which was deleted from the fragment for brevity.
The three key methods used in this fragment are:
The invocation of the isDone() method is used to provide the loop control parameter. This method tests to determine if the iteration is complete. It returns true if all the segments have been read and returns false otherwise.
Note that a not operator was used to cause the iteration to continue while the iteration is not done.
A description of the currentSegment() method was given above. Briefly, it uses a return value and an array parameter to return information about the current segment of the Shape object on which the iteration is being performed. This information is used to populate a segment in the new Shape object during each iteration.
According to Sun, the next() method
“Moves the iterator to the next segment of the path forwards along the primary direction of traversal as long as there are more points in that direction.”
All in all, this is a fairly standard iteration process, not unlike the use of the Enumeration interface that I discussed in detail in an earlier lesson. If enumeration, or iteration is new to you, you might want to go back and review the material in that lesson.
Now it’s time to go back and discuss the switch statement that was deleted from the previous fragment. Remember, this statement occurs inside the while loop of the previous fragment. In that fragment, the currentSegment() method is used to extract information from the original Shape object. That information is used in the following switch statement to populate the new object that was instantiated outside the while loop.
Figure 11 begins with a repeat of the invocation of the currentSegment() method from the previous fragment just to get you oriented. Then it picks up with the beginning of the switch statement.
The switch statement compares the segment type returned by the currentSegment() method against the five possible segment types. The statement uses the information returned in the array by the currentSegment() method to populate the new Shape object whenever a match is found.
The code also displays the type of segment on the command-line screen. I will show you the complete output on the command-line screen following the next segment.
The code in Figure 11 will be executed whenever the type of segment returned by the currentSegment() method is SEG_MOVETO. By this, I mean when the int value returned by the method matches the symbolic constant SEG_MOVETO defined in the PathIterator interface.
This code invokes the moveTo() method on the new GeneralPath object to store a segment of that type in the path. The coordinate values passed to the moveTo() method are the coordinate values extracted from the original Shape object with a one-inch offset in both the horizontal and vertical directions.
The code also displays the type of the segment on the command-line screen, but this is for information only, and is not critical to the process.
The execution of the switch statement inside the execution of the while loop produces the following output on the command line screen.
SEG_MOVETO
SEG_LINETO
SEG_LINETO
SEG_LINETO
SEG_CLOSE
You will note that, as expected, this is an exact match for the types of segments that were created when the original Shape object was created using the methods of the GeneralPath class. Thus, as mentioned earlier, a Shape object knows about its segment types and coordinate values. It can provide that information to us for whatever purpose we may need it.
Figure 12 does essentially the same thing as the previous fragment. This code is executed when there is a match for SEG_LINETO.
Since I was the person who created the original Shape object that is being replicated here, and since I knew that the object being replicated did not contain any Bezier curves, I didn’t provide the ability to support those segment types. However, I did include those types in the switch statement to make it general in nature, as shown in Figure 13.
Finally, Figure 14 creates a SEG_CLOSE segment in the new Shape object whenever a matching segment type is found in the object being replicated.
And that is the end of the switch statement.
Figure 15 sets the drawing color to red, and draws the new object on the Frame object. When you run this program, you should see the original Shape object appearing as a diamond, drawn in black, and centered on the origin. The new Shape object, which is an offset replica of the original object, is drawn in red, one inch down and one inch to the right of the original object. (Your output may not match the dimensions in inches, depending on actual screen resolution.)
That brings us to the end of this lesson. In this lesson, I have shown you how to use the GeneralPath class to create a new Shape object consisting of lines and spaces. I did not show you how to include Bezier curves in your new object. I plan to do that in a subsequent lesson.
The GeneralPath class was used in this lesson to support the primary objective of the lesson – learning how to get and use a PathIterator object that represents another object that implements the Shape interface.
In this lesson, I have shown you
Complete listings of both programs are provided in Figure 16 and Figure 17.
Copyright 2000, Richard G. Baldwin. Reproduction in whole or in part in any form or medium without express written permission from Richard Baldwin is prohibited.
About the author
Richard Baldwin is a college professor and private consultant whose primary focus is a combination of Java and XML. In addition to the many platform-independent benefits of Java applications, he believes that a combination of Java and XML will become the primary driving force in the delivery of structured information on the Web.
Richard has participated in numerous consulting projects involving Java, XML, or a combination of the two. He frequently provides onsite Java and/or XML training at the high-tech companies located in and around Austin, Texas. He is the author of Baldwin's Java Programming Tutorials, which has gained a worldwide following among experienced and aspiring Java programmers. He has also published articles on Java Programming in Java Pro magazine.
Richard holds an MSEE degree from Southern Methodist University and has many years of experience in the application of computer technology to real-world problems.
-end-