JavaFX:在窗格上拖动节点时动态更新(重新计算+重绘)折线

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

我有一个 GUI,它在至少三个绘制样条曲线的节点之间进行插值。左键单击窗格会导致在单击的位置设置一个新的(可拖动的)节点。如果至少存在三个节点,则按下按钮 "Draw Spline" 将在节点之间绘制插值样条曲线。一切正常,除了 当其中一个节点被向上或向下拖动时,样条曲线不会动态重绘 。相反,在我移动一个节点后,我需要按下按钮 "Draw Spline",新的 Spline 就会出现。但我想看到节点立即移动位置的效果,而不是总是按下按钮。我在 NetBeans 8.1 中开发了这个 JavaFX 项目。

这是我的代码:

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();
            }
        }
    };
}

重新计算和重绘操作的​​实现方式是通过 MouseGestures.circleOnMouseDraggedEventHandler 访问 Controller.handleButtonDrawSpline 方法。但是这样做 pane.getChildren() 是空的(它至少应该包含已经添加到 panenode 对象),而当我点击 "Draw Spline" 时它不是空的按钮。两次执行 handleButtonDrawSpline 方法,但 pane 对象不同。我有 "feeling" 问题出在某处。任何帮助是极大的赞赏。

每次调用 circleOnMouseDraggedEventHandler 方法时,您都会创建一个带有新 pane 的新 gui 控件。这意味着调用 controller.handleButtonDrawSpline() 每次都会绘制一个新的窗格实例。

避免这种情况的最简单方法是将控制器作为参数传递。

在控制器中:

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

在 MyMouseEvents 中:

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

在鼠标手势中:

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();

    }
};

因为 draw 方法现在正在 pane 的同一个实例上绘制,所以您必须避免在 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);
}

结果: