0

I'm trying to periodically update a Google Maps marker in FXML. I tried to do this with a Timer and a new Thread, but can't get any of it to work.

I tested the new Thread with the simple task to update a TextField in my UI, which works just fine.

However, when I use the actual code that I need to update the map:

@FXML
public void handleTracking() throws IOException, InterruptedException {
    new Thread() {
        @Override
        public void run() {
            while (true) {
                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);");
                    try {
                        Thread.sleep(555);
                    } catch (InterruptedException ex) {
                        Logger.getLogger(FXMLDocumentController.class.getName()).log(Level.SEVERE, null, ex);
                    }
                } catch (IOException ex) {
                    Logger.getLogger(FXMLDocumentController.class.getName()).log(Level.SEVERE, null, ex);
                }
            }
        }
    }.start();  
}

I get the Output message:

Exception in thread "Thread-26" java.lang.IllegalStateException: Not on FX application thread; currentThread = Thread-26
    at com.sun.javafx.tk.Toolkit.checkFxUserThread(Toolkit.java:236)
    at com.sun.javafx.tk.quantum.QuantumToolkit.checkFxUserThread(QuantumToolkit.java:423)
    at javafx.scene.web.WebEngine.checkThread(WebEngine.java:1216)
    at javafx.scene.web.WebEngine.executeScript(WebEngine.java:980)
    at de.fkfs.v2x.eval.FXMLDocumentController$1.run(FXMLDocumentController.java:84=)

A similar thing happens when I use a Timer, it works for the task of updating a label, however if I try to update the marker position it throws the message:

Exception in thread "Timer-0" java.lang.IllegalStateException: Not on FX application thread; currentThread = Timer-0
fabian
  • 80,457
  • 12
  • 86
  • 114
Evo
  • 13
  • 6
  • 2
    Possible duplicate of [java.lang.IllegalStateException: Not on FX application thread; currentThread = Thread-4](http://stackoverflow.com/questions/29449297/java-lang-illegalstateexception-not-on-fx-application-thread-currentthread-t) – DVarga Jul 28 '16 at 08:58
  • 1
    You can find a example of updating the UI from a different thread here: http://stackoverflow.com/documentation/javafx/2230/threading/7291/updating-the-ui-using-platform-runlater#t=201607280912032640534 – fabian Jul 28 '16 at 09:13

2 Answers2

1

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.

James_D
  • 201,275
  • 16
  • 291
  • 322
  • thank you so much James_D! Both techniques work. You are right, I probably want to get new GPS-data from my log-files the whole time. I will have to go through your code a couple of times to completely understand it though. Thank you, I really appreciate it! – Evo Jul 28 '16 at 14:26
0

All JavaFX UI elements must be updated inside the FX application thread!

If using an additional thread be sure to use platform.Runlater() to update your UI elements!

Luiz Agner
  • 77
  • 10
Eudy Contreras
  • 396
  • 3
  • 10
  • When I wrap the code to be performed in Platform.runLater, the program is running, however it gets so slow that for example the map isn't scrollable anymore. – Evo Jul 28 '16 at 09:38
  • @Evo why dont you use a timeline instead! It would be a lot easier! – Eudy Contreras Jul 28 '16 at 09:46
  • @Evo make sure your whileloop is wrapped around the platform runlater method! I meant! – Eudy Contreras Jul 28 '16 at 09:53
  • yes the Platform.runLater comes right after the while loop, but still it makes the whole program slow. I will try to do it with a Timeline, however I had problems to understand the examples I already found about this topic. – Evo Jul 28 '16 at 10:12
  • @Evo somehow more objects are being created than what the application can handle! Hope you get to the bottom of it.. Best of luck! – Eudy Contreras Jul 28 '16 at 10:17
  • [This question](http://stackoverflow.com/questions/23488280/throttling-javafx-gui-updates) shows you how to limit the number of updates that are sent to the FX Application Thread using `Platform.runLater(...)`. You could use that technique here to prevent flooding the UI thread. However, an [`AnimationTimer`](http://docs.oracle.com/javase/8/javafx/api/javafx/animation/AnimationTimer.html) is probably simpler and more appropriate in this case. – James_D Jul 28 '16 at 12:24