In this lesson we'll finish up drawing the tic marks and labels on our Cartesian plane graphic. We'll also have another look at encapsulation, specifically of line positioning and drawing. This will require implementation of the Iterator and Iterable interfaces, so we'll start with a discussion of those.
GitHub repository: Cartesian Plane Part 4
Previous lesson: Cartesian Plane Lesson 3: Encapsulation
Lesson 4: Tic Marks, Labels... and more
1. Interfaces
Let's have a quick review of interfaces. An interface is a type, much like a class. Interfaces contain descriptions of methods, but without the method implementations (traditionally, interfaces contain no implementation code at all; this is no longer true, but let's leave that discussion for another time). A class can implement one or more interfaces; if a class implements an interface it is required to implement the methods described by the interface (or declare itself abstract, which can defer method implementation to concrete subclasses). One of the most commonly implemented interfaces is the Runnable interface. If you look at the documentation for Runnable, you will see that it describes the run method, as follows:
void run()
The Root class that we have been using in our lessons is an example of a class that implements Runnable; in summary, it looks like this:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 | public class Root implements Runnable { // ... /** * Required by the Runnable interface. * This method is the place where the initial content * of the frame must be configured. */ public void run() { /* Instantiate the frame. */ frame = new JFrame( "Graphics Frame" ); // ... } } |
Interfaces can be parameterized. One of the most popular such interfaces is List<E>. List<E> is declared like this:
interface List<E> { ... }
And it describes methods declared this way:
void add(int index, E element);
E get(int index)
When you declare a variable of type List<E> you substitute a type for the E; you've probably already done something like this:
1 2 3 4 5 6 7 8 9 10 11 | public class ListDemo { public static void main( String[] args ) { List<String> list = new ArrayList<>(); list.add( "string 1" ); list.add( "string 2" ); String str = list.get( 1 ); System.out.println( str ); } } |
In your code, the type you declare inside the diamond operator (<String>) is substituted for E, and now any operations on your list are limited to use with Strings.
Parameterization can be an involved topic, but for the moment we've covered all we need to know for this project. To learn more about interfaces see the Oracle tutorial Interfaces and Inheritance. For more about parameterization, see the Oracle Tutorial Generics.
A bit more about Parameterization (more commonly referred to as Generics)
You cannot parameterize on a primitive type, only on a class or an interface. You cannot have a List of ints or booleans. Recall, however, that every primitive type has a corresponding wrapper class, so you can have a List of type Integer or Boolean. This may be confusing if you've ever seen code that looks like this:
1 2 3 4 5 6 7 8 9 10 | public class AutoboxingDemo { public static void main(String[] args) { List<Integer> iList = new ArrayList<>(); for ( int inx = 0 ; inx < 10 ; ++inx ) iList.add( inx ); iList.set( 5, 42 ); } } |
To clear up the confusion, the code at lines 7 and 8 are not adding primitive values to iList; instead the compiler is automatically converting the int primitive to an instance of its wrapper class Integer in an operation called autoboxing. So the above code is equivalent to this:
for ( int inx = 0 ; inx < 10 ; ++inx )
iList.add( Integer.valueOf( inx ) );
iList.set( 5, Integer.valueOf( 42 ) );
2. Iterators and Iterables
In Java, an iterator is something that produces a sequence of values. The interface Iterator<E> describes a type that produces a sequence of values of type E. If you look at the documentation of interface Iterator<E> you'll see that implementing it requires you to write two methods:
boolean hasNext()
E next()
The method next() returns the next value in the sequence (assuming there is one). The method hasNext() returns true if the iterator has a next element to return. If hasNext() is false, calling next() throws a NoSuchElementException. A common example of an iterator is ListIterator<E>. If you have a variable of type List<String> invoking method listIterator() returns an object of type ListIterator<String> that can be used to traverse the contents of the list. Here's an example.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 | public class listIteratorDemo { public static void main(String[] args) { List<String> list = new ArrayList<>(); list.add( "every" ); list.add( "good" ); list.add( "boy" ); list.add( "deserves" ); list.add( "favor" ); ListIterator<String> iter = list.listIterator(); while ( iter.hasNext() ) { String str = iter.next(); System.out.println( str ); } } } |
Writing your own iterator isn't difficult. You just need an object that stores enough state to know the next element to produce and when the sequence has been exhausted. Let's write the class IntIterator, which implements Iterator<Integer> and iterates over a range of integers. We'll have a constructor that describes the target range, and instance variables that describe the next integer to produce and the upper bound of the range.
private final int upperBound;
private int next;
public IntIterator( int lowerBound, int upperBound )
{
this.upperBound = upperBound;
next = lowerBound;
}
Note: you may ask, "why did you make upperBound final"? The answer is because I could. Declaring variables to be final (meaning they can't be changed after initialization) makes your code more reliable, and easier to maintain.
Now we just need a hasNext() method that compares next to upperBound, and a next() method that correctly manipulates the value of next. Here's the complete class.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 | public class IntIterator implements Iterator<Integer> { private final int upperBound; private int next; // Iterates over the sequence num, //where lowerBound <= num < upperBound public IntIterator( int lowerBound, int upperBound ) { this.upperBound = upperBound; next = lowerBound; } @Override public boolean hasNext() { boolean hasNext = next < upperBound; return hasNext; } @Override public Integer next() { if ( next >= upperBound ) throw new NoSuchElementException( "iterator exhausted" ); int nextInt = next++; return nextInt; } } |
A class that implements Iterable<E> simply has a method iterator() that returns an object of type Iterator<E>. The List<E> interface has such a method (allowing us to say that a List<E> is iterable). The nice thing about iterable objects is that they can be used in an enhanced for statement (a.k.a. for-each loop). Here's an example:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 | public class ForEachDemo { public static void main( String[] args ) { List<String> list = new ArrayList<>(); list.add( "every" ); list.add( "good" ); list.add( "boy" ); list.add( "deserves" ); list.add( "favor" ); for ( String str : list ) System.out.println( str ); } } |
For more about the enhanced for statement, see The for Statement, in the Oracle Java tutorial.
If we want a class that produces objects that can iterate over a range of integers inum1 through inumN, all we need to do is implement the method Iterator<Integer> iterator() that returns new IntIterator(inum1, unumN). Let's write such a class:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 | public class Range implements Iterable<Integer> { private final int lowerBound; private final int upperBound; // Iterable over num, // where lowerBound <= num < upperBound public Range( int lowerBound, int upperBound ) { this.lowerBound = lowerBound; this.upperBound = upperBound; } public Iterator<Integer> iterator() { IntIterator iter = new IntIterator( upperBound, lowerBound ); return iter; } } |
And here's an example that uses our Range class.
1 2 3 4 5 6 7 8 9 | public class RangeDemo { public static void main(String[] args) { Range range = new Range( -10, 10 ); for ( int num : range ) System.out.println( num ); } } |
A quick look at inner classes
Another strategy for creating iterable classes is to declare the iterator class directly inside the class that uses it. A class declared directly inside another class is called a nested class. The class may be declared static; if it is not static it is an inner class. The advantage of inner classes is that they have access to all the instance variables and instance methods inside the class that contains them (the outer class). Here's a trivial example.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 | public class InnerClassDemo { private double datum; private DatumRoot fourthRoot; public static void main( String[] args ) { InnerClassDemo demo = new InnerClassDemo( 16 ); System.out.println( demo.fourthRoot.getRoot() ); } public InnerClassDemo( double datum ) { this.datum = datum; fourthRoot = new DatumRoot( 4 ); } private void printMessage( String msg ) { System.out.println( "Message: " + msg ); } private class DatumRoot { private double radicand; private DatumRoot( double radicand ) { this.radicand = radicand; } private double getRoot() { printMessage( "calculating root" ); double root = Math.pow( datum, 1 / radicand ); return root; } } } |
The above code serves no purpose other than to demonstrate that an instance of the inner class (DatumRoot) can access the instance variable (datum) and instance method (printMessage) of the outer class (InnerClassDemo).
We can apply this strategy to, for example, the Range class. If we declare the iterator to be an inner class in Range we have a) a Range class with no external dependencies on other classes; and b) a simplified iterator which doesn't have to store lowerBound and upperBound in its own state; instead it obtains these values from the outer class. Here is the code for the enhanced Range class.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 | public class RangeWithInnerClass implements Iterable<Integer> { private final int lowerBound; private final int upperBound; // Iterable over num, // where lowerBound <= num < upperBound public RangeWithInnerClass( int lowerBound, int upperBound ) { this.lowerBound = lowerBound; this.upperBound = upperBound; } public Iterator<Integer> iterator() { InnerIntIterator iter = new InnerIntIterator(); return iter; } private class InnerIntIterator implements Iterator<Integer> { private int next = lowerBound; @Override public boolean hasNext() { boolean hasNext = next < upperBound; return hasNext; } @Override public Integer next() { if ( next >= upperBound ) throw new NoSuchElementException( "iterator exhausted" ); int nextInt = next++; return nextInt; } } } |
For more about inner classes see Nested Classes in the Oracle Java Tutorial.
3. Encapsulating Line Drawing
So why encapsulate the line drawing? For one thing it applies to so many different pieces of the Cartesian Plane drawing logic:
- The grid lines.
- The axes.
- The minor tic marks.
- The major tic marks.
- Even the labels, which have to be horizontally centered on the vertical line drawn through a major tic mark on the x-axis, and vertically centered on the horizontal line drawn through a major tic mark on the y-axis.
(Other reasons: it's a great exercise in encapsulation, and in writing iterators, iterables and inner classes.)
The LineGenerator Class
Let's have a separate class that is able to describe the coordinates of the necessary horizontal and vertical lines of our grid. It will be type Iterable<Line2D>, capable of iterating through horizontal lines, vertical lines or both. We'll begin by declaring constant variables that the user can use to tell us which lines to iterate over.
1 2 3 4 5 | public class LineGenerator implements Iterable<Line2D> { public static final int HORIZONTAL = 1; public static final int VERTICAL = 2; public static final int BOTH = 3;; |
For its state we'll need to keep track of various properties of the rectangle that bounds the grid, the number of pixels per unit, and the lines per unit. It will need to know the length of a line, and the total number of horizontal and vertical lines to draw. Here are the declarations of our state variables.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 | private final float gridWidth; // width of the rectangle private final float gridHeight; // height of the rectangle private final float centerXco; // center x-coordinate private final float maxXco; // right-most x-coordinate private final float centerYco; // center y-coordinate private final float maxYco; // bottom-most y-coordinate private final float gridUnit; // pixels per unit private final float lpu; // lines per unit private final float length; // the length of a line private final int orientation; // HORIZONTAL, VERTICAL or BOTH private final float gridSpacing; // pixels between lines private final float totalHorLines; // total number of horizontal lines private final float totalVerLines; // total number of vertical lines |
Our primary constructor will set all the above properties. It looks like this:
public LineGenerator(
Rectangle2D rect,
float gridUnit,
float lpu,
float length,
int orientation
)
The rectangle will describe the bounds of the grid. It will supply the width, height and necessary x- and y-coordinates. The length will describe the length of the line to be calculated or -1, in which case lines will be calculated to span the width or height of the grid. Let's also have a convenience constructor which allows the length and orientation to default (to -1 and BOTH, respectively). As we did with the CartesianPlane constructors, we'll use chaining for the constructor implementations. Here are our constructors.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 | public LineGenerator( Rectangle2D rect, float gridUnit, float lpu ) { this( rect, gridUnit, lpu, -1, BOTH ); } public LineGenerator( Rectangle2D rect, float gridUnit, float lpu, float length, int orientation ) { gridWidth = (float)rect.getWidth(); gridHeight = (float)rect.getHeight(); centerXco = (float)rect.getCenterX(); maxXco = (float)rect.getMaxX(); centerYco = (float)rect.getCenterY(); maxYco = (float)rect.getMaxY(); this.gridUnit = gridUnit; this.lpu = lpu; this.length = length; this.orientation = orientation; gridSpacing = gridUnit / lpu; totalVerLines = (float)Math.floor( gridWidth / gridSpacing ); totalHorLines = (float)Math.floor( gridHeight / gridSpacing ); } |
In addition to the iterator method required by the Iterable interface, we'll have getters for the total-vertical-lines and total-horizontal-lines properties. (These are primarily for support of the label drawing logic.)
1 2 3 4 5 6 7 8 9 | public float getTotalHorizontalLines() { return totalHorLines; } public float getTotalVerticalLines() { return totalVerLines; } |
We'll have three inner classes, one for iterating over horizontal lines, one for vertical lines and one for both. Here is the inner class used for iterating over vertical lines (note that a design requirement for the iterator is that it will generate lines sequentially, from top to bottom; this is primarily for support of the label drawing logic).
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 | // produces a sequence of vertical lines from left to right. private class Line2DVerticalIterator implements Iterator<Line2D> { private final float yco1; private final float yco2; private float xco; public Line2DVerticalIterator() { // Calculate the number of lines drawn left of the origin. float numLeft = (float)Math.floor( totalVerLines / 2 ); // The actual length is the length passed by the user, or, // if the user passed a negative value, the height of the grid. float actLength = length >= 0 ? length : gridHeight; // Calculate the top (yco1) and bottom (yco2) of the line yco1 = centerYco - actLength / 2; yco2 = yco1 + actLength; // Calculate the x-coordinate of the leftmost line. The // x-coordinate is the same for both endpoints of a line. // After generating a line, the x-coordinate is incremented // to the next line. xco = centerXco - numLeft * gridSpacing; } @Override // This method required by "implements Iterator<Line2D>" public boolean hasNext() { // The iterator is exhausted when the x-coordinate // exceeds the bounds of the grid. boolean hasNext = xco < maxXco; return hasNext; } @Override // This method required by "implements Iterator<Line2D>" public Line2D next() { // Throw an exception if there is no next line. if ( xco > maxXco ) { String msg = "Grid bounds exceeded at x = " + xco; throw new NoSuchElementException( msg ); } Line2D line = new Line2D.Float( xco, yco1, xco, yco2 ); xco += gridSpacing; return line; } } |
The horizontal line iterator is very similar to the vertical line iterator.
Note: it's a good exercise for students to attempt to write the code for the horizontal line iterator before examining the solution, below.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 | // Produces a sequence of horizontal lines from top to bottom private class Line2DHorizontalIterator implements Iterator<Line2D> { private final float gridSpacing; private final float xco1; private final float xco2; private float yco; public Line2DHorizontalIterator() { float actLength = length >= 0 ? length : gridWidth; float numTop = (float)Math.floor( totalHorLines / 2 ); gridSpacing = gridUnit / lpu; xco1 = centerXco - actLength / 2; xco2 = xco1 + actLength; yco = centerYco - numTop * gridSpacing; } @Override public boolean hasNext() { boolean hasNext = yco < maxYco; return hasNext; } @Override public Line2D next() { Line2D line = new Line2D.Float( xco1, yco, xco2, yco ); yco += gridSpacing; return line; } } |
The third inner class, the one that generates both vertical and horizontal lines, is a simple composite of the two iterators, above.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 | private class Line2DIterator implements Iterator<Line2D> { private final Iterator<Line2D> horizontalIter = new Line2DHorizontalIterator(); private final Iterator<Line2D> verticalIter = new Line2DVerticalIterator(); @Override public boolean hasNext() { boolean hasNext = horizontalIter.hasNext() | verticalIter.hasNext(); return hasNext; } @Override public Line2D next() { Line2D next = horizontalIter.hasNext() ? horizontalIter.next() : verticalIter.next(); return next; } } |
To complete the LineGenerator class we have to add the iterator method required by "implements Iterable<Line2D>."
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 | @Override public Iterator<Line2D> iterator() { Iterator<Line2D> iter = null; switch ( orientation ) { case HORIZONTAL: iter = new Line2DHorizontalIterator(); break; case VERTICAL: iter = new Line2DVerticalIterator(); break; default: iter = new Line2DIterator(); } return iter; } |
4. Incorporating LineGenerator into CartesianPlane
The first thing we can do in CartesianPlane is revise our instance variable declarations. We no longer need the variables for grid width/height, or min-/max-/center-coordinates. We do, however, need a rectangle that encapsulates those properties. We also need to add instance variables that we'll need to draw the labels on the x- and y-axes, a Font and a FontRenderContext (we'll talk about these soon). That section of the class declaration now looks like this:
1 2 3 4 5 6 7 8 9 10 11 12 | /////////////////////////////////////////////////////// // // The following values are recalculated every time // paintComponent is invoked. // /////////////////////////////////////////////////////// private int currWidth; private int currHeight; private Graphics2D gtx; private Rectangle2D gridRect; private Font labelFont; private FontRenderContext labelFRC; |
Next we can rewrite the logic to draw the grid lines and axes. The new code is shown below. Note that I have taken the liberty of renaming drawGrid to drawGridLines. Also, in drawGridLines I have added the logic to conditionally draw the grid lines based on the property gridLineDraw.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 | private void drawAxes() { gtx.setColor( axisColor ); gtx.setStroke( new BasicStroke( axisWeight ) ); // Set the gridUnit to the width of the grid... // ... set the LPU to 1... // ... LineGenerator will iterate lines only for the axes. float gridUnit = (float)gridRect.getWidth(); LineGenerator lineGen = new LineGenerator( gridRect, gridUnit, 1 ); for ( Line2D line : lineGen ) gtx.draw( line ); } private void drawGridLines() { if ( gridLineDraw ) { LineGenerator lineGen = new LineGenerator( gridRect, gridUnit, gridLineLPU ); gtx.setStroke( new BasicStroke( gridLineWeight ) ); gtx.setColor( gridLineColor ); for ( Line2D line : lineGen ) gtx.draw( line ); } } |
Now that we have LineGenerator, adding the logic to draw the tic marks is straightforward.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 | private void drawMinorTics() { if ( ticMinorDraw ) { LineGenerator lineGen = new LineGenerator( gridRect, gridUnit, ticMinorMPU, ticMinorLen, LineGenerator.BOTH ); gtx.setStroke( new BasicStroke( ticMinorWeight ) ); gtx.setColor( ticMinorColor ); for ( Line2D line : lineGen ) gtx.draw( line ); } } private void drawMajorTics() { if ( ticMajorDraw ) { LineGenerator lineGen = new LineGenerator( gridRect, gridUnit, ticMajorMPU, ticMajorLen, LineGenerator.BOTH ); gtx.setStroke( new BasicStroke( ticMajorWeight ) ); gtx.setColor( ticMajorColor ); for ( Line2D line : lineGen ) gtx.draw( line ); } } |
The next task will be to draw the labels. We'll use two helper methods for this: drawHorizontalLabels and drawVerticalLabels. Before we get to those, however, let's look at the final version (for this lesson) of paintComponent.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 | public void paintComponent( Graphics graphics ) { // begin boilerplate super.paintComponent( graphics ); currWidth = getWidth(); currHeight = getHeight(); gtx = (Graphics2D)graphics.create(); gtx.setColor( mwBGColor ); gtx.fillRect( 0, 0, currWidth, currHeight ); // end boilerplate // set up the label font // round font size to nearest int int fontSize = (int)(labelFontSize + .5); labelFont = new Font( labelFontName, labelFontStyle, fontSize ); gtx.setFont( labelFont ); labelFRC = gtx.getFontRenderContext(); // Describe the rectangle containing the grid float gridWidth = currWidth - marginLeftWidth - marginRightWidth; float minXco = marginLeftWidth; float gridHeight = currHeight - marginTopWidth - marginBottomWidth; float minYco = marginTopWidth; gridRect = new Rectangle2D.Float( minXco, minYco, gridWidth, gridHeight ); drawGridLines(); drawMinorTics(); drawMajorTics(); drawAxes(); drawHorizontalLabels(); drawVerticalLabels(); paintMargins(); // begin boilerplate gtx.dispose(); // end boilerplate } |
5. Drawing the Labels
The TextLayout Facility
To draw/measure text using the TextLayout facility, you need a Font, a Graphics2D and a FontRenderContext. The Font and the FontRenderContext come from the Grahics2D. Once you have these three objects you can get a TextLayout object. Then you use the TextLayout object to obtain the bounding box (TextLayout.getBounds()), and draw the text (TextLayout.draw(Graphics2D g, float x, float y)). The x and y arguments passed to the draw method are the coordinates of the baseline of the text (for more about the baseline see the Java Graphics Tools lesson). This sample code below was used to draw the figure at the left.1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 | public void paintComponent( Graphics graphics ) { //////////////////////////////////// // Begin boilerplate //////////////////////////////////// super.paintComponent( graphics ); gtx = (Graphics2D)graphics.create(); currWidth = getWidth(); currHeight = getHeight(); gtx.setColor( bgColor ); gtx.fillRect( 0, 0, currWidth, currHeight ); //////////////////////////////////// // End boiler plate //////////////////////////////////// gtx.setColor( Color.BLACK ); Font font = new Font( Font.MONOSPACED, Font.PLAIN, 25 ); gtx.setFont( font ); FontRenderContext frc = gtx.getFontRenderContext(); String label = "3.14159"; TextLayout layout = new TextLayout( label, font, frc ); Rectangle2D bounds = layout.getBounds(); float textXco = 50F; float textYco = 50F; layout.draw( gtx, textXco, textYco ); Rectangle2D rect = new Rectangle2D.Float( textXco + (float)bounds.getX(), textYco + (float)bounds.getY(), (float)bounds.getWidth(), (float)bounds.getHeight() ); gtx.draw( rect ); //////////////////////////////////// // Begin boilerplate //////////////////////////////////// gtx.dispose(); //////////////////////////////////// // End boiler plate //////////////////////////////////// } |
(About the above code: the x- and y- coordinates of the rectangle returned by getBounds are offsets from the baseline to the upper-left corner of the rectangle. The x value is typically negative and the y value is typically 0. We won't be using these values in our code.)
Testing Floating Point Values for Equality
One more aside before we write the code to draw the labels. As part of this process we will omit drawing the labels at the origin. That entails testing the x- or y-coordinate for equality with 0. As you may know, testing floating point values for equality can be tricky because of rounding errors. If you are unaware of that, try running this bit of code:
1 2 3 4 5 6 7 8 9 | public class RoundingErrorDemo { public static void main(String[] args) { double dVar1 = .7 + .1; double dVar2 = .9 - .1; System.out.println( dVar1 == dVar2 ); } } |
To circumvent this problem we can use the epsilon test for equality (sometimes also called the delta test for equality). Choose a very small value for epsilon, say .00001. Now take the difference between two values. If the difference is less than epsilon we can treat the two values as though they are equal. CartesianPlane has a helper method to encapsulate this technique.
1 2 3 4 5 6 7 | private static boolean equal( float fVal1, float fVal2 ) { final float epsilon = .0001f; float diff = Math.abs( fVal1 - fVal2 ); boolean equal = diff < epsilon; return equal; } |
Note: if you're a devotee of numerical analysis you may object to calling this method equal. If so, feel free to rename it "closeEnoughForJazz."
Labels on the Y Axis
We'll use LineGenerator to return a sequence of lines corresponding to the major tic marks. Recall that part of the design of the iterator is to return lines in order, from top to bottom. So we need to figure out what will be the label on the first line returned, and what will be the increment when traversing to the next label. The increment will be labelIncr = 1/ticMajorMPU (the major tic marks-per-unit). The number of lines above the x-axis is found using numAbove = LineGenerator.getTotalHorizontalLines() / 2, so the value of the topmost label will be numAbove * labelIncr. Including a constant to define the margin between the end of a tic mark and its label, the first part of the task looks like this:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 | /** * Draw the labels on the horizontal tic marks * (top to bottom of y-axis). */ private void drawHorizontalLabels() { // padding between tic mark and label final int labelPadding = 3; LineGenerator lineGen = new LineGenerator( gridRect, gridUnit, ticMajorMPU, ticMajorLen, LineGenerator.HORIZONTAL ); int numAbove = (int)(lineGen.getTotalHorizontalLines() / 2); float labelIncr = 1 / ticMajorMPU; float nextLabel = numAbove * labelIncr; for ( Line2D line : lineGen ) { // ... } } |
Inside the loop, for each line we have to calculate the x- and y-coordinates of the baseline of the label. The x-coordinate is the x-coordinate of the right end of the tic mark + the label margin. The y-coordinate is the y-coordinate of the tic mark + half the height of the label's bounding rectangle. To get the bounding rectangle and draw the label we will need the current font and the FontRenderContext; recall that these are obtained each time the paintComponent method is invoked:
int fontSize = (int)(labelFontSize + .5);
labelFont = new Font( labelFontName, labelFontStyle, fontSize );
gtx.setFont( labelFont );
labelFRC = gtx.getFontRenderContext();
At the bottom of the loop we calculate the value of the next label to draw. Here's the loop in its entirety. Note that we don't draw a label at the origin.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 | for ( Line2D line : lineGen ) { // Don't draw a label at the origin if ( !equal( nextLabel, 0 ) ) { String label = String.format( "%3.2f", nextLabel ); TextLayout layout = new TextLayout( label, labelFont, labelFRC ); Rectangle2D bounds = layout.getBounds(); float yOffset = (float)(bounds.getHeight() / 2); float xco = (float)line.getX2() + labelPadding; float yco = (float)line.getY2() + yOffset; layout.draw( gtx, xco, yco ); } nextLabel -= labelIncr; } |
Labels on the X Axis
Drawing the labels on the vertical tic marks (on the x-axis) is similar. The main difference is that the y-coordinate label's baseline will be the y-coordinate of the tic mark + the height of the label's bounding rectangle + a margin. The x-coordinate is the x-coordinate of the tic mark less half the width of the bounding rectangle. The code follows, below.
Note: the student is encouraged to attempt to write this method before looking at the solution provided.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 | /** * Draw the labels on the vertical tic marks * (left to right on x-axis). */ private void drawVerticalLabels() { // padding between tic mark and label final int labelPadding = 3; LineGenerator lineGen = new LineGenerator( gridRect, gridUnit, ticMajorMPU, ticMajorLen, LineGenerator.VERTICAL ); int numLeft = (int)(lineGen.getTotalVerticalLines() / 2); float labelIncr = 1 / ticMajorMPU; float nextLabel = -numLeft * labelIncr; for ( Line2D line : lineGen ) { // Don't draw a label at the origin if ( !equal( nextLabel, 0 ) ) { String label = String.format( "%3.2f", nextLabel ); TextLayout layout = new TextLayout( label, labelFont, labelFRC ); Rectangle2D bounds = layout.getBounds(); float yOffset = (float)(bounds.getHeight() + labelPadding); float xOffset = (float)(bounds.getWidth() / 2); float xco = (float)line.getX2() - xOffset; float yco = (float)line.getY2() + yOffset; layout.draw( gtx, xco, yco ); } nextLabel += labelIncr; } } |
Short of plotting a curve, that's the code for displaying our Cartesian Plane. In our lessons we have quite a bit to do before we get to plotting, including documentation, testing and property management. I understand, however, if you're chomping at the bit, and are ready for some visual results. If so I encourage you to add some simple plotting code to CartesianPlane, and a main method that plots a simple curve. If you look in the GitHub repository you'll see that I have done so in classes CartesianPlaneTemp and MainTemp.
No comments:
Post a Comment