JavaFX: Dynamically update (recalculate+redraw) Polyline when a node is dragged on a pane

1.9k views Asked by At

I have a GUI that interpolates between at least three nodes drawing a Spline. A left click on the pane leads to setting a new (draggable) node at the clicked position. If at least three nodes are present a button push "Draw Spline" will draw the interpolated Spline between the nodes. Everything works just fine except for that the Spline is not redrawn dynamically when one of the nodes is dragged up or down. Instead, after I moved a node I need to push the button "Draw Spline" and the new Spline appears. But I want to see the effect of a node shifted in its position immediately and not always hit the button. I developed this JavaFX project in NetBeans 8.1.

Here is my code:

Main.java

package InterpolationMinimal;

import javafx.application.Application;
import javafx.fxml.FXMLLoader;
import javafx.scene.Scene;
import javafx.scene.layout.AnchorPane;
import javafx.stage.Stage;

public class Main extends Application {

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

    @Override
    public void start(Stage stage) throws Exception {
        AnchorPane root = FXMLLoader.load(getClass().getResource("gui.fxml"));
        Scene scene = new Scene(root);

        stage.setTitle("Interpolation using cubic splines");
        stage.setScene(scene);
        stage.show();

        // this is needed to resize object if window/scene is resized
        root.prefWidthProperty().bind(scene.widthProperty());
        root.prefHeightProperty().bind(scene.heightProperty());
    }
}

Controller.java

package InterpolationMinimal;

//import DraggableNodesGraph.MyAlerts;
import java.net.URL;
import java.util.ResourceBundle;
import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
import javafx.event.ActionEvent;
import javafx.fxml.FXML;
import javafx.fxml.Initializable;
import javafx.scene.control.ChoiceBox;
import javafx.scene.control.Separator;
import javafx.scene.layout.AnchorPane;
import javafx.scene.layout.Pane;

public class Controller implements Initializable {

    @FXML private AnchorPane anchorPane;
    @FXML private Pane pane;
    @FXML private Separator separator;
    @FXML private ChoiceBox choseBoundaryConditions;
    @FXML private ChoiceBox choseSolverTechnique;

    private ObservableList boundaryConditionsToChose; //!< List of boundary condtions type
    private ObservableList solvingTechniqueToChose; //!< List of techniques for solving a linear system of equations
    public static Spline mySpline = new Spline();   

    @Override 
    public void initialize(URL url, ResourceBundle rb) {
        // Set choices, default and event handling for the boundary conditions ChoiceBox
        boundaryConditionsToChose = FXCollections.observableArrayList();
        boundaryConditionsToChose.add("Natural");
        boundaryConditionsToChose.add("Periodic");
        choseBoundaryConditions.setItems(boundaryConditionsToChose);
        choseBoundaryConditions.setValue(boundaryConditionsToChose.get(0));

        // Set choices, default and event handling for the type of solving technique ChoiceBox
        solvingTechniqueToChose = FXCollections.observableArrayList();
        solvingTechniqueToChose.add("Jacobi");
        solvingTechniqueToChose.add("Gauss-Seidel");
        choseSolverTechnique.setItems(solvingTechniqueToChose);
        choseSolverTechnique.setValue(solvingTechniqueToChose.get(0));

        // Add listeners to the window size and redraw in case the window's size is changed.
        // Subtract canvas.layout* to make sure the center of drawing is in the center
        // of the canvas and not in the center of the whole window.
        anchorPane.prefWidthProperty().addListener((ov, oldValue, newValue) -> {
            pane.setPrefWidth(newValue.doubleValue() - pane.layoutXProperty().doubleValue());
            rescaleObjects('x', oldValue.doubleValue(), newValue.doubleValue());
        });
        anchorPane.prefHeightProperty().addListener((ov, oldValue, newValue) -> {
            pane.setPrefHeight(newValue.doubleValue() - pane.layoutYProperty().doubleValue());
            separator.setPrefHeight(pane.getPrefHeight());
            rescaleObjects('y', oldValue.doubleValue(), newValue.doubleValue());
        });

        MyMouseEvents.paneMouseEvents(pane);
    }

    @FXML public void handleButtonDrawSpline() {
        if ( mySpline.getNodeList().size() >= 3 ) {
            mySpline.calculate(String.valueOf(choseBoundaryConditions.getValue()),
                String.valueOf(choseSolverTechnique.getValue()));
            mySpline.draw(pane);
        } /*else {
            MyAlerts.displayAlert("You need at least 3 points for the interpolation!");
        }*/
    }

    @FXML private void handleButtonClearCanvas(ActionEvent event) {
        pane.getChildren().clear();
        mySpline.getNodeList().clear();
    }

    private void rescaleObjects (char xOrY, double oldVal, double newVal) {
        double scaleFactor = newVal / oldVal;
        for (Node node: mySpline.getNodeList()) {
            if ( xOrY == 'x' ) {
                node.setX(node.getX()*scaleFactor);
                node.relocate(node.getX(), node.getY());
            } else if ( xOrY == 'y' ) {
                node.setY(node.getY()*scaleFactor);
                node.relocate(node.getX(), node.getY());
            }
        }
    }
}

gui.fxml

<?xml version="1.0" encoding="UTF-8"?>

<?import javafx.scene.canvas.*?>
<?import javafx.geometry.*?>
<?import javafx.scene.text.*?>
<?import java.lang.*?>
<?import java.util.*?>
<?import javafx.scene.*?>
<?import javafx.scene.control.*?>
<?import javafx.scene.layout.*?>

<AnchorPane fx:id="anchorPane" prefHeight="700.0" prefWidth="1000.0" xmlns="http://javafx.com/javafx/8" xmlns:fx="http://javafx.com/fxml/1" fx:controller="InterpolationMinimal.Controller">
   <children>
      <HBox alignment="CENTER_LEFT" layoutX="0.0" prefHeight="40.0" prefWidth="250.0">
         <children>
            <ChoiceBox fx:id="choseBoundaryConditions" prefWidth="210.0">
               <HBox.margin>
                  <Insets left="10.0" />
               </HBox.margin>
            </ChoiceBox>
         </children>
      </HBox>
      <HBox alignment="CENTER_LEFT" layoutX="0.0" layoutY="40.0" prefHeight="40.0" prefWidth="250.0">
         <children>
            <ChoiceBox fx:id="choseSolverTechnique" prefWidth="210.0">
               <HBox.margin>
                  <Insets left="10.0" />
               </HBox.margin>
            </ChoiceBox>
         </children>
      </HBox>
      <HBox alignment="CENTER_LEFT" layoutY="80.0" prefHeight="40.0" prefWidth="250.0">
         <children>
                  <HBox alignment="CENTER_LEFT" prefHeight="40.0" prefWidth="250.0">
                     <children>
                        <Button fx:id="drawSpline" mnemonicParsing="false" onAction="#handleButtonDrawSpline" text="Draw Spline">
                           <HBox.margin>
                              <Insets left="10.0" />
                           </HBox.margin>
                           <font>
                              <Font size="15.0" />
                           </font>
                        </Button>
                     </children>
                  </HBox>
         </children>
      </HBox>
      <HBox alignment="CENTER_LEFT" layoutY="120.0" prefHeight="40.0" prefWidth="250.0">
         <children>
                  <HBox alignment="CENTER_LEFT" prefHeight="40.0" prefWidth="250.0">
                     <children>
                        <Button fx:id="clearCanvas" mnemonicParsing="false" onAction="#handleButtonClearCanvas" text="Clear Canvas">
                           <font>
                              <Font size="15.0" />
                           </font>
                           <HBox.margin>
                              <Insets left="10.0" />
                           </HBox.margin>
                        </Button>
                     </children>
                  </HBox>
         </children>
      </HBox>
      <Pane fx:id="pane" layoutX="250.0" prefHeight="700.0" prefWidth="750.0"/>
      <Separator fx:id="separator" layoutX="250.0" layoutY="0.0" orientation="VERTICAL" prefHeight="700.0" />
   </children>
</AnchorPane>

Node.java

package InterpolationMinimal;

import javafx.scene.shape.Circle;

public class Node extends Circle  implements Comparable<Node> {

    private double x;
    private double y;

    public Node () {

    } 

    public Node (double x, double y) 
    {
        this.x = x;
        this.y = y;
        // also initialize the Circle properties:
        this.setCenterX(x);
        this.setCenterY(y);
    }

    @Override
    public int compareTo(Node n) {
        return this.x<n.getX()?-1:this.x>n.getX()?1:0;
    }

    public double getX () 
    {
        return this.x;
    }

    public double getY () 
    {
        return this.y;
    }

    public void setX (double x) 
    {
        this.x = x;
    }

    public void setY (double y) 
    {
        this.y = y;
    }
}

Spline.java

package InterpolationMinimal;

import java.util.ArrayList;
import java.util.Collections;
import java.util.Objects;
import javafx.scene.shape.Polyline;
import javafx.scene.layout.Pane;

public class Spline {

    private ArrayList<Node> nodeList = new ArrayList<>();
    private Polyline polyline = new Polyline();
    private double[][] coefficients;

    public ArrayList<Node> getNodeList()
    {
        return this.nodeList;
    }

    public void calculate (String boundaryConditions, String solverTechnique) 
    {
        int numberOfNodes = this.nodeList.size();
        int n = numberOfNodes - 1; // = number of splines
        this.coefficients = new double[n][4];

        Collections.sort(this.nodeList);

        // System of equations to solve: Mc = m

        // Fill the matrix M:
        double[][] M = new double[n-1][n-1];
        int i, j;
        double h, hPlus1;

        for (i=0; i<n-1; i++) {
            j = i;

            h = this.nodeList.get(i+1).getX() - this.nodeList.get(i).getX();
            hPlus1 = this.nodeList.get(i+2).getX() - this.nodeList.get(i+1).getX();

            M[i][j] = 2 * ( h + hPlus1 ); // Diagonale der Matrix mit i=j
            if ( j > 0   ) M[i][j-1] = h;
            if ( j < n-2 ) M[i][j+1] = hPlus1;
        }

        // Fill m
        double[] m = new double[n-1];
        for (i=0; i<n-1; i++) {

            h      = this.nodeList.get(i+1).getX() - this.nodeList.get(i  ).getX();
            hPlus1 = this.nodeList.get(i+2).getX() - this.nodeList.get(i+1).getX();

            m[i] = 3 * ( this.nodeList.get(i+2).getY() - this.nodeList.get(i+1).getY() ) / hPlus1 -
                   3 * ( this.nodeList.get(i+1).getY() - this.nodeList.get(i  ).getY() ) / h;

            // Use the boundary conditions
            if ( i==0 ) {
                m[i] -= h * 0.0;
            } else if ( i==n-2 ) {
                m[i] -= h * 0.0;
            }
        }

        // Iterative Lösung für c
        double[] cTemp = new double[n-1];
        double tolerance = 0.0001;
        if ( Objects.equals(solverTechnique,"Jacobi") ) {
            cTemp = jacobiVerfahren(M, m, tolerance);
        } /*else if ( Objects.equals(solverTechnique,"Gauss-Seidel") ) { 
            cTemp = Matrix.gaussSeidelVerfahren(M, m, tolerance);
        }*/

        double[] c = new double[n];
        c[0] = 0.0; // As in the left boundary condition
        for (i=1; i<n; i++) {
            c[i] = cTemp[i-1];
        }

        // Determine the other three coefficients
        double[] a = new double[n];
        double[] b = new double[n];
        double[] d = new double[n];
        for (i=0; i<n; i++) {
            a[i] = this.nodeList.get(i).getY();
            h = this.nodeList.get(i+1).getX() - this.nodeList.get(i).getX();
            if ( i < n-1 ) {
                b[i] = ((this.nodeList.get(i+1).getY()-this.nodeList.get(i).getY()) / h) -
                    h * (2*c[i] + c[i+1] ) / 3.0;
                d[i] = (c[i+1] - c[i]) / (3.0 * h);
            } else {
                // c[i+1] = 0.0 wegen der natürlichen Randbedingungen
                d[i] = (0.0 - c[i]) / (3.0 * h);
                b[i] = ((this.nodeList.get(i+1).getY()-this.nodeList.get(i).getY()) / h) -
                    h * (2*c[i] + 0.0 ) / 3.0;
            }
        }

        for (i=0; i<n; i++) {
            this.coefficients[i][0] = a[i];
            this.coefficients[i][1] = b[i];
            this.coefficients[i][2] = c[i];
            this.coefficients[i][3] = d[i];
        }
    }

    /**
    * Solve a linear system of equations (LSE) Ax=b 
    * @param A Matrix giving the coefficients of the LSE
    * @param b Right-hand side of the equations
    * @return Solution vector x of the LSE
    */
    public static double[] jacobiVerfahren(double[][] A, double[] b, double tolerance) 
    {
        int i, j;
        int n = A.length; // A.lenght = A[0].length, da quadratisch
        double[] x = new double[n];
        double startwert = 0.0; // willkürlicher Startwert
        double summe;
        double[] xOld = new double[n];
        for (i=0; i<n; i++) {
            xOld[i] = startwert;
        }
        double[] genauigkeit = new double[n];
        int howManyTimesUntilSmallerEpsilon = 0;
        int maxIterations = 100;
        boolean genauigkeitErreicht = false;

        while ( ! genauigkeitErreicht ) {
            howManyTimesUntilSmallerEpsilon++;
            if ( howManyTimesUntilSmallerEpsilon > maxIterations ) break;

            for (i=0; i<n; i++) {
                summe = 0.0;
                for (j=0; j<n; j++) {
                    if ( j != i ) {
                        summe += A[i][j] * xOld[j];
                    }
                }
                x[i] = ( b[i] - summe ) / A[i][i];
            }
            for (i=0; i<n; i++) {
                genauigkeit[i] = Math.abs(x[i] - xOld[i]);
            }
            // Die Genauigkeit von epsilon muss für jedes Element erreicht sein, so dass
            // bereits ein Element, auf das das nicht zutrifft, ausreicht, um die Iteration
            // weiter zu führen.
            for (i=0; i<n; i++) {
                if ( genauigkeit[i] > tolerance ) {
                    genauigkeitErreicht = false;
                    break;
                } else {
                    genauigkeitErreicht = true;
                }
            }
            for (i=0; i<n; i++) {
                xOld[i] = x[i];
            }
        }
        return x;
    }

    public void draw (Pane pane)
    {
        int i, j, s;
        int n = this.nodeList.size();
        double oneStep;
        double nSteps = 12.0;
        double x, y, a, b, c, d;
        double xDifference;

        polyline.getPoints().clear();
        for (i=0; i<n-1; i++) {
            // Calculate the increment by means of the distance between two neighboring points
            oneStep = Math.abs(this.nodeList.get(i).getX() - this.nodeList.get(i+1).getX()) / nSteps;

            a = this.coefficients[i][0];
            b = this.coefficients[i][1];
            c = this.coefficients[i][2];
            d = this.coefficients[i][3];

            for (s=0; s<nSteps; s++) {
                x = this.nodeList.get(i).getX() + s * oneStep;
                // to calculate y we need the formula of a third-order cubic spline of the form
                // y(x) = a(i) + b(i)(x-x(i))^1 + c(i)(x-x(i))^2 + d(i)(x-x(i))^3
                xDifference = x - this.nodeList.get(i).getX();
                y = a + (b*xDifference) + (c*Math.pow(xDifference,2.0)) + (d*Math.pow(xDifference,3.0));
                polyline.getPoints().addAll(x, y);
            }
        }
        polyline.getPoints().addAll(this.nodeList.get(n-1).getX(), this.nodeList.get(n-1).getY());
        polyline.toBack();
        pane.getChildren().add(polyline);
    }

}

MyMouseEvents.java

package InterpolationMinimal;

import javafx.event.EventHandler;
import javafx.scene.input.MouseEvent;
import javafx.scene.layout.Pane;

public class MyMouseEvents {

    public static void paneMouseEvents(Pane pane) {

        MouseGestures mg = new MouseGestures();

        pane.addEventHandler(MouseEvent.MOUSE_CLICKED, new EventHandler<MouseEvent>() {
            @Override
            public void handle(MouseEvent mouseEvent) {

                if (mouseEvent.getTarget() == pane) {
                    Node node = new Node(mouseEvent.getSceneX()-pane.getLayoutX(), mouseEvent.getSceneY());
                    node.setRadius(10.0);
                    mg.makeDraggable(node);
                    Controller.mySpline.getNodeList().add(node);
                    pane.getChildren().add(node);
                }
                mouseEvent.consume();
            }
        });
    }
}

MouseGestures.java

package InterpolationMinimal;

import javafx.event.EventHandler;
import javafx.fxml.FXMLLoader;
import javafx.scene.input.MouseEvent;

public class MouseGestures {

    double orgSceneX, orgSceneY;
    double orgTranslateX, orgTranslateY;

    public void makeDraggable(Node node) {
        node.setOnMousePressed(circleOnMousePressedEventHandler);
        node.setOnMouseDragged(circleOnMouseDraggedEventHandler);
    }

    EventHandler<MouseEvent> circleOnMousePressedEventHandler = new EventHandler<MouseEvent>() {
        @Override
        public void handle(MouseEvent me) {
            //orgSceneX = me.getSceneX();
            orgSceneY = me.getSceneY();
            Node p = (Node) me.getSource();
            //orgTranslateX = p.getCenterX();
            orgTranslateY = p.getCenterY();
        }
    };

    EventHandler<MouseEvent> circleOnMouseDraggedEventHandler = new EventHandler<MouseEvent>() {
        @Override
        public void handle(MouseEvent me) {
            //double offsetX = me.getSceneX() - orgSceneX;
            double offsetY = me.getSceneY() - orgSceneY;
            //double newTranslateX = orgTranslateX + offsetX;
            double newTranslateY = orgTranslateY + offsetY;

            Node p = (Node) me.getSource();
            System.out.println();
            //p.setCenterX(newTranslateX);
            p.setCenterY(newTranslateY);
            //p.setX(newTranslateX);
            p.setY(newTranslateY);

            try {
                FXMLLoader loader = new FXMLLoader(getClass().getResource("gui.fxml"));
                loader.load();
                Controller controller = loader.getController();
                controller.handleButtonDrawSpline();
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
    };
}

The recalculate and redraw action is realized in a way that via MouseGestures.circleOnMouseDraggedEventHandler the Controller.handleButtonDrawSpline method is accessed. But in doing so pane.getChildren() is empty (it should at least contain the node objects that were already added to the pane) while it is not empty when I hit the "Draw Spline" button. Both times the handleButtonDrawSpline method is executed but the pane object is different. I have the "feeling" that the problem lies somewhere there. Any help is greatly appreciated.

1

There are 1 answers

2
Modus Tollens On BEST ANSWER

You create a new gui control with a new pane each time your circleOnMouseDraggedEventHandler method is invoked. This means that the call controller.handleButtonDrawSpline() will draw on a new pane instance each time.

The simplest way to avoid this would be to pass the controller as argument.

In Controller:

@Override 
public void initialize(URL url, ResourceBundle rb) {
    [...]
    MyMouseEvents.paneMouseEvents(pane, this);
}

In MyMouseEvents:

public static void paneMouseEvents(Pane pane, Controller controller) {
    MouseGestures mg = new MouseGestures(controller);
    [...]
}

In MouseGestures:

private final Controller controller;

public MouseGestures(Controller controller) {
    this.controller = controller;
}

[...]

EventHandler<MouseEvent> circleOnMouseDraggedEventHandler = new EventHandler<MouseEvent>() {
    @Override
    public void handle(MouseEvent me) {
        // double offsetX = me.getSceneX() - orgSceneX;
        double offsetY = me.getSceneY() - orgSceneY;
        // double newTranslateX = orgTranslateX + offsetX;
        double newTranslateY = orgTranslateY + offsetY;

        Node p = (Node) me.getSource();
        System.out.println();
        // p.setCenterX(newTranslateX);
        p.setCenterY(newTranslateY);
        // p.setX(newTranslateX);
        p.setY(newTranslateY);

        controller.handleButtonDrawSpline();

    }
};

Because the draw method is drawing on the same instance of pane now, you have to avoid adding the polyline more than once to the pane in Spline#draw:

polyline.getPoints().addAll(this.nodeList.get(n - 1).getX(), this.nodeList.get(n - 1).getY());
polyline.toBack();
if (!pane.getChildren().contains(polyline)) {
    pane.getChildren().add(polyline);
}

Result:

animated gif