2

I am working on an application that measures 'distance' driven by a Car object. Each Car is it's own thread, has a randomly generated speed, and I calculate the distance each car has driven. While I am able to get the correct data to print to the console from within the thread at whatever time interval I want (currently once/second but may update that later), I am trying to figure out a way to instead display that info in a swing GUI (either as a table row, or just as a textarea row) Since the Car class is a separate file, I can't have it push to the GUI directly (or at least, I don't know how to do that). There are 2 'columns' of info I need to update periodically: current speed, and distance.

What I tried doing: set a jtable and pull the row data: this allowed me to capture a snapshot but not to constantly update the data.

Pushing infor to a jtextarea: I can do that as a snapshot. When I tried wrapping it in a while loop (while thread running, append...) the system crashed. Same result when I tried wrapping the append in a Thread.sleep.

Since at any given time I can take a snapshot of the Car location (car.getLocation()), what I am thinking is that perhaps the main method can actively seek that snapshot every second, however, when I tried that with a while loop and a Thread.sleep, as mentioned, it crashed the system.

Also of note is that when complete, the GUI will allow for the creation of any number of Car objects, and I will want a row for each of them that will be updated periodically, so the distance numbers can be compared.

edit: based on the suggestion of @matt, I added a swing timer and modified the GUI to accommodate. The challenge now is that the new jtextfield only pops up on the display when I resize the page. Is there a way to also somehow update/refresh the GUI?

updated GUI element:

JButton carb = new JButton("add car");
        GridBagConstraints carC = new GridBagConstraints();
        carC.gridx = 1;
        carC.gridy = 2;
        carb.addActionListener(a -> {
            totalCars++;
            String carName = "Car" + totalCars;
            Car car = new Car(carName);
            cars.add(car);

            Thread thread = new Thread(car);

            JTextField t = new JTextField(50);
            GridBagConstraints tC = new GridBagConstraints();
            t.setEditable(false);
            tC.gridx = 1;
            tC.gridy = currentcol;
            currentcol++;
            content.add(t, tC);

            running = true;
            thread.start();
            ActionListener taskPerformer = new ActionListener() {
                public void actionPerformed(ActionEvent evt) {
                    t.setText(df.format(car.getLocation()));
                    t.setText("Name: " + carName + ", Drive speed: " + car.getSpeed() + ", Current speed: "
                            + car.getCurrentSpeed() + ", Location (distance from beginning): "
                            + df.format(car.getLocation()));
                }
            };
            Timer timer = new Timer(1000, taskPerformer);
            timer.start();
        });
        content.add(carb, carC);

The code (so far): Car class:

      private void setLocation(long timeElapsed) {
        location = location + ((timeElapsed-lastCheck)*(currentSpeed*0.44704));
        lastCheck = timeElapsed;
    }  

      public void run() {
        long startTime = System.currentTimeMillis();
        long elapsedTime;
        while (flag) {
            try {
                Thread.sleep(1000);
                elapsedTime = System.currentTimeMillis()-startTime;
                elapsedTime = elapsedTime/1000;
                setLocation(elapsedTime);
                System.out.println(getName()+": " + df.format(getLocation()) + " meters");
            } catch (InterruptedException e) {
            }
        }
    }


Here is how I tried using Thread.sleep from the main method. This didn't work (crashed the system):

            while (running) {
                try {
                    Thread.sleep(1000);
                    carArea.append("\nName: " + carName + ", Drive speed: " + car.getSpeed() + ", Current speed: "
                            + car.getCurrentSpeed() + ", Location (distance from beginning): " + car.getLocation());

                } catch (InterruptedException e) {

                }

            }
tzvik15
  • 123
  • 1
  • 11
  • The first thing I think of when I hear "update a GUI Swing thread" is to use a SwingWorker as a background thread and override the `process()` method to update my GUI. https://docs.oracle.com/en/java/javase/16/docs/api/java.desktop/javax/swing/SwingWorker.html – markspace Apr 22 '22 at 21:00
  • 1
    You could use a swing timer, and at each tic, query the information from the cards. What do you mean by the "System crashed" can you be a bit more specific about your error. Are you calling "Thread.sleep" on the EDT and your GUI is locking up? – matt Apr 22 '22 at 21:36
  • [`SwingUtilities.invokeLater(...)`](https://docs.oracle.com/en/java/javase/17/docs/api/java.desktop/javax/swing/SwingUtilities.html#invokeLater(java.lang.Runnable)) is your friend. It's generally a _Bad Idea_ to call any Swing function from any thread other than the Event Dispatch Thread (EDT). `Swingutilities.invokeLater(()->{...})` can be called from any thread, and it posts a message to the EDT asking the EDT to perform the `...`. – Solomon Slow Apr 22 '22 at 22:24
  • @matt I am having some success with your suggestion. I have modified my GUI component so query the thread on a timer and it sort of works. The problem now is that the updated GUI element only gets added when I resize the JFrame window. (see code in edited main question) – tzvik15 Apr 22 '22 at 23:53

2 Answers2

4

First, start by taking a look at Concurrency in Swing. Important, Swing is NOT thread safe and you should never update the UI (or state it depends on) from outside the context of the Event Dispatching Thread

You have three basic options

Swing Timer

This provides you with a means to schedule a repeated callback on a regular interval, which is executed within the context of the Event Dispatching Thread

SwingWorker

This provides you with a wrapper class which can execute functionality on a background thread, but which can be used to pass information back to the Event Dispatching Thread for "processing"

EventQueue.invokeLater

This is basically provides away to execute functionality back on the Event Dispatching Thread (via a Runnable interface). While this is useful, it's difficult to pass information from one context to another without having to build it in, which should have you looking at SwingWorker instead


Push or poll?

The next question you need to ask is, do you want to "push" the updates or "poll" for them. This will effect which API approach you use to update the UI.

For example, if you want to poll the state, the a Swing Timer is a good choice. Updates can be performed when you want them to be. This would be a decent solution if there are a large number of updates, as you're less likely to overload the EDT, which can cause other issues.

If, instead, you wanted to push the updates (up from the car, probably via some kind of observer), then a SwingWorker "could" work, but it seems like a lot of overhead which could be more easily achieved with by just using EventQueue.invokeLater. If the car wasn't actually backed by it's own thread, then a SwingWorker would be VERY useful, but it would be acting, more or less, like a Swing Timer.

If, however, you only wanted to know when the car had crossed some kind of threshold (ie travelled another 10km), then I might be tempted to use a SwingWorker to poll the car's state and when the threshold is triggered, push the update to the UI via the publish/process workflow.

Having said that, SwingWorker is limited to only 10 active workers, so that's a consideration which you need to take into account.

Polling example (Swing Timer)

This example makes use of a "polling" approach, via a Swing Timer, the benefit of this is, the order of the cars doesn't changed (is controlled by the UI). While there are ways you could control this via JTable, this is pretty simple.

enter image description here

import java.awt.BorderLayout;
import java.awt.EventQueue;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.text.NumberFormat;
import java.time.Duration;
import java.time.Instant;
import java.util.ArrayList;
import java.util.List;
import java.util.Random;
import javax.swing.JFrame;
import javax.swing.JPanel;
import javax.swing.JScrollPane;
import javax.swing.JTextArea;
import javax.swing.Timer;

public class Main {
    public static void main(String[] args) {
        new Main();
    }

    public Main() {
        EventQueue.invokeLater(new Runnable() {
            @Override
            public void run() {
                JFrame frame = new JFrame();
                frame.add(new TestPane());
                frame.pack();
                frame.setLocationRelativeTo(null);
                frame.setVisible(true);
            }
        });
    }

    public class TestPane extends JPanel {

        private List<Car> cars = new ArrayList<>(32);
        private JTextArea ta = new JTextArea(10, 20);

        private Random rnd = new Random();

        private NumberFormat format = NumberFormat.getNumberInstance();

        public TestPane() {
            for (int index = 0; index < 32; index++) {
                Car car = new Car("Car " + index, 40.0 + (rnd.nextDouble() * 180.0));
                cars.add(car);
            }

            setLayout(new BorderLayout());
            add(new JScrollPane(ta));

            Timer timer = new Timer(1000, new ActionListener() {
                @Override
                public void actionPerformed(ActionEvent e) {
                    for (Car car : cars) {
                        ta.append(car.getName() + " @ " + format.format(car.getSpeedKPH()) + " ~ " + format.format(car.getLocation()) + "km\n");
                        if (!car.isRunning() && rnd.nextBoolean()) {
                            car.start();
                            ta.append(car.getName() + " got started\n");
                        }
                    }
                }
            });
            timer.start();
        }

    }

    public class Car {
        private String name;
        private double speedKPH;
        private Instant timeStartedAt;

        public Car(String name, double kmp) {
            this.speedKPH = kmp;
            this.name = name;
        }

        public String getName() {
            return name;
        }

        public double getSpeedKPH() {
            return speedKPH;
        }

        public Instant getTimeStartedAt() {
            return timeStartedAt;
        }

        public boolean isRunning() {
            return timeStartedAt != null;
        }

        public void start() {
            timeStartedAt = Instant.now();
        }

        protected double distanceTravelledByMillis(long millis) {
            double time = millis / 1000d / 60d / 60d;
            return getSpeedKPH() * time;
        }

        public double getLocation() {
            Instant timeStartedAt = getTimeStartedAt();
            if (timeStartedAt == null) {
                return 0;
            }
            Duration time = Duration.between(timeStartedAt, Instant.now());
            return distanceTravelledByMillis(time.toMillis());            
        }
    }
}

You also control the speed of the updates, which would allow you to scale the solution to a much larger number of cars (tens of thousands) before you might see a issue (although JTextArea would be the bottle neck before then)

Push (EventQueue.invokeLater)

This is a more random example, each car is given its own update interval, which triggers a callback via an observer. The observer then needs to sync the call back to the Event Dispatching Queue before the update can be added to the text area.

import java.awt.BorderLayout;
import java.awt.EventQueue;
import java.text.NumberFormat;
import java.time.Duration;
import java.time.Instant;
import java.util.ArrayList;
import java.util.List;
import java.util.Random;
import javax.swing.JFrame;
import javax.swing.JPanel;
import javax.swing.JScrollPane;
import javax.swing.JTextArea;

public class Main {
    public static void main(String[] args) {
        new Main();
    }

    public Main() {
        EventQueue.invokeLater(new Runnable() {
            @Override
            public void run() {
                JFrame frame = new JFrame();
                frame.add(new TestPane());
                frame.pack();
                frame.setLocationRelativeTo(null);
                frame.setVisible(true);
            }
        });
    }

    public class TestPane extends JPanel {

        private List<Car> cars = new ArrayList<>(32);
        private JTextArea ta = new JTextArea(10, 20);

        private Random rnd = new Random();
        private NumberFormat format = NumberFormat.getNumberInstance();

        public TestPane() {
            for (int index = 0; index < 32; index++) {
                int timeInterval = 500 + rnd.nextInt(4500);
                Car car = new Car("Car " + index, 40.0 + (rnd.nextDouble() * 180.0), timeInterval, new Car.Observer() {
                    @Override
                    public void didChangeCar(Car car) {
                        updateCar(car);
                    }
                });
                cars.add(car);
                car.start();
            }

            setLayout(new BorderLayout());
            add(new JScrollPane(ta));
        }

        protected void updateCar(Car car) {
            if (!EventQueue.isDispatchThread()) {
                EventQueue.invokeLater(new Runnable() {
                    @Override
                    public void run() {
                        updateCar(car);
                    }
                });
            }
            ta.append(car.getName() + " @ " + format.format(car.getSpeedKPH()) + " ~ " + format.format(car.getLocation()) + "km\n");
        }

    }

    public class Car {
        public interface Observer {
            public void didChangeCar(Car car);
        }

        private String name;
        private double speedKPH;
        private Instant timeStartedAt;

        private int notifyInterval;
        private Observer observer;

        private Thread thread;

        public Car(String name, double kmp, int notifyInterval, Observer observer) {
            this.speedKPH = kmp;
            this.name = name;
            this.notifyInterval = notifyInterval;
            this.observer = observer;
        }

        public String getName() {
            return name;
        }

        public double getSpeedKPH() {
            return speedKPH;
        }

        public Instant getTimeStartedAt() {
            return timeStartedAt;
        }

        public boolean isRunning() {
            return timeStartedAt != null;
        }

        public void start() {
            if (thread != null) {
                return;
            }
            timeStartedAt = Instant.now();
            thread = new Thread(new Runnable() {
                @Override
                public void run() {
                    try {
                        Thread.sleep(notifyInterval);
                        observer.didChangeCar(Car.this);
                    } catch (InterruptedException ex) {
                    }
                }
            });
            thread.start();
        }

        protected double distanceTravelledByMillis(long millis) {
            double time = millis / 1000d / 60d / 60d;
            return getSpeedKPH() * time;
        }

        public double getLocation() {
            Instant timeStartedAt = getTimeStartedAt();
            if (timeStartedAt == null) {
                return 0;
            }
            Duration time = Duration.between(timeStartedAt, Instant.now());
            return distanceTravelledByMillis(time.toMillis());
        }
    }
}

This places a large amount of overhead on the EDT, as each car needs to add a request on the EDT to be processed, so the more cars you add, the more the EDT will begin to slow down.

MadProgrammer
  • 343,457
  • 22
  • 230
  • 366
1

Consider constructing your code using the MCV model. This splits responsibilities between the Model, the View, and the Controller.
Each one (M, V, and C) becomes a well-defined single-responsibility class. At first, the number of classes, and the relations between them may look puzzling.
After studying and understanding the structure you realize that it actually divides the task on-hand into smaller and easier to handle parts.
My answer (and code) is base on the "push" option in MadProgramer's comprehensive answer:

import java.awt.*;
import java.text.*;
import java.time.*;
import java.util.*;
import java.util.List;
import javax.swing.*;

public class Main {
    public static void main(String[] args) {
        new CarsControler().startCars();
    }
}

class CarsControler implements Observer{

    private final Cars cars;
    private final CarGui gui;
    private final NumberFormat format = NumberFormat.getNumberInstance();
    private boolean stopCars = false;

    public CarsControler() {
        cars = new Cars(32);
        gui = new CarGui();
    }

    public void startCars(){

        Random rnd = new Random();
        gui.update("Strarting cars\n");

        for(Car car : cars.getCars()){
            if (! car.isRunning()) {
                car.start();
            }
            final int moveInterval = 2000 + rnd.nextInt(8000);
            Thread thread = new Thread(() -> {
                try {
                    while(! stopCars){
                        Thread.sleep(moveInterval);
                        carChanged(car);
                    }
                } catch (InterruptedException ex) {
                    ex.printStackTrace();
                }
            });
            thread.start();
        }
    }

    public void setStopCars(boolean stopCars) {
        this.stopCars = stopCars;
    }

    @Override
    public void carChanged(Car car) {
        String message = car.getName() + " @ " + format.format(car.getSpeedKPH()) + " ~ " + format.format(car.getLocation()) + "km\n";
        gui.update(message);
    }
}

/**
 * Model
 * TODO: add thread safety if model is used by multiple threads.
 */
class Cars {

    private final List<Car> cars;
    private final Random rnd = new Random();

    public Cars(int numberOfCars) {
        cars = new ArrayList<>(numberOfCars);

        for (int index = 0; index < 32; index++) {
            Car car = new Car("Car " + index, 40.0 + rnd.nextDouble() * 180.0);
            cars.add(car);
        }
    }

    //returns a defensive copy of cars
    public List<Car> getCars() {
        return new ArrayList<>(cars);
    }
}

class CarGui{

    private final JTextArea ta = new JTextArea(10, 20);

    public CarGui() {
        JFrame frame = new JFrame();
        JPanel testPane = new JPanel(new BorderLayout());
        testPane.add(new JScrollPane(ta));
        frame.add(testPane);
        frame.pack();
        frame.setLocationRelativeTo(null);
        frame.setVisible(true);
    }

    void update(String s) {
        if (!EventQueue.isDispatchThread()) {
            EventQueue.invokeLater(() -> update(s));
        } else {
            ta.append(s);
        }
    }
}

class Car {

    private final String name;
    private final double speedKPH;
    private Instant timeStartedAt;

    public Car(String name, double speedKPH) {
        this.speedKPH = speedKPH;
        this.name = name;
    }

    public String getName() {
        return name;
    }

    public double getSpeedKPH() {
        return speedKPH;
    }

    public Instant getTimeStartedAt() {
        return timeStartedAt;
    }

    public boolean isRunning() {
        return timeStartedAt != null;
    }

    public void start() {
        timeStartedAt = Instant.now();
    }

    protected double distanceTravelledByMillis(long millis) {
        double time = millis / 1000d / 60d / 60d;
        return getSpeedKPH() * time;
    }

    public double getLocation() {
        Instant timeStartedAt = getTimeStartedAt();
        if (timeStartedAt == null)      return 0;
        Duration time = Duration.between(timeStartedAt, Instant.now());
        return distanceTravelledByMillis(time.toMillis());
    }
}

interface Observer {
    void carChanged(Car car);
}
c0der
  • 18,467
  • 6
  • 33
  • 65