-2

I'm a complete novice to JavaFX, and fairly new to Java overall. I'm designing a graphical representation of an undirected graph for use in a self-teaching project. Right now, I'm trying to make nodes draggable such that the edges will stretch to stay connected with their nodes. I have achieved that in the case of 2 nodes with a connection. However, adding a third does something weird.

Say we have this situation:

Cell testOne = new Cell ("testOne", 123);
Cell testTwo = new Cell ("testTwo", 456);
Cell testThree = new Cell ("testThree", 200);

testOne.addConnection(testTwo);
testOne.addConnection(testThree);

What I get is three nodes with two lines strewn randomly in their general area (worth noting the nodes are positioned in a crudely random way). If I move around testTwo or testThree, a single line will trade off being connected to testOne. The second line remains unchanged no matter what. I have to think that somehow what's happening is that one of the EventHandlers is getting "unplugged" from their respective cells, or else somehow one of the lines is getting lost in memory. Here's the code to draw lines (I know it's really clunky). This method is in the Graph class, which controls graphic (oop) representation of the class. "cells" is the ArrayList storing all its nodes, "connections" is the arrayList in the Cell instance that keeps track of all the nodes it's connected to, and "LinesBetween" is a HashMap the Cell instance keeping track of whether a line has already been drawn between the two nodes.

public void drawAndManageEdgeLines(){
        if (cells.size() > 1) { //don't wanna make connections if there's only one cell, or none
            int count = 0;
            for (Cell cell : cells) { // for every cell on the graph
                List<Cell> connectionsList = cell.getConnections(); // look at that cell's connections
                if (!connectionsList.isEmpty()) {// validate that the cell is actually supposed to be connected to something
                    for (Cell connection : connectionsList) { // go through all their connections
                        if (!cell.getLinesBetween().get(connection) && cell.getLinesBetween().get(connection) != null) { //check to see whether there is already a line between them

                            Circle sourceCircle = cell.getCellView();
                            Circle targetCircle = connection.getCellView();

                            Bounds sourceBound = sourceCircle.localToScene(sourceCircle.getBoundsInLocal());
                            Bounds targetBound = targetCircle.localToScene(targetCircle.getBoundsInLocal());

                            double targetX = targetBound.getCenterX();
                            double targetY = targetBound.getCenterY();
                            double sourceX = sourceBound.getCenterX();
                            double sourceY = sourceBound.getCenterY();

                            edge = new Line(sourceX, sourceY, targetX, targetY);
                            edge.setStroke(Color.BLACK);
                            edge.setStrokeWidth(2);

                            getChildren().add(edge);

                            edge.toBack();
                            cell.setLinesBetweenEntry(connection, true);
                            connection.setLinesBetweenEntry(cell, true);
                            // these handlers control where the line is dragged to 
                            cell.addEventHandler(MouseEvent.MOUSE_DRAGGED, new EventHandler<MouseEvent>() {
                                @Override
                                public void handle(MouseEvent e) {
                                    edge.setStartX(e.getSceneX()); //this is a really cool method
                                    edge.setStartY(e.getSceneY());
                                    e.consume();
                                }
                            });
                            System.out.println("on round " + count + " we got there: ");
                            connection.addEventHandler(MouseEvent.MOUSE_DRAGGED, new EventHandler<MouseEvent>() {
                                @Override
                                public void handle(MouseEvent e) {
                                    edge.setEndX(e.getSceneX());
                                    edge.setEndY(e.getSceneY());
                                    e.consume();
                                }
                            });
                        }
                    }
                }
            }
        }
    }
  • 1
    [mcve] please .. – kleopatra Jun 17 '21 at 09:14
  • I posted the most precise guess I could make for where the problem code lay. – phdavis1027 Jun 17 '21 at 13:15
  • That doesn't mean it's a [mre] (← read the linked help page for more information). – Slaw Jun 17 '21 at 21:57
  • Also, rather than trying to move the edges in event handlers it would probably be easier to bind them to the appropriate nodes' locations. Then when you move a node the correct edges will update automatically. – Slaw Jun 17 '21 at 22:04
  • Thank you, I was looking into making properties for binding but thought I'd cheated my way out when I saw the setEndX/Y methods. Would I then have to call relocate() or something for it update on the display? Sorry for the poor StackOverFlow etiquette, obviously still learning the ropes. – phdavis1027 Jun 18 '21 at 05:25

1 Answers1

0

It's hard to tell what's going wrong without a proper minimal reproducible example, but you seem to be making this more complicated than it needs to be. If you want the edges to be "linked" to the nodes then I recommend you use bindings. Here's a proof-of-concept:

import javafx.application.Application;
import javafx.beans.binding.Bindings;
import javafx.scene.Cursor;
import javafx.scene.Scene;
import javafx.scene.layout.Pane;
import javafx.scene.paint.Color;
import javafx.scene.shape.Circle;
import javafx.scene.shape.Line;
import javafx.stage.Stage;

public class Main extends Application {

  @Override
  public void start(Stage primaryStage) {
    Circle node1 = new Circle(100, 100, 50, Color.FIREBRICK);
    Circle node2 = new Circle(900, 550, 50, Color.DARKBLUE);
    Circle node3 = new Circle(900, 100, 50, Color.DARKGREEN);

    addDragHandling(node1);
    addDragHandling(node2);
    addDragHandling(node3);

    Line edge1 = createEdge(node1, node2);
    Line edge2 = createEdge(node1, node3);

    Pane root = new Pane(edge1, edge2, node1, node2, node3);
    primaryStage.setScene(new Scene(root, 1000, 650));
    primaryStage.show();
  }

  private Line createEdge(Circle from, Circle to) {
    Line edge = new Line();
    edge.setStrokeWidth(2);
    edge.startXProperty().bind(from.centerXProperty());
    edge.startYProperty().bind(from.centerYProperty());
    edge.endXProperty().bind(to.centerXProperty());
    edge.endYProperty().bind(to.centerYProperty());
    return edge;
  }

  private void addDragHandling(Circle circle) {
    // changing cursors not necessary, only included to help indicate
    // when something can be dragged and is being dragged
    circle
        .cursorProperty()
        .bind(
            Bindings.when(circle.pressedProperty())
                .then(Cursor.CLOSED_HAND)
                .otherwise(Cursor.OPEN_HAND));

    double[] offset = {0, 0}; // (x, y)
    circle.setOnMousePressed(
        e -> {
          offset[0] = e.getX() - circle.getCenterX();
          offset[1] = e.getY() - circle.getCenterY();
          e.consume();
        });
    circle.setOnMouseDragged(
        e -> {
          circle.setCenterX(e.getX() - offset[0]);
          circle.setCenterY(e.getY() - offset[1]);
          e.consume();
        });
  }
}

Note I added the edges to the Pane first so that they were drawn under the nodes. See Z-Order in JavaFX. Also, your drag logic may look different depending on what Node you use to represent your graph nodes.

Since you are representing a graph your application will be more complex. If the graph is dynamic and you want the view to update in real time then you'll need to keep references to the nodes and their associated edges to add and remove them at will. But remember the view is only a visual representation of the model. Don't use the view to store model information (e.g. what nodes and edges actually exist).

Slaw
  • 37,820
  • 8
  • 53
  • 80
  • Thank you so much. I struggle really badly with seeing the overall design of a project over and above the specific methods built to implement them, and I see from your example that making nodes extend Pane added way more heartache than it's worth (on the plus side, it forced me to look into the Bounds source code and learn something interesting). From here my plan is obviously to fill out the GUI, but also to built a bridge between it and the adjacency matrix I've already constructed from my data files. Thank you again for the guidance. – phdavis1027 Jun 19 '21 at 02:43