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) {
}
}
CodePudding user response:
First, start by taking a look at
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.
CodePudding user response:
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);
}