-1

I'm trying to write a code that will simulate sand pile, and I would like to open new window with simulation in another thread. In this case I have to dynamically change view of the 2nd window, but the view waits until the simulate() method ends even if it is run in another thread. What can I do to change view in SandPileApplicationSimulation:run() method body?

Controller class - simulation method:

public class SandPileController {

    @FXML
    public TextField interval;

    @FXML
    public TextField pathToBoard;

    @FXML
    public ChoiceBox<String> sandGenerationMode;

    @FXML
    protected void simulate() {

// you can ignore that 3 lines
        FieldRoleFileParser parser = new FieldRoleFileParser();
        FieldRole[][] fieldRoles = parser.parse(pathToBoard.getText());

        String value = sandGenerationMode.getValue();
       // this construtor is running initializeView method and it is creating new Stage
        SandPileApplicationSimulation sandPileApplicationSimulation = new SandPileApplicationSimulation(fieldRoles,
                Double.parseDouble(interval.getText()),
                SandGenerationMode.valueOf(value));
        Thread thread = new Thread(sandPileApplicationSimulation);
        thread.start();
    }
}

Stage view:

private void initializeView(FieldRole[][] values) {
        Stage stage = new Stage();

        GridPane root = new GridPane();

       //this loops generates some rectangles based on input
        for (int i = 0; i < columnCount; i++) {
            for (int j = 0; j < columnLength; j++) {
                FieldRole fieldRole = values[i][j];
                Rectangle square = new Rectangle(50, 50);
                square.setFill(fieldRole.getColor());
                root.add(square, i, j);
                board[i][j] = new Field(fieldRole, square);
            }
        }

        stage.setScene(new Scene(root));
        stage.show();
    }

run() method (from Runnable) in SandPileApplicationSimulation class - the method wherein I would like to change the view of scene:

@Override
    @SneakyThrows
    public void run() {

        boolean simulationMode = true;
        boolean hasAnyChange = true;

        generateSand(); // <-- this method change the color of some rectangles


        while (hasAnyChange) {

            hasAnyChange = false;

            Thread.sleep(interval);
            hasAnyChange = simulateFrame(simulationMode); // <-- this lane should change the view by changing the color of rectangles 

            simulationMode = !simulationMode;
            generateSand();
        }

    }

I tried to write only the important information; if I forget something, just tell me please. Thanks for your answers.

EDIT: Here is also main application class and fxml file (only main app has it) class:

@Override
    public void start(Stage stage) throws IOException {
        FXMLLoader fxmlLoader = new FXMLLoader(SandPileApplication.class.getResource("view.fxml"));
        Scene scene = new Scene(fxmlLoader.load(), 320, 175);
        stage.setTitle("Sand pile simulation");
        stage.resizableProperty()
                .setValue(false);
        stage.setScene(scene);
        stage.show();
    }

    public static void main(String[] args) {
        launch();
    }

fxml

<VBox alignment="CENTER" spacing="20.0" xmlns:fx="http://javafx.com/fxml"
      fx:controller="pl.umcs.sandpilesimulator.SandPileController">
    <padding>
        <Insets bottom="20.0" left="20.0" right="20.0" top="20.0"/>
    </padding>

    <TextField fx:id="interval">1.0</TextField>
    <TextField fx:id="pathToBoard" promptText="Path to file with board..."/>
    <ChoiceBox fx:id="sandGenerationMode">
        <items>
            <FXCollections fx:factory="observableArrayList">
                <String fx:value="CENTER" />
                <String fx:value="RANDOM" />
            </FXCollections>
        </items>
    </ChoiceBox>
    <Button text="Simulate" onAction="#simulate"/>
</VBox>
trashgod
  • 203,806
  • 29
  • 246
  • 1,045
Oskar
  • 379
  • 4
  • 21
  • [mcve] please .. and make sure you stick to the most important rule of concurrency in fx: __do not__ change any node in an active scenegraph off the fx application thread (as you seem to do in the snippet of the run method) – kleopatra Dec 18 '21 at 15:45
  • I'm not sure how your simulation evolves, but this [example](https://stackoverflow.com/a/44056730/230513) runs a `Task` to publish intermediate results on the FX application thread via `updateValue()`. – trashgod Dec 18 '21 at 19:07
  • 1
    As each iteration is computationally simple, an `AnimationTimer` like [this](https://stackoverflow.com/a/31761362/230513). might be simpler. – trashgod Dec 18 '21 at 19:29
  • Everything was working well by javaFx side, but runnable object was running in the same thread for my mistake and I fixed this (code is edited).. BTW why I should do not change any node in active scenegraph? What should I do insteed of this to change the view in another thread? – Oskar Dec 19 '21 at 15:17
  • Using AnimationTimer might be simpler, but I didnt know it before, thanks for your advice – Oskar Dec 19 '21 at 15:18

2 Answers2

0

Everything was working well by javaFx side, but runnable object was running in the same thread for my mistake and I fixed this (code is edited).

Oskar
  • 379
  • 4
  • 21
-1

This may help. It opens another window to display the ongoing simulation (https://en.wikipedia.org/wiki/Conway%27s_Game_of_Life):

import java.nio.IntBuffer;
import java.util.Random;
import javafx.application.Application;
import javafx.application.Platform;
import javafx.event.ActionEvent;
import javafx.geometry.Insets;
import javafx.geometry.Pos;
import javafx.scene.Group;
import javafx.scene.Scene;
import javafx.scene.control.Label;
import javafx.scene.control.ToggleButton;
import javafx.scene.control.ToggleGroup;
import javafx.scene.image.ImageView;
import javafx.scene.image.PixelFormat;
import javafx.scene.image.WritableImage;
import javafx.scene.image.WritablePixelFormat;
import javafx.scene.layout.HBox;
import javafx.scene.layout.VBox;
import javafx.scene.paint.Color;
import javafx.stage.Stage;
import javafx.stage.StageStyle;


public class MultiWindowDynamic extends Application {

    private static WritablePixelFormat<IntBuffer> pixFmt = PixelFormat.getIntArgbInstance();
    private int width = 200;
    private int height = 200;
    private ToggleButton startButton;
    private ToggleButton stopButton;
    private WritableImage simImage;
    private int [] pixelBuffer;
    private int [] nextBuffer;
    private Thread thread;
    private Stage simWindow;

    public static void main(String[] args) {
        launch(args);
    }

    @Override
    public void start(Stage primaryStage) throws Exception {
        startButton = new ToggleButton("Start");
        stopButton = new ToggleButton("Stop");
        startButton.disableProperty().bind(startButton.selectedProperty());
        stopButton.disableProperty().bind(stopButton.selectedProperty());
        ToggleGroup group = new ToggleGroup();
        group.getToggles().addAll(startButton, stopButton);
        stopButton.setSelected(true);
        startButton.setOnAction(this::startSimulation);
        stopButton.setOnAction(this::stopSimulation);
        HBox buttons = new HBox(16, startButton, stopButton);
        buttons.setAlignment(Pos.CENTER);
        VBox root = new VBox(8, new Label("Press the button to start the simulation."), buttons);
        root.setPadding(new Insets(20));
        Scene scene = new Scene(root);
        primaryStage.setScene(scene);
        primaryStage.show();
    }

    private void startSimulation(ActionEvent event) {
        if (simWindow == null) {
            nextBuffer = new int[width * height];
            simImage = new WritableImage(width, height);
            ImageView simImageView = new ImageView(simImage);
            Scene scene = new Scene(new Group(simImageView));
            scene.setFill(Color.BLACK);
            simWindow = new Stage(StageStyle.DECORATED);
            simWindow.setScene(scene);
        }
        simWindow.show();
        Random rng = new Random(System.currentTimeMillis());
        pixelBuffer = rng.ints(width * height, -1, 1).toArray();
        simImage.getPixelWriter().setPixels(0, 0, width, height, pixFmt, pixelBuffer, 0, width);
        thread = new Thread(this::simulationThread);
        thread.setDaemon(true);
        thread.start();
    }

    private void stopSimulation(ActionEvent event) {
        thread.interrupt();
    }

    private void simulationThread() {
        int [] buffer = calculateImage();
        try {
            while (!Thread.interrupted()) {
                nextBuffer = pixelBuffer;
                pixelBuffer = buffer;
                Platform.runLater(() -> {
                    simImage.getPixelWriter().setPixels(0, 0, width, height, pixFmt, pixelBuffer, 0, width);
                });
                buffer = calculateImage();
                Thread.sleep(100);
            }
        } catch (InterruptedException ex) {}
    }

    private int [] calculateImage() {
        for (int p = 0; p < nextBuffer.length; p++) {
            int count = countNeighbours(p);
            if (count == 3) { // birth/survive
                nextBuffer[p] = 0xffffffff;
            } else if (count < 2 || count > 3) { // death
                nextBuffer[p] = 0;
            } else { // stay
                nextBuffer[p] = pixelBuffer[p];
            }
        }
        return nextBuffer;
    }

    private int countNeighbours(int p) {
        return getPixel(p-width-1)
             + getPixel(p-width)
             + getPixel(p-width+1)
             + getPixel(p-1)
             + getPixel(p+1)
             + getPixel(p+width-1)
             + getPixel(p+width)
             + getPixel(p+width+1);
    }

    // returns 1 for a 'set' pixel, and 0 for 'blank'
    // takes advantage of white pixel int being -1;
    private int getPixel(int p) {
        if (p < 0) {
            p += pixelBuffer.length;
        } else if (p >= pixelBuffer.length) {
            p -= pixelBuffer.length;
        }
        return -pixelBuffer[p];
    }

}
swpalmer
  • 3,890
  • 2
  • 23
  • 31
  • What is the adventage of using Platform.runLater isnteed of doing another activities in another thread? – Oskar Dec 19 '21 at 15:20
  • I believe you MUST use Platform.runLater in this situation as the image is part of an active scene and therefore must be updated on the JavaFX platform thread. There are some exceptions to this, but I saw nothing in the JavaFX documentation that indicated this was one of them. – swpalmer Dec 19 '21 at 17:42
  • Note that this solution is running the simulation in another thread, as it should. Only UI updates run on the JavaFX thread, as they must. – swpalmer Dec 19 '21 at 17:47
  • I tried something like this before but Platform.runLater(callable) wasnt working well, cuz despite that was in another thread it has to end work and then runLater changed the view. short example (pseudocode): public void doAnyChange() { platform.runLater(() -> changeTheView()) thread.sleep(10000) } and that method changed anything when the method doAnyChange() ends work – Oskar Dec 19 '21 at 18:27
  • @Oskar I'm not sure what you mean... the ```doAnyChange``` method does not have to end before ```changeTheView``` runs. You can see from my example that the ```simulationThread``` method does not end while the view is updating. – swpalmer Dec 19 '21 at 18:37