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.
You create a new
gui
control with a newpane
each time yourcircleOnMouseDraggedEventHandler
method is invoked. This means that the callcontroller.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:
In MyMouseEvents:
In MouseGestures:
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 inSpline#draw
:Result: