[ Exercises | Chapter Index | Main Index ]

Solution for Programming Exercise 6.9


This page contains a sample solution to one of the exercises from Introduction to Programming Using Java.


Exercise 6.9:

A polygon is a geometric figure made up of a sequence of connected line segments. The points where the line segments meet are called the vertices of the polygon. Subsection 6.2.4 has a list of shape-drawing methods in a GraphicsContext. Among them are methods for stroking and for filling polygons: g.strokePolygon(xcoords,ycoords,n) and g.fillPolygon(xcoords,ycoords,n), For these commands, the coordinates of the vertices of the polygon are stored in arrays xcoords and ycoords of type double[], and the number of vertices of the polygon is given by the third parameter, n. Note that it is OK for the sides of a polygon to cross each other, but the interior of a polygon with self-intersections might not be exactly what you expect.

Write a program that lets the user draw polygons. As the user clicks a sequence of points in a Canvas, count the points and store their x- and y-coordinates in two arrays. These points will be the vertices of the polygon. As the user is creating the polygon, you should just connect all the points with line segments. When the user clicks near the starting point, draw the complete polygon. Draw it with a red interior and a black border. Once the user has completed a polygon, the next click should clear the data and start a new polygon from scratch.

Here is a picture of my solution after the user has drawn a fairly complex polygon:

screen shot of the solution


Discussion

This is an exercise in using arrays, but it is also an exercise in using instance variables to record the state of the program. When the user clicks the canvas, my program completely redraws the canvas. (This is not really necessary, but it's often the easiest way to organize a drawing program, since it's the easiest way to ensure that the picture always agrees perfectly with the state of the program.) The draw() method that draws the canvas needs enough state information in the instance variables to correctly draw the picture. Obviously, that includes the coordinate arrays and the number of points that have been stored in the arrays. But the picture is different depending on whether or not the user has completed the polygon. If the polygon is complete, the picture shows a polygon; if not, the picture shows line segments connecting each point to the next. We need to record that basic distinction as part of the state. We can do that using a boolean instance variable, complete, which is true when a complete polygon should be drawn and false while the polygon is under construction. So, here are the necessary instance variables:

double[] xCoord, yCoord;  // Arrays to hold the coordinates for up to 500 points.
int pointCt;              // Number of points in the arrays.
boolean complete;         // Set to true when a polygon is complete.

The arrays xCoord and yCoord are examples of partially full arrays (Subsection 3.8.4), although there is only one counter variable that applies to both arrays.

Given these instance variables, the draw() method can be written. To make the picture look nicer, I decided to use wide lines. There is also a question about what to draw when there is only one point. It's nice if the user can see that the point has been added to the data, but one point is not enough to draw a line or polygon. I decided to draw a small square at the first point, just to make it visible and noticeable. This also helps the user to find the first point when she is trying to click on it to complete the polygon. The fill color of the polygon is given by a global constant, POLYGON_COLOR.

private void draw() {
    GraphicsContext g = canvas.getGraphicsContext2D();
    g.setFill(Color.WHITE);
    g.fillRect(0,0,canvas.getWidth(),canvas.getHeight());
    if (pointCt == 0)
        return;
    g.setLineWidth(2);
    g.setStroke(Color.BLACK);
    if (complete) { // draw a polygon
        g.setFill(POLYGON_COLOR);
        g.fillPolygon(xCoord, yCoord, pointCt);
        g.strokePolygon(xCoord, yCoord, pointCt);
    }
    else { // show the lines the user has drawn so far
        g.setFill(Color.BLACK);
        g.fillRect(xCoord[0]-2, yCoord[0]-2, 4, 4);  // small square marks first point
        for (int i = 0; i < pointCt - 1; i++) {
            g.strokeLine( xCoord[i], yCoord[i], xCoord[i+1], yCoord[i+1]);
        }
    }
}

The rest of the program logic is in a mousePressed() routine that is called when the user presses the mouse on the canvas. In that method, state variables have to change in response to the user's actions. It requires some care to do things in the right order. The three things that can happen are that the user starts a new polygon, the user completes the current polygon, or the user just adds a point to the current polygon. The conditions should be tested in that order:

if the current polygon is complete
    start a new polygon with the point where the user clicked
else if the user clicked near the starting point
    complete the current polygon
else
    add the point that the user clicked to the data
call draw() to make the change visible

Actually, in my solution, I decided to add another case: The polygon can also be completed by right-clicking, or—to be very safe—if the number of points has reached 500. Also, there is a bug in the algorithm as stated, where it tests "if the user clicked near the first point". This test doesn't make sense unless there actually is a first point, that is unless pointCt is greater than zero. The test should really read "if pointCt > 0 and if the user clicked near (xCoord[0],yCoord[0])."

To complete a polygon just means setting the value of the variable complete to true. When draw() is called, the data will be displayed as a polygon. When a new polygon is started, the value of the variable complete has to be reset to false. Also, the first point on the polygon has to be put into the coordinate arrays, and pointCt must be set to 1 to indicate that there is only one point in the data. This much is pretty straightforward to implement.

The only thing in the algorithm that still needs implementing is to test whether the user clicks "near the starting point". The starting point has coordinates (xCoord[0],yCoord[0]) and the point where the user clicked has coordinates (evt.getX(),evt.getY()). In my program, I check whether the x-coordinates of these points are three pixels or less apart and the y-coordinates are also three pixels or less apart. This is done by checking whether "Math.abs(xCoord[0] - evt.getX()) <= 3 && Math.abs(yCoord[0] - evt.getY()) <= 3".


The Solution

import javafx.application.Application;
import javafx.stage.Stage;
import javafx.scene.Scene;
import javafx.scene.layout.StackPane;
import javafx.scene.canvas.Canvas;
import javafx.scene.canvas.GraphicsContext;
import javafx.scene.input.MouseEvent;
import javafx.scene.input.MouseButton;
import javafx.scene.paint.Color;

/**
 * This program lets the user draw filled polygons.
 * The user inputs a polygon by clicking a series of points.
 * The points are connected with lines from each point to the
 * next Clicking near the starting point (within 3 pixels) or
 * right-clicking will complete the polygon, so the user can 
 * begin a new one.  As soon as the user begins drawing a new 
 * polygon, the old one is discarded.
 */
public class SimplePolygons extends Application {
     
    public static void main(String[] args) {
        launch();
    }

    //---------------------------------------------------------------------

    private Canvas canvas;

    /* Variables for implementing polygon input. */

    private double[] xCoord, yCoord;    // Arrays containing the points of 
                                        //   the polygon.  Up to 500 points 
                                        //   are allowed.

    private int pointCt;  // The number of points that have been input.

    private boolean complete;   // Set to true when the polygon is complete.
                                // When this is false, only a series of lines are drawn.
                                // When it is true, a filled polygon is drawn.

    private final static Color POLYGON_COLOR = Color.RED;  
                                // Color that is used to draw the polygons.  


    /**
     * Set up the GUI, and install a mouse hander its data.
     */
    public void start (Stage stage) {
        
        xCoord = new double[500];  // create arrays to hold the polygon's points
        yCoord = new double[500];
        pointCt = 0;
        
        canvas = new Canvas(400,400);
        draw();
        canvas.setOnMousePressed( e -> mousePressed(e) );
        
        StackPane root = new StackPane(canvas);
        root.setStyle("-fx-border-color: black");
        
        Scene scene = new Scene(root);
        stage.setScene(scene);
        stage.setTitle("Polygons");
        stage.setResizable(false);
        stage.show();
        
    }

    
    /**
     * Fill the canvas with white.  If the polygon is complete, draw it.
     * If not, draw the lines that the user has input so far.  (If only
     * one point has been input, it will still be visible as a small dot.)
     */
    private void draw() {
        GraphicsContext g = canvas.getGraphicsContext2D();
        g.setFill(Color.WHITE);
        g.fillRect(0,0,canvas.getWidth(),canvas.getHeight());
        if (pointCt == 0)
            return;
        g.setLineWidth(2);
        g.setStroke(Color.BLACK);
        if (complete) { // draw a polygon
            g.setFill(POLYGON_COLOR);
            g.fillPolygon(xCoord, yCoord, pointCt);
            g.strokePolygon(xCoord, yCoord, pointCt);
        }
        else { // show the lines the user has drawn so far
            g.setFill(Color.BLACK);
            g.fillRect(xCoord[0]-2, yCoord[0]-2, 4, 4);  // small square marks first point
            for (int i = 0; i < pointCt - 1; i++) {
                g.strokeLine( xCoord[i], yCoord[i], xCoord[i+1], yCoord[i+1]);
            }
        }
    }


    /**
     * Processes a mouse click.
     */
    private void mousePressed(MouseEvent evt) { 

        if (complete) {
                // Start a new polygon at the point that was clicked.
            complete = false;
            xCoord[0] = evt.getX();
            yCoord[0] = evt.getY();
            pointCt = 1;
        }
        else if ( pointCt > 0 && pointCt > 0 && (Math.abs(xCoord[0] - evt.getX()) <= 3)
                   && (Math.abs(yCoord[0] - evt.getY()) <= 3) ) {
                // User has clicked near the starting point.
                // The polygon is complete.
            complete = true;
        }
        else if (evt.getButton() == MouseButton.SECONDARY || pointCt == 500) {
                // The polygon is complete.
            complete = true;
        }
        else {
                // Add the point where the user clicked to the list of
                // points in the polygon, and draw a line between the
                // previous point and the current point.  A line can
                // only be drawn if there are at least two points.
            xCoord[pointCt] = evt.getX();
            yCoord[pointCt] = evt.getY();
            pointCt++;
        }
        draw();  // in all cases, redraw the picture.
    } // end mousePressed()


}  // end class SimplePolygons

[ Exercises | Chapter Index | Main Index ]