Solution for Programmming Exercise 8.5

Exercise 8.5:

This exercise uses the
class Expr, which was described in
Exercise 8.4 and which is defined in the source code
file Expr.java. For this exercise, you
should write a GUI program that can graph a function, f(x), whose
definition is entered by the user. The program should have a text-input box
where the user can enter an expression involving the variable x, such
as x^2 or sin(x-3)/x. This expression is the definition of
the function. When the user presses return in the text input box, the program
should use the contents of the text input box to construct an object of type
Expr. If an error is found in the definition, then the program should
display an error message. Otherwise, it should display a graph of the function.
(Note: A JTextField generates an ActionEvent when the user
presses return.)

The program will need a JPanel for displaying the graph. To keep
things simple, this panel should represent a fixed region in the xy-plane,
defined by -5 <= x <= 5 and
-5 <= y <= 5. To draw the graph, compute a
large number of points and connect them with line segments. (This method does
not handle discontinuous functions properly; doing so is very hard, so you
shouldn't try to do it for this exercise.) My program divides the interval
-5 <= x <= 5 into 300 subintervals and uses
the 301 endpoints of these subintervals for drawing the graph. Note that the
function might be undefined at one of these x-values. In that case,
you have to skip that point.

A point on the graph has the form (x,y) where y is
obtained by evaluating the user's expression at the given value of x.
You will have to convert these real numbers to the integer coordinates of the
corresponding pixel on the canvas. The formulas for the conversion are:

a = (int)( (x + 5)/10 * width );
b = (int)( (5 - y)/10 * height );

where a and b are the horizontal and vertical coordinates
of the pixel, and width and height are the width and height
of the panel.

Here is an applet version of my solution to this exercise:

Discussion

I wrote my solution as a subclass, SimpleGrapher,
of JPanel that represents
the main panel of the program. The class includes a main() routine
that makes it possible to run the class as a stand-alone application; main()
just opens a window and sets its content pane to be a SimpleGrapher.
To make it possible to run the program as an applet, I added a static nested class
of JApplet to SimpleGrapher, but
this is not required by the exercise.

My solution uses a nested class, GraphPanel, for displaying the graph.
This class is defined as a subclass of JPanel. It has an instance
variable, func, of type Expr that represents the function to
be drawn. If there is no function, then func is null. This is true
when the applet is first started and when the user's input has been found to be
illegal. The paintComponent() method checks the value of func
to decide what to draw. If func is null, then the paint
method simply draws a message on the panel stating that no function is
available. Otherwise, it draws a pair of axes and the graph of the
function.

The interesting work in class GraphPanel is done in the
drawFunction() method, which is called by the
paintComponent() method. This function draws the graph of the function
for -5 <= x <= 5. This interval on the x axis is
divided into 300 subintervals. Since the length of the interval is 10, the
length of each subinterval is given by dx, where dx is
10.0/300. The x values for the points that I want to plot are
given by -5, -5+dx, -5+2*dx, and so on. Each
x-value is obtained by adding dx to the previous value. For
each x value, the y-value of the point on the graph is
computed as func.value(x). As the points on the graph are computed,
line segments are drawn to connect pairs of points (unless the y-value
of either point is undefined). An algorithm for the drawFunction()
method is:

The method for drawing the line segment uses the conversion from real
coordinates to integer pixel coordinates that is given in the exercise. By the
way, more general conversion formulas can be given in the case where x
extends from xmin to xmax and y extends from
ymin to ymax. The general formulas are:

The formulas for a and b are of slightly different form to
reflect the fact that a increases from 0 to width as
x increases from xmin to xmax, while bdecreases from height to 0 as y increases
from ymin to ymax. You could improve the applet by adding
text input boxes where the user can enter values for xmin,
xmax, ymin, and ymax.

In the main applet class, the init() method lays out the components
in the content pane of the applet with a BorderLayout that has a
vertical gap, to allow some space between the graph and the components that are
above it and below it. The "North" component is a JLabel that is used
to display messages to the user. The "South" component holds
a JTextField
where the user enters the definition of the function. Since I wanted to add a
label, "f(x) = ", next to the text field, I created a
sub-panel to hold both the label and the text field, and I put the sub-panel
in the "South" position of the main panel. Finally, the "Center"
component of the main panel is the GraphPanel
where the graph is drawn. A listener for ActionEvents
is registered with the JTextField.
When the user presses return in the JTextField, the listener's
actionPerformed() method will be called.

The actionPerformed() method just has to get the user's input
string from the JTextField. It tries to use this string to construct
an object of type Expr. The constructor throws an
IllegalArgumentException if the string contains an error, so the
constructor is called in a try statement that can catch and handle the
error. If an error occurs, then the error message in the exception object is
displayed in the JLabel at the top of the applet, and the graph is
cleared. If no error occurs, the graph is set to display the user's function,
and the JLabel is set to display the generic message, "Enter a
function and press return." The code for all this is:

(After viewing my applet for the first time, I was dissatisfied with the
appearance of the label at the top of the applet.
There was no space between the text of the label and
the gray background of the component. I decided to fix this by adding an
EmptyBorder to the label to allow more space around the text where the
white background of the label shows through. I also added borders around the main
panel and around the subpanel that contains the text field. Borders were covered in
Subsection 6.7.2)

The Solution

import java.awt.*;
import java.awt.event.*;
import javax.swing.*;
/*
The SimpleGrapher program can draw graphs of functions input by the
user. The user enters the definition of the function in a text
input box. When the user presses return, the function is graphed.
(Unless the definition contains an error. In that case, an error
message is displayed.)
The graph is drawn on a canvas which represents the region of the
(x,y)-plane given by -5 <= x <= 5 and -5 <= y <= 5. Any part of
the graph that lies outside this region is not shown. The graph
is drawn by plotting 301 points and joining them with lines. This
does not handle discontinuous functions properly.
This program requires the class Expr,
which is defined in by a separate file, Expr.java.
That file contains a full description of the syntax
of legal function definitions.
This class can be run as a main program. It has a static subclass,
SimpleGrapherApplet, which can be used to run the same program as an applet.
*/
public class SimpleGrapher extends JPanel {
//-- Support for running this class as a stand-alone application and as an applet --
public static void main(String[] args) {
// Open a window that shows a SimpleGrapher panel.
JFrame window = new JFrame("SimpleGrapher");
window.setContentPane( new SimpleGrapher() );
window.setLocation(50,50);
window.setSize(500,540);
window.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
window.setVisible(true);
}
public static class SimpleGrapherApplet extends JApplet {
public void init() {
setContentPane( new SimpleGrapher() );
}
}
//---------------------------------------------------------------------------------
private GraphPanel graph; // The JPanel that will display the graph.
// The nested class, Graph, is defined below.
private JTextField functionInput; // A text input box where the user enters
// the definition of the function.
private JLabel message; // A label for displaying messages to the user,
// including error messages when the function
// definition is illegal.
public SimpleGrapher() {
// Initialize the panel by creating and laying out the components
// and setting up an action listener for the text field.
setBackground(Color.GRAY);
setLayout(new BorderLayout(3,3));
setBorder(BorderFactory.createLineBorder(Color.GRAY,3));
graph = new GraphPanel();
add(graph, BorderLayout.CENTER);
message = new JLabel(" Enter a function and press return");
message.setBackground(Color.WHITE);
message.setForeground(Color.RED);
message.setOpaque(true);
message.setBorder( BorderFactory.createEmptyBorder(5,0,5,0) );
add(message, BorderLayout.NORTH);
functionInput = new JTextField();
JPanel subpanel = new JPanel();
subpanel.setLayout(new BorderLayout());
subpanel.setBorder(BorderFactory.createEmptyBorder(3,3,3,3));
subpanel.add(new JLabel("f(x) = "), BorderLayout.WEST);
subpanel.add(functionInput, BorderLayout.CENTER);
add(subpanel, BorderLayout.SOUTH);
functionInput.addActionListener(new ActionListener() {
public void actionPerformed(ActionEvent evt) {
// Get the user's function definition from the box and use it
// to create a new object of type Expr. Tell the GraphPanel to
// graph this function. If the definition is illegal, an
// IllegalArgumentException is thrown by the Expr constructor.
// If this happens, the graph is cleared and an error message
// is displayed in the message label.
Expr function; // The user's function.
try {
String def = functionInput.getText();
function = new Expr(def);
graph.setFunction(function);
message.setText(" Enter a function and press return.");
}
catch (IllegalArgumentException e) {
graph.clearFunction();
message.setText(e.getMessage());
}
functionInput.selectAll();
functionInput.requestFocus(); // Let's user start typing in input box.
}
});
} // end constructor
// -------------------------- Nested class ----------------------------
private static class GraphPanel extends JPanel {
// A object of this class can display the graph of a function
// on the region of the (x,y)-plane given by -5 &lt;= x &lt;= 5 and
// -5 &lt;= y &lt;= 5. The graph is drawn very simply, by plotting
// 301 points and connecting them with line segments.
Expr func; // The definition of the function that is to be graphed.
// If the value is null, no graph is drawn.
GraphPanel() {
// Constructor.
setBackground(Color.WHITE);
func = null;
}
public void setFunction(Expr exp) {
// Set the canvas to graph the function whose definition is
// given by the function exp.
func = exp;
repaint();
}
public void clearFunction() {
// Set the canvas to draw no graph at all.
func = null;
repaint();
}
public void paintComponent(Graphics g) {
// Draw the graph of the function or, if func is null,
// display a message that there is no function to be graphed.
super.paintComponent(g); // Fill with background color, white.
if (func == null) {
g.drawString("No function is available.", 20, 30);
}
else {
g.drawString("y = " + func, 5, 15);
drawAxes(g);
drawFunction(g);
}
}
void drawAxes(Graphics g) {
// Draw horizontal and vertical axes in the middle of the
// canvas. A 5-pixel border is left at the ends of the axes.
int width = getWidth();
int height = getHeight();
g.setColor(Color.BLUE);
g.drawLine(5, height/2, width-5, height/2);
g.drawLine(width/2, 5, width/2, height-5);
}
void drawFunction(Graphics g) {
// Draw the graph of the function defined by the instance
// variable func. Just plot 301 points with lines
// between them.
double x, y; // A point on the graph. y is f(x).
double prevx, prevy; // The previous point on the graph.
double dx; // Difference between the x-values of consecutive
// points on the graph.
dx = 10.0 / 300;
g.setColor(Color.RED);
/* Compute the first point. */
x = -5;
y = func.value(x);
/* Compute each of the other 300 points, and draw a line segment
between each consecutive pair of points. Note that if the
function is undefined at one of the points in a pair, then
the line segment is not drawn. */
for (int i = 1; i <= 300; i++) {
prevx = x; // Save the coords of the previous point.
prevy = y;
x += dx; // Get the coords of the next point.
y = func.value(x);
if ( (! Double.isNaN(y)) && (! Double.isNaN(prevy)) ) {
// Draw a line segment between the two points.
putLine(g, prevx, prevy, x, y);
}
} // end for
} // end drawFunction()
void putLine(Graphics g, double x1, double y1,
double x2, double y2) {
// Draw a line segment from the point (x1,y1) to (x2,y2).
// These real values must be scaled to get the integer
// coordinates of the corresponding pixels.
int a1, b1; // Pixel coordinates corresponding to (x1,y1).
int a2, b2; // Pixel coordinates corresponding to (x2,y2).
int width = getWidth(); // Width of the canvas.
int height = getHeight(); // Height of the canvas.
a1 = (int)( (x1 + 5) / 10 * width );
b1 = (int)( (5 - y1) / 10 * height );
a2 = (int)( (x2 + 5) / 10 * width );
b2 = (int)( (5 - y2) / 10 * height );
if (Math.abs(y1) < 30000 && Math.abs(y2) < 30000) {
// Only draw lines for reasonable y-values.
// This should not be necessary, I think,
// but I got a problem when y was very large.)
g.drawLine(a1,b1,a2,b2);
}
} // end putLine()
} // end nested class GraphPanel
} // end class SimpleGrapher