import javafx.application.Application; import javafx.stage.Stage; import javafx.scene.Scene; import javafx.scene.control.Label; import javafx.scene.control.Button; import javafx.scene.control.ComboBox; import javafx.scene.canvas.Canvas; import javafx.scene.canvas.GraphicsContext; import javafx.scene.layout.Pane; import javafx.scene.input.MouseEvent; import javafx.scene.paint.Color; import javafx.scene.text.Font; import javafx.scene.text.FontWeight; import javafx.geometry.Pos; /** * This program demonstrates recursion by counting the number of * squares in a "blob". The squares are arranged in a grid, * and each position in the grid can be either empty or filled. * A blob is defined to be a filled square and any square that * can be reached from that square by moving horizontally or * vertically to other filled squares. This program fills * the grid randomly. If the user clicks on a filled square, * all the squares in the blob that contains that square are * colored red, and the number of squares in the blob is * reported. The program can also count and report the number * of blobs. When the user clicks a "New Blobs" button, * the grid is randomly re-filled. */ public class Blobs extends Application { public static void main(String[] args) { launch(args); } //------------------------------------------------------------------------- final static int SQUARE_SIZE = 9; // Size of one square in the grid. final static int width = 454; // full size of the Scene final static int height = 400; Canvas canvas; // Where the blobs are drawn. GraphicsContext g; // For drawing on the canvas. Label message; // For displaying information to the user. ComboBox percentFill; // When the user clicks the "New Blobs" button // to randomly fill the grid, this menu controls // the probability that a given square in the grid // is filled. int rows; // Number of rows in the grid. This depends on the size of the window. int columns; // Number of columns in the grid. This depends on the size of the window. boolean[][] filled; // filled[r][c] is true if the square at row r, column c is filled. boolean[][] visited; // visited[r][c] is true if the square at row r, column c has // has already been visited by the getBlobSize() method. public void start(Stage stage) { /* Determine the number of rows and columns and create the * filled and visited arrays. Fill the squares at random. */ rows = (height - 120) / SQUARE_SIZE; columns = (width - 20) / SQUARE_SIZE; filled = new boolean[rows][columns]; visited = new boolean[rows][columns]; canvas = new Canvas( 1+columns*SQUARE_SIZE, 1+rows*SQUARE_SIZE ); g = canvas.getGraphicsContext2D(); canvas.setOnMousePressed( e -> mousePressed(e) ); /* Create the components. */ message = new Label("Click a square to get the blob size."); message.setTextFill(Color.BLUE); message.setFont( Font.font(null,FontWeight.BOLD,14) ); percentFill = new ComboBox(); percentFill.getItems().add("10% fill"); percentFill.getItems().add("20% fill"); percentFill.getItems().add("30% fill"); percentFill.getItems().add("40% fill"); percentFill.getItems().add("50% fill"); percentFill.getItems().add("60% fill"); percentFill.getItems().add("70% fill"); percentFill.getItems().add("80% fill"); percentFill.getItems().add("90% fill"); percentFill.setEditable(false); percentFill.setValue("40% fill"); Button newButton = new Button("New Blobs"); newButton.setOnAction( e -> fillGrid() ); Button countButton = new Button("Count the Blobs"); countButton.setOnAction( e -> countBlobs() ); /* Create a root pane and add all the components. * Do the layout by hand! */ Pane root = new Pane(canvas, message, percentFill, newButton, countButton); root.setStyle("-fx-background-color: #BBF; -fx-border-color: #00A; -fx-border-width:2px"); canvas.relocate(10,10); message.setManaged(false); message.relocate(15, height-100); message.resize( width-30, 23); message.setAlignment(Pos.CENTER); countButton.setManaged(false); countButton.relocate(15, height-72); countButton.resize(width-30, 28); newButton.setManaged(false); newButton.relocate(15, height-37); newButton.resize((width-40)/2, 28); percentFill.setManaged(false); percentFill.relocate(width/2 + 5, height-37); percentFill.resize((width-40)/2, 28); /* Set up the scene and window and show the window. */ Scene scene = new Scene(root, width, height ); stage.setScene(scene); stage.setResizable(false); stage.setTitle("Random Blob Counter"); fillGrid(); stage.show(); } // end start() /** * When the user clicks the "New Blobs" button, fill the grid of squares * randomly. The probability that a given square is filled is given by * the percentFill Choice menu. The probabilities corresponding to the * items in that menu are 0.1, 0.2,... 0.9. The visited array is cleared * so there won't be any red-colored squares in the grid. */ private void fillGrid() { double probability = (percentFill.getSelectionModel().getSelectedIndex() + 1) / 10.0; for (int r = 0; r < rows; r++) for (int c = 0; c < columns; c++) { filled[r][c] = (Math.random() < probability); visited[r][c] = false; } message.setText("Click a square to get the blob size."); draw(); } /** * When the user clicks the "Count the Blobs" button, find the number * of blobs in the grid and report the number in the message Label. */ private void countBlobs() { int count = 0; // Number of blobs. /* First clear out the visited array. The getBlobSize() method will mark every filled square that it finds by setting the corresponding element of the array to true. Once a square has been marked as visited, it will stay marked until all the blobs have been counted. This will prevent the same blob from being counted more than once. */ for (int r = 0; r < rows; r++) for (int c = 0; c < columns; c++) visited[r][c] = false; /* For each position in the grid, call getBlobSize() to get the size of the blob at that position. If the size is not zero, count a blob. Note that if we come to a position that was part of a previously counted square, getBlobSize() will return 0 and the blob will not be counted again. */ for (int r = 0; r < rows; r++) for (int c = 0; c < columns; c++) { if (getBlobSize(r,c) > 0) count++; } draw(); // Note that all the filled squares will be red! message.setText("The number of blobs is " + count); } // end countBlobs() /** * Counts the squares in the blob at position (r,c) in the * grid. Squares are only counted if they are filled and * unvisited. If this routine is called for a position that * has been visited, the return value will be zero. */ private int getBlobSize(int r, int c) { if (r < 0 || r >= rows || c < 0 || c >= columns) { // This position is not in the grid, so there is // no blob at this position. return 0; } if (filled[r][c] == false || visited[r][c] == true) { // This square is not part of a blob, or else it has // already been counted, so return zero. return 0; } visited[r][c] = true; // Mark the square as visited so that // we won't count it again during the // following recursive calls to this method. int size = 1; // Count the square at this position, then count the // the blobs that are connected to this square // horizontally or vertically. size += getBlobSize(r-1,c); size += getBlobSize(r+1,c); size += getBlobSize(r,c-1); size += getBlobSize(r,c+1); return size; } // end getBlobSize() /** * The user has clicked the mouse on the panel. If the * user has clicked on a position in the grid, count * the number of squares in the blob at that position. */ private void mousePressed(MouseEvent evt) { int row = (int)((evt.getY()-1) / SQUARE_SIZE); int col = (int)((evt.getX()-1) / SQUARE_SIZE); if (row < 0 || row >= rows || col < 0 || col >= columns) { message.setText("Please click on a square!"); // shouldn't happen return; } for (int r = 0; r < rows; r++) for (int c = 0; c < columns; c++) visited[r][c] = false; // Clear visited array before counting. int size = getBlobSize(row,col); if (size == 0) message.setText("There is no blob at (" + row + "," + col + ")."); else if (size == 1) message.setText("Blob at (" + row + "," + col + ") contains 1 square."); else message.setText("Blob at (" + row + "," + col + ") contains " + size + " squares."); draw(); } /** * Paint the panel, showing the grid of squares. (The other components * in the panel draw themselves.) */ public void draw() { /* Fill the entire canvas with white, then draw black lines around * the edges and between the squares of the grid. */ g.setFill(Color.WHITE); g.fillRect(0, 0, columns*SQUARE_SIZE, rows*SQUARE_SIZE); g.setStroke(Color.BLACK); for (int i = 0; i <= rows; i++) g.strokeLine(0.5, 0.5 + i*SQUARE_SIZE, columns*SQUARE_SIZE + 0.5, 0.5 + i*SQUARE_SIZE); for (int i = 0; i <= columns; i++) g.strokeLine(0.5 + i*SQUARE_SIZE, 0.5, 0.5 + i*SQUARE_SIZE, rows*SQUARE_SIZE + 0.5); /* Fill "visited" squares with red and "filled" squares with gray. Other squares remain white. */ for (int r = 0; r < rows; r++) for (int c = 0; c < columns; c++) { if (visited[r][c]) { g.setFill(Color.RED); g.fillRect(1 + c*SQUARE_SIZE, 1 + r*SQUARE_SIZE, SQUARE_SIZE - 1, SQUARE_SIZE - 1); } else if (filled[r][c]) { g.setFill(Color.GRAY); g.fillRect(1 + c*SQUARE_SIZE, 1 + r*SQUARE_SIZE, SQUARE_SIZE - 1, SQUARE_SIZE - 1); } } } // end draw(); } // end class Blobs