JavaFX: динамическое обновление (пересчет+перерисовка) полилинии при перетаскивании узла на панель.

У меня есть графический интерфейс, который интерполирует по крайней мере между тремя узлами, рисующими сплайн. Щелчок левой кнопкой мыши на панели приводит к установке нового (перетаскиваемого) узла в месте щелчка. Если присутствуют как минимум три узла, нажатие кнопки «Нарисовать сплайн» нарисует интерполированный сплайн между узлами. Все работает отлично, за исключением того, что сплайн не перерисовывается динамически, когда один из узлов перетаскивается вверх или вниз. Вместо этого после того, как я переместил узел, мне нужно нажать кнопку «Нарисовать сплайн», и появится новый сплайн. Но я хочу видеть эффект смещения узла сразу, а не всегда нажимать кнопку. Я разработал этот проект JavaFX в NetBeans 8.1.

Вот мой код:

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

Контроллер.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;
    }
}

Сплайн.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() пуст (он должен по крайней мере содержать объекты node, которые уже были добавлены в pane), в то время как он не пуст, когда я нажимаю кнопку «Нарисовать сплайн». Оба раза выполняется метод handleButtonDrawSpline, но объект pane отличается. У меня такое "чувство", что проблема где-то там. Любая помощь приветствуется.


person 732E772E    schedule 09.01.2017    source источник


Ответы (1)


Вы создаете новый элемент управления gui с новым pane каждый раз, когда вызывается ваш метод circleOnMouseDraggedEventHandler. Это означает, что вызов 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);
    [...]
}

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

    }
};

Поскольку метод рисования теперь рисует один и тот же экземпляр 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);
}

Результат:

анимированный gif

person Modus Tollens    schedule 09.01.2017
comment
Большое спасибо @Modus Tollens! - person 732E772E; 12.01.2017