Updates to the UI, including calls to webEngine.executeScript(...)
must be executed on the FX Application thread.
On the other hand, the FX Application Thread is (effectively) the thread used for rendering the UI and processing user input. So if you block this thread with an infinite loop, or other long running process, or if you schedule too many things to run on that thread, you will make the UI unresponsive.
What you are trying to do in your code appears to be to update the UI as fast as you can. If you put the loop in the FX Application Thread you will block it entirely: if you put it on a background thread and schedule the updates using Platform.runLater(...)
you will flood the FX Application Thread with too many updates and prevent it from doing its usual work, and it will become unresponsive.
The general solution here revolves around the fact that it's really redundant to update the UI so often. The human eye can only detect visible changes at a limited rate, and in technology terms you are limited by, e.g. the refresh rate of the physical screen and of the underlying graphical software. JavaFX attempts to update the UI at no more than 60Hz (in the current implementation). So there's really no point in updating more often than the underlying JavaFX toolkit updates the scene.
The AnimationTimer
provides a handle
method that is guaranteed to be invoked once per scene update, no matter how frequently that occurs. AnimationTimer.handle(...)
is invoked on the FX Application Thread, so you can safely make changes to the UI here. So you could implement your tracking with:
private AnimationTimer tracker ;
public void initialize() {
tracker = new AnimationTimer() {
@Override
public void handle(long timestamp) {
try {
double ar[] = FileImport.getGpsPosition();
// System.out.println("Latitude: " + ar[0] + " Longitude: " + ar[1]);
double Ltd = ar[0];
double Lng = ar[1];
webEngine.executeScript(""
+ "window.lat = " + Ltd + ";"
+ "window.lon = " + Lng + ";"
+ "document.goToLocation(window.lat, window.lon);");
} catch (IOException ex) {
Logger.getLogger(FXMLDocumentController.class.getName()).log(Level.SEVERE, null, ex);
}
}
};
}
@FXML
public void handleTracking() {
tracker.start();
}
The only thing to be wary of here is that, because handle()
is invoked on the FX Application Thread, you should not perform any long-running code here. It looks as though your FileImport.getGpsPosition()
method performs some IO operations, so it should probably be delegated to a background thread. The trick here, which is the one used by JavaFX classes such as Task
, is to continually update a value from a background thread, and only schedule a call to Platform.runLater(...)
if one is not already pending.
First, just define a simple class for representing the location (make it immutable so it is thread-safe):
class Location {
private final double longitude ;
private final double latitude ;
public Location(double longitude, double latitude) {
this.longitude = longitude ;
this.latitude = latitude ;
}
public double getLongitude() {
return longitude ;
}
public double getLatitude() {
return latitude ;
}
}
and now:
@FXML
private void handleTracking() {
AtomicReference<Location> location = new AtomicReference<>(null);
Thread thread = new Thread(() -> {
try {
while (true) {
double[] ar[] = FileImport.getGpsPosition();
Location loc = new Location(ar[0], ar[1]);
if (location.getAndSet(loc) == null) {
Platform.runLater(() -> {
Location updateLoc = location.getAndSet(null);
webEngine.executeScript(""
+ "window.lat = " + updateLoc.getLatitude() + ";"
+ "window.lon = " + updateLoc.getLongitude() + ";"
+ "document.goToLocation(window.lat, window.lon);");
});
}
}
} catch (IOException exc) {
Logger.getLogger(FXMLDocumentController.class.getName()).log(Level.SEVERE, null, ex);
}
});
thread.setDaemon(true);
thread.start();
}
The way this works is that it creates a (thread-safe) holder for the current location, and updates it as fast as possible. When it updates it, it (atomically) also checks if the current value is null
. If it's null
, it schedules a UI update via Platform.runLater()
. If not, it simply updates the value but schedules no new UI update.
The UI update (atomically) gets the current (i.e. most recent) value and sets it to null, indicating it is ready to receive a new UI update. It then processes the new update.
This way you "throttle" the UI updates so that new ones are only scheduled when the current one is being processed, avoiding flooding the UI thread with too many requests.