Search code examples
javaswinganimationplotpaint

Why is JFrame paint() plotting the same point multiple times (or not at all)?


Context

I'm writing a program that approximates the value of π using the Monte Carlo method.
My plan is have it visually plot X amount of points and calculate the result based on that plot.

The design so far is this:

  1. Ask the user how many points they want to use in the approximation.
  2. Ask the user how fast they'd like the simulation to run.
  3. Do the simulation based on those inputs.

A quick warning: I am new to Java - please forgive any horrendous code setup or stupid questions.

The Problem

When running the program, one of two things will happen based on the input speed:

a.) For slow or medium speeds, two points are plotted before the timer is even started and the first "real" point is plotted.

enter image description here

b.) For fast or very fast speeds, the first point gets plotted twice.

enter image description here

This can also happen to the second point instead of the first for some reason.

enter image description here

I'm at a loss as to why this is happening. Here are some things I've tried:

  • Adjusting the input point count
  • Adjusting the boolean conditions under which the points are plotted
  • Making JFrame use a 'dummy' point for the first point, then use regular points thereafter
  • Adjusting the boolean conditions for repaint

My Classes

Main.java

public class Main {

    public static void main(String[] args) {
        SimSetup simSetup = new SimSetup();
        simSetup.simSetup();
        SimFrame frame = new SimFrame();
        SimDrawing d = new SimDrawing();
        frame.add(d);
        frame.setVisible(true);
    }
}

SimSetup.java

public class SimSetup {

    private final UserInput userInput = new UserInput();


    public void simSetup() {
        title.displayTitle();
        boolean startupSuccess = false;
        while (!startupSuccess) {
            userInput.setPoints();
            userInput.setPlotSpeed();
            if ((userInput.setVerification(userInput.getPoints(), 
                    userInput.getPlotSpeed()))) {
                break;
            }
        }
        SimDrawing.setPoints(userInput.getPoints());
        SimDrawing.setSpeed(userInput.getPlotSpeed());
    }
}  

UserInput.java

import java.util.InputMismatchException;
import java.util.Objects;
import java.util.Scanner;

public class UserInput {

    private int numPoints;
    private short plotSpeed;
    private String verification;


    public int getPoints() {
        return numPoints;
    }

    public short getPlotSpeed() {
        return plotSpeed;
    }

    public String getVerification() {
        return verification;
    }


    public int setPoints() {
        System.out.print("Enter the number of points to be plotted using a positive integer between 1 and 100,000: ");


        try {
            Scanner scanner = new Scanner(System.in);
            numPoints = scanner.nextInt();

            if (numPoints < 0) {
                System.out.println("\nNumber of points must be a positive integer. Please try again.\n");
                setPoints();
            } else if (numPoints == 0) {
                System.out.println("\nCan't generate a plot using zero points! Please try again.\n");
                setPoints();
            } else if (numPoints > 100000) {
                System.out.println("\nNumber of points is too large. Please try again.\n");
                setPoints();
            }

        } catch (InputMismatchException e) {
            System.out.println("\nThe number of points must be an integer between 1 and 100,000. Please try again.\n");
            setPoints();
        }
        return numPoints;
    }

    public short setPlotSpeed() {
        System.out.print("\nSelect a plotting animation speed from the choices below:");
        System.out.println("\n0 -- Slow\n1 -- Medium\n2 -- Fast\n3 -- Very Fast\n");

        try {
            Scanner scanner = new Scanner(System.in);
            short speedChoice = scanner.nextShort();

            if (speedChoice == 0) {
                plotSpeed = 100;
            } else if (speedChoice == 1) {
                plotSpeed = 50;
            } else if (speedChoice == 2) {
                plotSpeed = 10;
            } else if (speedChoice == 3) {
                plotSpeed = 1;
            }

        } catch (InputMismatchException e) {
            System.out.println("Speed must be an integer between 0 to 3. Please try again.");
        }
        return plotSpeed;
    }

    public boolean setVerification(long numPoints, short plotSpeed) {

        if (numPoints > 50000 && (plotSpeed == 100 || plotSpeed == 50)) {
            System.out.println("This combination of points and animation speed will result in long completion time.");
            System.out.println("Are you sure? (Y to continue, N to re-enter choices.)");

            try {
                Scanner scanner = new Scanner(System.in);
                verification = scanner.nextLine();
                verification = verification.toLowerCase();

                if (Objects.equals(verification, "n")) {
                    System.out.println("\nProcess aborted.\n");
                    return false;
                } else if (!Objects.equals(verification, "n") && !Objects.equals(verification, "y")) {
                    System.out.println("\nVerification input not recognized. Please try again.");
                    setVerification(numPoints, plotSpeed);
                    return false;
                }

            } catch (Exception e) {
                System.out.println("Exception: " + e);
            }
        }
        return true;
    }
}  

SimDrawing.java

import javax.swing.*;
import java.awt.*;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;

public class SimDrawing extends JPanel implements ActionListener {


    int count = 0;
    int max = 525;
    int min = 75;
    int circleRadius = 450;
    int x = min + (int) (Math.random() * (max - min + 1));
    int y = min + (int) (Math.random() * (max - min + 1));
    private static int points;
    private static short speed;
    private int circlePoints;
    private int outsidePoints;
    Timer t = new Timer(speed, this);


    public static void setPoints(int numPoints) {
        points = numPoints;
    }

    public static void setSpeed(short plotSpeed) {
        speed = plotSpeed;
    }

    public SimDrawing() {
        t.start();
    }

    @Override
    public void actionPerformed(ActionEvent e) {

        System.out.println("\nCount: " + count);

        x = min + (int) (Math.random() * (max - min + 1));
        y = min + (int) (Math.random() * (max - min + 1));

        System.out.println("\nPoint generated: " + "(" + x + "," + y + ")");
        repaint();

        if (count == points) {
            t.stop();
            pointCheck();
        }
        count++;
    }

    public void paint(Graphics g) {

        Graphics2D g2d = (Graphics2D) g;
        double distanceFromCenter = (Math.sqrt(Math.pow((x - 75), 2) + Math.pow((y - 525), 2)));

        g2d.setColor(Color.BLACK);
        g2d.setStroke(new BasicStroke(2));
        g2d.drawLine(74, 74, 526, 74);
        g2d.drawLine(74, 74, 74, 526);
        g2d.drawLine(74, 526, 526, 526);
        g2d.drawLine(526, 74, 526, 526);


        g2d.setStroke(new BasicStroke(3));
        if (distanceFromCenter <= circleRadius) {
            g2d.setColor(Color.BLACK);
            g2d.drawLine(x, y, x, y);
            System.out.println("Plotting inside point: " + "(" + x + "," + y + ")");
            circlePoints++;
        }
        if (distanceFromCenter > circleRadius) {
            g2d.setColor(Color.RED);
            g2d.drawLine(x, y, x, y);
            System.out.println("Plotting outside point: " + "(" + x + "," + y + ")");
            outsidePoints++;
        }
    }

    public void pointCheck() {
        System.out.println("\nThe number of points inside the circle is: " + circlePoints);
        System.out.println("The number of points outside the circle is: " + outsidePoints);

        int totalMappedPoints = circlePoints + outsidePoints;

        System.out.println("The total number of points is: " + totalMappedPoints);
    }
}  

SimFrame.java

import javax.swing.*;
import java.awt.*;

public class SimFrame extends JFrame {
    public SimFrame() {

        setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
        setAlwaysOnTop(true);
        setTitle("Monte Carlo Method of Approximating π");
        setSize(616, 639);
        setResizable(false);
        setLayout(new GridLayout());
        setLocationRelativeTo(null);
    }
}

Solution

  • You've got a bunch of problems with your code, much of this outlined in comments, but the main ones include:

    • You're mixing linear console I/O (input and output) with event-driven Swing GUI I/O. This can lead to threading issues and blocked code, and so it is best to use either one or the other, here just stick with Swing I/O, since the program absolutely needs to be graphics-based.
    • You are overriding the JFrame's paint method. This method paints the entire GUI, including its borders and child components, and the method is not double-buffered, almost always leading to choppy animations. Much better to override a drawing JPanel's paintComponent method, which is only concerned with painting, and which uses double buffering by default, leading to smoother animations.
    • You rely on the painting graphics to "remember" previous states, graphic residuals, that are not stable. To show that this is so, after you program runs, minimize the GUI and then restore it and see if your drawn points still persist. To fix this, either draw on a BufferedImage, and then display that same buffered image within your painting method, or use a for loop in the painting method to paint each point.
    • You don't call the painting method's super painting method, leading to an absence of "house-keeping" painting, which can leand to retained "dirty" pixels in your GUI and can lead to retained graphics artifacts polluting your GUI. If you override paintComponent, which you should do, you should call super.paintComponent in that override to allow the JPanel to do its house-keeping graphics clean up.

    Myself, I would consider several changes, including:

    • I would create a separate non-GUI model class (or classes), starting with one perhaps called "MonteCarloPoint" that holds a java.awt.Point field and a boolean field called inside. This will represent a single point and the point will know if it is inside or outside your designated border.
    • I would create another non-GUI model class, say called MonteCarloModel, give it an array of MonteCarloPoints, and give it code to do the randomization, and point creation. I would give this class the Swing Timer, and would give it the ability to notify other classes if and when it changes, so that they can extract any new or changing data from the model, and then respond to these changes (such as by plotting the data points in the view, the GUI).
    • I would create a PlotSpeed enum to represent the possible animation speeds, from slow to very-fast. I'd also give the very-fast value a more reasonable value, since 1 msec is not a realistic time slice for a Swing Timer. Consider, perhaps 10 to 14.
    • I would create a GUI class (or classes) that listen for changes to the model class, and that then display the state of the model
    • I would get numeric input from the user from within a JSpinner, which is sort of like a text field, but which only accepts numbers.
    • I would display the speed choices in a JComboBox
    • I'd show the points created within a JList
    • And I'd display progress in a JProgressBar.
    • The dots would be displayed in a JPanel, say named PointPanel, that would listen for changes in the MonteCarloModel object, and then which would plot a new point when notified that one has been created.
    • The JList and JProgressBar would also listen for model changes, and then change their display accordingly.

    For example:

    enter image description here

    import javax.swing.*;
    
    public class MonteCarloMain {
    
        public static void main(String[] args) {
            SwingUtilities.invokeLater(() -> {
                JFrame frame = new JFrame("Random Points");
                frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
                frame.add(new MonteCarloView());
                frame.pack();
                frame.setLocationRelativeTo(null);
                frame.setVisible(true);
            });
        }
    
    }
    
    import java.awt.Point;
    
    public record MonteCarloPoint(Point point, boolean inside) {
    }
    
    public enum PlotSpeed {
        // let's use a log scale for speeds
        SLOW(300), MEDIUM(102), FAST(35), VERY_FAST(12);
    
        private final int speed;
    
        PlotSpeed(int speed) {
            this.speed = speed;
        }
    
        public int getSpeed() {
            return speed;
        }
    }
    
    import java.awt.Point;
    import java.beans.PropertyChangeListener;
    import javax.swing.Timer;
    
    import javax.swing.event.SwingPropertyChangeSupport;
    
    public class MonteCarloModel {
        public static final String POINT = "point";
        public static final String FINISHED = "finished";
        private SwingPropertyChangeSupport pcSupport = new SwingPropertyChangeSupport(this);
        private int pointsInsideCircle = 0;
        private int pointsOutsideCircle = 0;
        private int totalPoints = 0;
        private Timer timer;
        private int pointsToPlot;
        private PlotSpeed plotSpeed;
        private MonteCarloPoint[] points;
        private int size = 0;
        private int circleRadius = 0;
        
        public MonteCarloModel(int points, PlotSpeed medium, int size, int circleRadius) {
            pointsToPlot = points;
            plotSpeed = medium;
            this.size = size;
            this.circleRadius = circleRadius;
        }
    
        // add prop change listeners
        public void addPropertyChangeListener(String propertyName, PropertyChangeListener listener) {
            pcSupport.addPropertyChangeListener(propertyName, listener);
        }
    
        public void removePropertyChangeListener(String propertyName, PropertyChangeListener listener) {
            pcSupport.removePropertyChangeListener(propertyName, listener);
        }
    
        public void startPlotting() {
            points = new MonteCarloPoint[pointsToPlot];
            pointsInsideCircle = 0;
            pointsOutsideCircle = 0;
            totalPoints = 0;
            timer = new Timer(plotSpeed.getSpeed(), e -> {
                if (totalPoints < pointsToPlot) {
                    int x = (int) (Math.random() * size);
                    int y = (int) (Math.random() * size);
                    Point newPoint = new Point(x, y);
                    boolean inside = isInsideCircle(x, y);
                    MonteCarloPoint newMyPoint = new MonteCarloPoint(newPoint, inside);
                    points[totalPoints] = newMyPoint;
                    if (inside) {
                        pointsInsideCircle++;
                    } else {
                        pointsOutsideCircle++;
                    }
                    pcSupport.firePropertyChange(POINT, null, newMyPoint);
                    totalPoints++;
                } else {
                    timer.stop();
                    pcSupport.firePropertyChange(FINISHED, false, true);
                }
            });
            timer.start();
        }
    
        public void stopPlotting() {
            if (timer != null) {
                timer.stop();
                pcSupport.firePropertyChange(FINISHED, false, true);
            }
        }
    
        private boolean isInsideCircle(int x, int y) {
            int dx = x - size / 2;
            int dy = y - size / 2;
            return dx * dx + dy * dy <= circleRadius * circleRadius;
        }
    
        public int getPointsInsideCircle() {
            return pointsInsideCircle;
        }
    
        public int getPointsOutsideCircle() {
            return pointsOutsideCircle;
        }
    
        public int getTotalPoints() {
            return totalPoints;
        }
    
        public int getPointsToPlot() {
            return pointsToPlot;
        }
    
        public MonteCarloPoint[] getPoints() {
            return points;
        }
    }
    
    import java.awt.BorderLayout;
    import java.awt.event.KeyEvent;
    import javax.swing.*;
    
    public class MonteCarloView extends JPanel {
        public static final int MAX_POINTS = 100000;
        public static final int PADDING = 20;
        private static final int SIZE = 600;
        private static final int CIRCLE_RADIUS = 250;
        private PointPanel pointPanel = new PointPanel(CIRCLE_RADIUS, SIZE);
        private JSpinner pointSpinner = new JSpinner(new SpinnerNumberModel(200, 1, MAX_POINTS, 1));
        private JComboBox<PlotSpeed> speedCombo = new JComboBox<>(PlotSpeed.values());
        private JButton startButton = new JButton("Start");
        private JButton stopButton = new JButton("Stop");
        private DefaultListModel<String> listModel = new DefaultListModel<>();
        private JList<String> pointList = new JList<>(listModel);
        private JProgressBar progressBar = new JProgressBar();
        private MonteCarloModel pointModel = null;
    
        public MonteCarloView() {
            JPanel controlPanel = new JPanel();
            controlPanel.add(new JLabel("Number of points: "));
            controlPanel.add(pointSpinner);
            controlPanel.add(new JLabel("Speed: "));
            speedCombo.setSelectedItem(PlotSpeed.FAST);
            controlPanel.add(speedCombo);
            controlPanel.add(startButton);
            controlPanel.add(stopButton);
            startButton.setMnemonic(KeyEvent.VK_S);
            stopButton.setMnemonic(KeyEvent.VK_T);
            startButton.addActionListener(e -> startPlotting());
            stopButton.addActionListener(e -> stopPlotting());
    
            pointList.setVisibleRowCount(10);
            pointList.setPrototypeCellValue("Point 123456: Outside");
            pointList.setLayoutOrientation(JList.VERTICAL);
            JScrollPane pointListScrollPane = new JScrollPane(pointList);
            JPanel pointListWrapperPanel = new JPanel(new BorderLayout());
            pointListWrapperPanel.add(pointListScrollPane, BorderLayout.CENTER);
            pointListWrapperPanel.setBorder(BorderFactory.createTitledBorder("Points"));
    
            progressBar.setStringPainted(true);
            progressBar.setString(String.format("%d%%", 0));
            JPanel progressPanel = new JPanel(new BorderLayout());
            progressPanel.add(progressBar, BorderLayout.CENTER);
            progressPanel.setBorder(BorderFactory.createTitledBorder("Progress"));
    
            setBorder(BorderFactory.createEmptyBorder(PADDING, PADDING, PADDING, PADDING));
    
            setLayout(new BorderLayout(PADDING, PADDING));
            add(pointPanel, BorderLayout.CENTER);
            add(controlPanel, BorderLayout.PAGE_END);
            add(pointListWrapperPanel, BorderLayout.LINE_END);
            add(progressPanel, BorderLayout.PAGE_START);
        }
    
        private void startPlotting() {
            if (pointModel != null) {
                pointModel.stopPlotting();
            }
            pointModel = new MonteCarloModel((int) pointSpinner.getValue(), (PlotSpeed) speedCombo.getSelectedItem(), SIZE, CIRCLE_RADIUS);
            pointModel.addPropertyChangeListener(MonteCarloModel.POINT, evt -> {
                MonteCarloPoint newPoint = (MonteCarloPoint) evt.getNewValue();
                listModel.addElement(String.format("Point %d: %s", pointModel.getTotalPoints(), newPoint.inside() ? "Inside" : "Outside"));
                pointPanel.setModel(pointModel);
                int value = (pointModel.getTotalPoints() + 1) * 100 / pointModel.getPointsToPlot();
                value = Math.min(100, value);
                progressBar.setValue(value);
                progressBar.setString(String.format("%d%%", value));
            });
            pointModel.addPropertyChangeListener(MonteCarloModel.FINISHED, evt -> {
                startButton.setEnabled(true);
                pointSpinner.setEnabled(true);
                speedCombo.setEnabled(true);
            });
            startButton.setEnabled(false);
            pointSpinner.setEnabled(false);
            speedCombo.setEnabled(false);
            listModel.clear();
            progressBar.setValue(0);
            pointModel.startPlotting();
        }
    
        private void stopPlotting() {
            if (pointModel != null) {
                pointModel.stopPlotting();
            }
        }
    }
    
    import java.awt.Color;
    import java.awt.Dimension;
    import java.awt.Graphics;
    import java.awt.Graphics2D;
    import java.awt.RenderingHints;
    
    import javax.swing.*;
    
    
    public class PointPanel extends JPanel {
        private static final int DIAMETER = 5;
        private MonteCarloModel pointModel;
        private int circleRadius = 0;
        private int size = 0;
    
        public PointPanel(int circleRadius, int size) {
            setBackground(Color.WHITE);
            setBorder(BorderFactory.createLineBorder(Color.BLACK));
            this.circleRadius = circleRadius;
            this.size = size;
        }
    
        @Override
        public Dimension getPreferredSize() {
            Dimension dim = super.getPreferredSize();
            if (isPreferredSizeSet()) {
                return dim;
            } else {
                int w = Math.max(size, dim.width);
                int h = Math.max(size, dim.height);
                return new Dimension(w, h);
            }
        }
    
        public void setModel(MonteCarloModel pointModel) {
            // remove old listeners
            if (this.pointModel != null) {
                this.pointModel.removePropertyChangeListener(MonteCarloModel.POINT, evt -> {
                    repaint();
                });
            }
            this.pointModel = pointModel;
            pointModel.addPropertyChangeListener(MonteCarloModel.POINT, evt -> {
                repaint();
            });
        }
    
        @Override
        protected void paintComponent(Graphics g) {
            super.paintComponent(g);
            if (pointModel == null) {
                return;
            }
            g.setColor(Color.BLACK);
            // rendering hints for better circle rendering
            ((Graphics2D) g).setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
            g.drawOval(size / 2 - circleRadius, size / 2 - circleRadius, 2 * circleRadius, 2 * circleRadius);
            MonteCarloPoint[] points = pointModel.getPoints();
    
            for (int i = 0; i < pointModel.getTotalPoints(); i++) {
                g.setColor(points[i].inside() ? Color.BLUE : Color.RED);
                g.fillOval(points[i].point().x - DIAMETER / 2, points[i].point().y - DIAMETER / 2, DIAMETER, DIAMETER);
            }
        }
    
    }