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:

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:

    Myself, I would consider several changes, including:

    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);
            }
        }
    
    }