Drag nodes like in a patience/Klondike card game - java

I'm doing a Klondike game. The logic is all working. I'm just having trouble with the UI in javafx.
I've been trying to move/drag the cards from the 'tableau pile' arround without the expected result.
My card is a ImageView with an Image inside. The cards are inside a Pane:
Pane tableau = new Pane();
for (int i = 0; i < n; i++) {
Image img = new Image("resources/images/" + (i + 1) + ".png");
ImageView imgView = new ImageView(img);
imgView.setY(i * 20);
//imgView Mouse Events here
tableau.getChildren().add(imgView);
}
I tried:
imgView.setOnMousePressed((MouseEvent mouseEvent) -> {
dragDelta.x = imgView.getLayoutX() - mouseEvent.getSceneX();
dragDelta.y = imgView.getLayoutY() - mouseEvent.getSceneY();
});
imgView.setOnMouseDragged((MouseEvent mouseEvent) -> {
imgView.setLayoutX(mouseEvent.getSceneX() + dragDelta.x);
imgView.setLayoutY(mouseEvent.getSceneY() + dragDelta.y);
});
This solution dont work because I'm setting the positions so when release the card wont return to where it was, and because the card is colliding with other UI objects.
Another attempt:
imgView.setOnDragDetected((MouseEvent event) -> {
ClipboardContent content = new ClipboardContent();
content.putImage(img);
Dragboard db = imgView.startDragAndDrop(TransferMode.ANY);
db.setDragView(img, 35, 50);
db.setContent(content);
event.consume();
});
In this solution the problems are: the card comes semitransparent, like moving a file, the cursor becames no/forbidden but other than that it works well: no collisions and the card goes to his original place if I release the mouse.
Another problem is that I dont know if I can move more than 1 card with this solution?
My final question is, How can I move a node (in this case an ImageView) or a group of nodes, from one pile to another, like in a Solitaire Card Game?

For knowing the original card position you should use the setTranslateX (and Y) instead of setLayoutX in your mouse handler. So when the user releases the card, you can simply invoke a Transition and let the card fly back to the layout position. If the user releases the card on a valid place, you set the translate coordinates to 0 and change the layout position or use relocate.
If you want to make the cards semitransparent, you could e. g. change the opacity or apply CSS.
By the way, I wouldn't use Clipboardcontent, it seems inappropriate for your needs.
You can move multiple objects with your mouse handling code. You simple have to apply the translation to multiple objects simultaneously. When you drag a pile, you determine the cards on top of the selected card and apply the transition to all of them.
Here's a quick example to show you how it could look like.
Card.java, you'll use an ImageView.
public class Card extends Rectangle {
static Random rand = new Random();
public Card() {
setWidth(100);
setHeight(200);
Color color = createRandomColor();
setStroke(color);
setFill( color.deriveColor(1, 1, 1, 0.4));
}
public static Color createRandomColor() {
int max = 200;
Color color = Color.rgb( (int) (rand.nextDouble() * max), (int) (rand.nextDouble() * max), (int) (rand.nextDouble() * max));
return color;
}
}
Game.java, the application.
public class Game extends Application {
static List<Card> cardList = new ArrayList<>();
#Override
public void start(Stage primaryStage) {
MouseGestures mg = new MouseGestures();
Group root = new Group();
for( int i=0; i < 10; i++) {
Card card = new Card();
card.relocate( i * 20, i * 10);
mg.makeDraggable(card);
cardList.add( card);
}
root.getChildren().addAll( cardList);
Scene scene = new Scene( root, 1600, 900);
primaryStage.setScene( scene);
primaryStage.show();
}
public static void main(String[] args) {
launch(args);
}
// TODO: don't use a static method, I only added for the example
public static List<Card> getSelectedCards( Card currentCard) {
List<Card> selectedCards = new ArrayList<>();
int i = cardList.indexOf(currentCard);
for( int j=i + 1; j < cardList.size(); j++) {
selectedCards.add( cardList.get( j));
}
return selectedCards;
}
}
MouseGestures.java, the mouse handling mechanism.
public class MouseGestures {
final DragContext dragContext = new DragContext();
public void makeDraggable(final Node node) {
node.setOnMousePressed(onMousePressedEventHandler);
node.setOnMouseDragged(onMouseDraggedEventHandler);
node.setOnMouseReleased(onMouseReleasedEventHandler);
}
EventHandler<MouseEvent> onMousePressedEventHandler = new EventHandler<MouseEvent>() {
#Override
public void handle(MouseEvent event) {
dragContext.x = event.getSceneX();
dragContext.y = event.getSceneY();
}
};
EventHandler<MouseEvent> onMouseDraggedEventHandler = new EventHandler<MouseEvent>() {
#Override
public void handle(MouseEvent event) {
Node node = (Node) event.getSource();
double offsetX = event.getSceneX() - dragContext.x;
double offsetY = event.getSceneY() - dragContext.y;
node.setTranslateX(offsetX);
node.setTranslateY(offsetY);
// same for the other cards
List<Card> list = Game.getSelectedCards( (Card) node);
for( Card card: list) {
card.setTranslateX(offsetX);
card.setTranslateY(offsetY);
}
}
};
EventHandler<MouseEvent> onMouseReleasedEventHandler = new EventHandler<MouseEvent>() {
#Override
public void handle(MouseEvent event) {
Node node = (Node) event.getSource();
moveToSource(node);
// same for the other cards
List<Card> list = Game.getSelectedCards( (Card) node);
for( Card card: list) {
moveToSource(card);
}
// if you find out that the cards are on a valid position, you need to fix it, ie invoke relocate and set the translation to 0
// fixPosition( node);
}
};
private void moveToSource( Node node) {
double sourceX = node.getLayoutX() + node.getTranslateX();
double sourceY = node.getLayoutY() + node.getTranslateY();
double targetX = node.getLayoutX();
double targetY = node.getLayoutY();
Path path = new Path();
path.getElements().add(new MoveToAbs( node, sourceX, sourceY));
path.getElements().add(new LineToAbs( node, targetX, targetY));
PathTransition pathTransition = new PathTransition();
pathTransition.setDuration(Duration.millis(1000));
pathTransition.setNode(node);
pathTransition.setPath(path);
pathTransition.setCycleCount(1);
pathTransition.setAutoReverse(true);
pathTransition.play();
}
/**
* Relocate card to current position and set translate to 0.
* #param node
*/
private void fixPosition( Node node) {
double x = node.getTranslateX();
double y = node.getTranslateY();
node.relocate(node.getLayoutX() + x, node.getLayoutY() + y);
node.setTranslateX(0);
node.setTranslateY(0);
}
class DragContext {
double x;
double y;
}
// pathtransition works with the center of the node => we need to consider that
public static class MoveToAbs extends MoveTo {
public MoveToAbs( Node node, double x, double y) {
super( x - node.getLayoutX() + node.getLayoutBounds().getWidth() / 2, y - node.getLayoutY() + node.getLayoutBounds().getHeight() / 2);
}
}
// pathtransition works with the center of the node => we need to consider that
public static class LineToAbs extends LineTo {
public LineToAbs( Node node, double x, double y) {
super( x - node.getLayoutX() + node.getLayoutBounds().getWidth() / 2, y - node.getLayoutY() + node.getLayoutBounds().getHeight() / 2);
}
}
}
When you pick a card or a multiple of cards, they'll transition back to their origin.
Here's a screenshot where I dragged the 3rd card from top.
When you check if a card is on a valid destination, you should invoke the fixPosition method. It simply relocates the card, i. e. calculates the position using the translation values, repositions the node and sets its translation to 0.
The tricky part was the PathTransition. It's working from the center of a node, not from the x/y coordinates. Here's a thread regarding that issue to which I replied in case you wish to know more about it.

Related

Add a node in a gridpane using mouse

I'm trying to do a quite easy boardgame (Carcassonne) but I'm having a lot of troubles with the graphic interface. The problem is that I don't see the way to make a relation between the mouse clicks and the gridpane row and columns.
The first tile is given and it's always added to the 100, 100 gridpanes position. You won't see it in the code, but for each tile added if the adjacents are empty it's added a white tile, so it looks like this:
Then, the player is expected to do a legal move ( we're not controlling cheaters, so yeah, it will be a weak game ) in the positions x = 99 y = 100, x = 101 y = 100, x = 100 y = 99, x = 100 y = 101.
But when i click there using the method play() the e.getSceneX(); method returns me the pixel position, and I need a way to convert it to a valid row index. So this is happening:
This is the console output:
tile XCCCC added at 100 100 // this is always given by the program, it's always the same
tile MFFCF added at 391 380
In this case, I clicked to the x = 100 y = 101 gridpane but the mouse returned me the pixel 391, 380.
Any idea?
This is the structure of my code:
public final class GUI extends Application {
private Game _game;
private GridPane _visualBoard;
#Override
public void start(final Stage primaryStage) {
// some stuff setting the gridpane, which will be inside a scrollpane and the scrollpane will be inside a borderpane which will alse have 2 additional VBoxes with the current turn information
play();
primaryStage.show();
}
public static void main(String[] args) {
Application.launch(args);
}
public void play() {
_visualBoard.setOnMousePressed((MouseEvent e) -> {
double x = e.getSceneX();
double y = e.getSceneY();
_game.doMove(x, y);
});
}
public void insertTile(myTile r, int x, int y) {
myVisualTile rV = new myVisualTile(r);
_visualBoard.add(rV, x, y);
}
And this is the class game:
public class Game {
private GUI _gui;
private List<Player> _players; // An arraylist of players
private int _currentPlayer; // The index of the current player in the ArrayList
private Board _board; // The logical board, totally separeted from the gridpane
private tileStack _stack;
public void doMove(double x, double y) {
int ax = (int) x;
int ay = (int) y;
if (_stack.isEmpty()) {
System.out.println("Game finnished");
//stuff
}
else {
myTile r = _stack.poll(); // takes the first tile of the stack
_gui.insertTile(r, ax, ay);
}
}
public void play() {
_visualBoard.setOnMousePressed((MouseEvent e) -> {
Node source = (Node)e.getTarget() ;
Integer x = GridPane.getColumnIndex(source);
Integer y = GridPane.getRowIndex(source);
_game.doMove(x, y);
});
}
That actualy worked. Thanks everybody!

How To Visualize A Matrix on Java FX

I have a matrix which consists of 0s and 1s. I want to visualize this matrix like a grid and put a mark to the cells which has 1 in it. I made my research in order to find a way of doing this in Java FX but I could not find something similar to what I said. Is there any way to do that in Java FX? Best Regards,
I visualized the matrix with using GridPane and Timeline in Java FX. I found this method thanks to c0der's comment above. In my case I needed to draw a path on the matrix, it is like finding a minimum path problem. Here is my source code:
public class VisualizationThread implements EventHandler<ActionEvent> {
private ArrayList<ArrayList<Integer>> matrix; // Matrix
private ArrayList<Integer> solution; // One individual
private ArrayList<ArrayList<Integer>> visited; // Visited coordinate list
private int currentX; // Current x coordinate
private int currentY; // Current y coordinate
private int index; // Current movement index
private int attempt; // unsuccessful attempt count (used while visualizing all generations)
private boolean closeAtTheAnd; // Close all stages except the last one which shows final situation, the correct path
private boolean isSuccessful; // Currently visualizing an unsuccessful attempt or correct path?
private Stage primaryStage; // primary stage (gui component)
public VisualizationThread(ArrayList<ArrayList<Integer>> matrix, ArrayList<Integer> solution, int currentX, int currentY,
boolean isSuccessful, int attempt, boolean closeAtTheAnd){
this.matrix = matrix;
this.solution = solution;
this.currentY = currentY;
this.currentX = currentX;
this.isSuccessful = isSuccessful;
this.attempt = attempt;
this.primaryStage = new Stage();
this.index = 0;
this.closeAtTheAnd = closeAtTheAnd;
this.visited = new ArrayList<ArrayList<Integer>>();
}
// Visualize a single individual
public void run() {
// GUI processes
BorderPane root = new BorderPane();
GridPane grid = new GridPane();
grid.setPadding(new Insets(10));
grid.setHgap(10);
grid.setVgap(10);
StackPane[][] screen_buttons = new StackPane[matrix.size()][matrix.size()];
visited.add(new ArrayList<Integer>(){{add(currentX); add(currentY);}});
for (int y=0;y<matrix.size();y++) {
for (int x=0;x<matrix.get(y).size();x++) {
screen_buttons[y][x] = new StackPane();
Rectangle rec = new Rectangle(60,60);
// Visualize visited points as green and others yellow and foods red
if(!(currentX>=matrix.size() || currentY>=matrix.size())){
if(isVisited(visited, x, y)){
rec.setFill(Color.GREEN);
}else {
rec.setFill(matrix.get(y).get(x) == 0 ? Color.YELLOW : Color.RED);
}
}else {
rec.setFill(matrix.get(y).get(x) == 0 ? Color.YELLOW : Color.RED);
}
rec.setStyle("-fx-arc-height: 20; -fx-arc-width: 20;");
screen_buttons[y][x].getChildren().addAll(rec);
grid.add(screen_buttons[y][x], x, y);
}
}
//container for controls
GridPane controls = new GridPane();
root.setCenter(grid);
root.setBottom(controls);
Scene scene = new Scene(root);
primaryStage.setScene(scene);
primaryStage.setTitle(isSuccessful ? "Correct Path" : "Unsuccessful Attempt: " + attempt);
primaryStage.show();
if(!(index >= solution.size())){
switch (solution.get(index)){
case 1:
setCurrentY(getCurrentY()-1);
break;
case 2:
setCurrentX(getCurrentX()-1);
break;
case 3:
setCurrentY(getCurrentY()+1);
break;
case 4:
setCurrentX(getCurrentX()+1);
break;
}
index++;
}
if(closeAtTheAnd && index == solution.size()){
primaryStage.close();
}
}
// This method finds if current coordinate is visited before or not
// #param visitedList: List of visited points
// #param x: x coordinate
// #param y: y coordinate
public boolean isVisited(ArrayList<ArrayList<Integer>> visitedList, int x, int y){
boolean result = false;
// Check if x and y coordinates are visited before
for (ArrayList<Integer> temp : visitedList){
if(temp.get(0) == x && temp.get(1) == y){
result = true;
break;
}
}
return result;
}
public int getCurrentX() {
return currentX;
}
public void setCurrentX(int currentX) {
this.currentX = currentX;
}
public int getCurrentY() {
return currentY;
}
public void setCurrentY(int currentY) {
this.currentY = currentY;
}
public void handle(ActionEvent event) {
run();
}
}
// In another part of the code which I need to trigger the event above.
ArrayList<Integer> bestOfLastPopulation = geneticAlgorithm.findMostAte(finalPopulation, matrix, centerX, centerY);
VisualizationThread visualizationThread = new VisualizationThread(matrix, bestOfLastPopulation, centerX, centerY, true, 0, false);
Timeline fiveSecondsWonder = new Timeline(new KeyFrame(Duration.millis(150), visualizationThread));
fiveSecondsWonder.setCycleCount(bestOfLastPopulation.size() + 1);
fiveSecondsWonder.play();
For someone who is trying to find the same thing, should look at the part which begins with a nested for loop and ends with primaryStage.show(). At this part of code, it is visualizing the matrix in different colors. For example, if a cell contains 1 then red, if it contains a 0 then yellow and if it is visited before then it will painted to green color. Hope that helps to someone else. Best Regards,

Regular Polygon intersection with a circle

I would like to Draw an Regular Polygon inscrit , and I have wrote some code with the usually formula angle = 2*Math.Pi/numside ecc etc.
The Poylgon it's drawn with the circle dynamically with mouse movement in this way:
public void Set_beahvior(Pane DrawPane, BottomPane bottompane )
{
this.selectedProperty().addListener(new ChangeListener<Boolean>()
{
private final EventHandler<MouseEvent> pressedHandler = (event) -> {
cerchio = new MyCircle(event.getX(), event.getY(), 0.0, DrawPane ,bottompane1);
cerchio.setCenterX(event.getX());
cerchio.setCenterY(event.getY());
cerchio.setStrokeType(StrokeType.OUTSIDE);
cerchio.setStroke(Color.RED);
cerchio.setFill(Color.TRANSPARENT);
cerchio.setStrokeWidth(1);
currentPolygon= new MyPolygon2(DrawPane,bottompane);
DrawPane.getChildren().addAll(cerchio);
DrawPane.getChildren().addAll(currentPolygon);
};
private final EventHandler<MouseEvent> draggedHandler = (event) -> {
cerchio.setRadius((event.getX()-cerchio.getCenterX()));
currentPolygon.setPolygon(cerchio.getCenterX(), cerchio.getCenterY(), cerchio.getRadius(),cerchio);
};
private final EventHandler<MouseEvent> releasedHandler = (event) -> {
cerchio.setRadius((event.getX()-cerchio.getCenterX()));
cerchio.setStroke(Color.TRANSPARENT);
};
#Override
public void changed(ObservableValue<? extends Boolean> observable, Boolean oldValue, Boolean newValue) {
// add/remove event handlers based on toggle state of the button
if (newValue) {
DrawPane.addEventHandler(MouseEvent.MOUSE_PRESSED, pressedHandler);
DrawPane.addEventHandler(MouseEvent.MOUSE_DRAGGED, draggedHandler);
DrawPane.addEventHandler(MouseEvent.MOUSE_RELEASED, releasedHandler);
} else {
DrawPane.removeEventHandler(MouseEvent.MOUSE_PRESSED, pressedHandler);
DrawPane.removeEventHandler(MouseEvent.MOUSE_DRAGGED, draggedHandler);
DrawPane.removeEventHandler(MouseEvent.MOUSE_RELEASED, releasedHandler);
}
}
});
And another pieace of code :
public void setPolygon(Double x, Double y, Double radius, MyCircle circle){
DoubleProperty raggio = new SimpleDoubleProperty(circle.getRadius());
double section = 2.0 * Math.PI/Numside;
this.getPoints().addAll((x + raggio.doubleValue() * Math.cos(0)), (y + raggio.doubleValue() * Math.sin(0)));
for(int i=1; i<Numside; i++){
this.getPoints().addAll((x + raggio.doubleValue() * Math.cos(section * i)), (y + raggio.doubleValue() * Math.sin(section * i)));
}
}
THE PROBLEM: I started with 3 side for then I'll write the code for a number of side generic, but All going fine if I increase dynamically the radius of circle (increment the radius -> triangle increase ).
But if I decrease the radius of circle, the polygon doesn't follow the behaviuor of the circle, and I can't understand why, any suggestion please?
PS: please ignore the Doubleproperty and some code written only for a test, I know that if I pass the circle object I don't need the other parameter. At the moment I'm looking at the reason why the polygon does not follow the circumference.
You never clear any of the points from the Polygon. You need to make sure the coordinates in the points list remains twice the number of sides. Furthermore the parameters of setPolygon are not defined well: You pass the same info though the first 3 and the last parameter; also you're using the reference type Double for no obvious reason. The following code improves on the code a bit:
private final int numside;
private final double[] points;
public MyPolygon2(int numside) {
this.numside = numside;
this.points = new double[numside * 2];
double step = 2 * Math.PI / numside;
for (int i = 0; i < numside; i++) {
int index = 2 * i;
double angle = i * step;
points[index] = Math.cos(angle);
points[index + 1] = Math.sin(angle);
}
}
public void setPolygon(double x, double y, double radius) {
getPoints().clear();
for (int i = 0; i < points.length;) {
getPoints().addAll(x + radius * points[i++], y + radius * points[i++]);
}
}

GUI edge to node recursive path not finding first index of path

I wrote a program that allows a user to add nodes and connect them with edges with a GUI interface. The first node is always labeled "0". When I call the findPath method however it shows the entire path appropriately just lacking the 0. For example if I want to find the path from 0-4, and they're all connected numerically it will show [1,2,3,4]. To get a proper reading of the path after connecting the nodes you need to hit the print adjacency button. Everything else is running as expected but I can't seem to figure out why this won't add the first node.
Thanks Guys!
import javax.swing.JFrame;//imports JFrame package
public class Graph//class header
{
public static void main (String[] args)//main method
{
JFrame frame = new JFrame ("Directed Graph");//sets variable frame to a JFrame object and titles it
frame.setDefaultCloseOperation (JFrame.EXIT_ON_CLOSE);//exits application when it is closed
frame.getContentPane().add (new GraphPanel());//adding GraphPanel() to the frame
frame.setLocationRelativeTo(null);//sets location to the center
frame.pack();//packs the frame, fits everything inside
frame.setVisible(true);//makes frame visible
}
}
import java.util.ArrayList;//all of these are the required to get classes from other packages
import javax.swing.*;
import java.awt.*;
import java.awt.event.*;
import javax.swing.JPanel;
public class GraphPanel extends JPanel
{
private final int SIZE = 10; // radius of each node
private double alpha; // double variable to define later
private JButton print, create, delete, path1; //allpath;//button variables
private JTextField label, label1;
boolean mode = true;//sets boolean for entire class to true, until later when I define it as false for certain methods to be complete
String x1, y1;
String str1 = new String("Mode Chosen Is : create" );//when you choose create it will print this instead
String str2 = new String("Mode Chosen Is : delete" );//when you choose delete it will print this
int x, y;
int mode1 = 1; // int mode1 variable, will comment why i need this when it is later defined/changed
private Point point1 = null, point2 = null, point3 = null;//makes pointers null, i added a third pointer to put the distance from the first drawn string at the top right, but no longer needed it
//keeping it incase I wanted to add it again
private ArrayList<Point> nodeList; // Graph nodes
private ArrayList<Edge> edgeList; // Graph edges
static private int[][] a = new int[100][100]; // Graph adjacency matrix
static ArrayList<Integer> path = new ArrayList<Integer>();
public GraphPanel()
{
nodeList = new ArrayList<Point>();//declares ArrayList<Point>
edgeList = new ArrayList<Edge>();//declares ArrayList<Edge>
GraphListener listener = new GraphListener();//assigns 'listener' to a Graphlistener object
addMouseListener (listener);//accepts and reads mouseListener methods to the GraphListener object
addMouseMotionListener (listener);//accepts and reads MouseMotionListener methods to the GraphListener object
label = new JTextField (1);
label1 = new JTextField (1);
//allpath = new JButton ("Find All Paths");
path1 = new JButton ("Find Path");//path button
create = new JButton("Create");//declares 'create' as a JButton object with text on it that says 'create'
delete = new JButton("Delete");//declares 'delete' as a JButton object with text that says 'delete'
JButton print = new JButton("Print adjacency matrix");//declares 'print' as a JButton object with text that reads 'print adjacency matrix'
print.addActionListener (new ButtonListener());//adds an action listener to the print button from the buttonlistener object
create.addActionListener (new ButtonListener1());//adds an actionListener to the create button from the buttonlistener1 object
delete.addActionListener (new ButtonListener2());//adds an actionliseneer to the delete button from the buttonlistener2 object
path1.addActionListener (new ButtonListener3());//adds button that will find path of nodes
//allpath.addActionListener (new ButtonListener4());
setBackground (Color.black);//sets backround as black
setPreferredSize (new Dimension(400, 300));//this is preferred size of graphpanel
add(print);//these next six add()'s add each button to GraphPanel
add(delete);//
add(create);//
//add(allpath);
add(path1);
add(label);
add(label1);
}
public static boolean findPath(int x, int y) //boolean method findpath that takes two ints as parameters
{
boolean found = false;
if (a[x][y]==1) //base case
{
//System.out.println(x + " -> " + y);//prints node -> node
found = true;//path is found
path.add(0,y);
}
else
{
int z;
for (z=0; !found& z<a.length; z++)
{
if (a[x][z]==1)
{
a[x][z]=2;
found = findPath(z,y);
}
if (found)
{
//System.out.println(x + " -> " + z);
path.add(0,z);
}
}
}
if (!found){
System.out.println("Path Does Not Exist");
}
return found;
}
// public static ArrayList findPath1(int x, int y, ArrayList path)
// {
// System.out.println("CALL FIND: " + path);
// ArrayList path2 = null;
// if (a[x][y]==1)
// {
// ArrayList path3 = (ArrayList)path.clone();
// path3.add(y);
// System.out.println("EXIT BASE: " + path3);
// path2 = path3;
// }
// else
// {
// int z;
// for (z=0; z<a.length; z++)
// if (a[x][z]==1)
// {
// ArrayList path3 = (ArrayList)path.clone();
// path3.add(z);
// path3 = findPath1(z,y,path3);
// System.out.println("EXIT RECU: " + path);
// path2 = path3;
// }
// }
// return path2;
// }
// Draws the graph
public void paintComponent (Graphics page)
{
super.paintComponent(page);//using parent methods
// Draws the edge that is being dragged
page.setColor (Color.green);//setting color of what will be drawn to green
if (mode1 == 1)//if mode1 is set to 1, which is when the create button is selected
{
page.drawString(str1, 125, 75);//then this is printed
}
if (mode1 == 2)//if mode1 is set to 2, which is when the delete button is selected
{
page.drawString(str2, 125, 75);//then this is printed
}
if (point1 != null && point2 != null)//if and only if both points are not null
{
page.drawLine (point1.x, point1.y, point2.x, point2.y);//then it draws the line from between point1(x, y cords), and point2(x, y cords)
page.fillOval (point2.x-3, point2.y-3, 6, 6);//fills this oval by the rectangle it is specified from
}
// Draws the nodes
for (int i=0; i<nodeList.size(); i++) //for loop going through the nodeList ArrayList
{
page.setColor (Color.green);//color is set to green
page.fillOval (nodeList.get(i).x-SIZE, nodeList.get(i).y-SIZE, SIZE*2, SIZE*2);//fills oval by subtracting the SIZE from the x, y and setting the height and width as two times the SIZE
page.setColor (Color.black);//sets next line to black
page.drawString (String.valueOf(i), nodeList.get(i).x-SIZE/2, nodeList.get(i).y+SIZE/2);//writes inside the node what number index it is from the ArrayList with black text
}
// Draws the edges
for (int i=0; i<edgeList.size(); i++) // for loop going through the edgeList ArrayList
{
page.setColor (Color.green);//sets the next line to green
page.drawLine (edgeList.get(i).a.x, edgeList.get(i).a.y,edgeList.get(i).b.x, edgeList.get(i).b.y);//draws the line from x and y cords of where it starts to end
//page.fillOval (edgeList.get(i).b.x-3, edgeList.get(i).b.y-3, 6, 6);
//page.drawString (String.valueOf(point1.x*(point2.y-point3.y)+point2.x*(point3.y-point1.y)+point3.x*(point1.y-point2.y)), 5, 15);
alpha = Math.atan((double)(edgeList.get(i).b.y-edgeList.get(i).a.y)/(edgeList.get(i).b.x-edgeList.get(i).a.x));//alpha = b.y-a.y cords/b.x-a.x chords as a double variable
if (edgeList.get(i).a.x > edgeList.get(i).b.x)//if the x chord of a is greater than the x cordinate of b
{
alpha = alpha + Math.PI;//then alpha = previously defined alpha multiplied by PI
}
if (edgeList.get(i).a.x < edgeList.get(i).b.x && edgeList.get(i).a.y > edgeList.get(i).b.y)//if a.x is less than b.x and a.y is less than b.y
{
alpha = alpha + 2*Math.PI;//then alpha becomes the previously defined alpha multiplied by 2 PI
}
arrow(page,edgeList.get(i).b.x,edgeList.get(i).b.y,0,1.57-alpha);//arrow method, taking a x(edgeList.get(i).b.x, y(edgeList.get(i).b.y, length = 0 so arrow is at the
// very end of the drawn line subtracted by a double aplha
}
}
// arrow method to call when making an arrow
private void arrow(Graphics page, int x, int y, int len, double alpha)//arrow method, taking a x, y, length and double parameter
{
page.setColor (Color.green);//sets arrow to green
int x1 = x+(int)(len*Math.sin(alpha));//x1 is set to x plus length *sin of alpha
int y1 = y+(int)(len*Math.cos(alpha));//y1 is set to y plus the length * cosin of alpha
page.drawLine (x, y, x1, y1);//drawa the x and y, and then previously defined x1, and y1
page.drawLine (x1, y1, x1+(int)(20*Math.sin(alpha+2.5)), y1+(int)(20*Math.cos(alpha+2.5)));//arithmatic to draw one side of the line for the arrow
page.drawLine (x1, y1, x1+(int)(20*Math.sin(alpha+3.7)), y1+(int)(20*Math.cos(alpha+3.7)));//arithmatic to draw the corresponding line for the arrow to complete the arrow
}
// The listener for mouse events.
private class GraphListener implements MouseListener, MouseMotionListener//GraphListener which implements MouseListener and MouseMotionListener, meaning it takes methods from both of those classes
{
public void mouseClicked (MouseEvent event)//when mouse is clicked and released
{
if (mode == true)//if the mode is true
{
nodeList.add(event.getPoint());//the current point to the nodeList
repaint();//whenever you change the look of a componenet you call this method
}
if (mode == false)//if boolean mode is false
{
for (int i=0; i<nodeList.size(); i++)//for loop to go through the nodeList ArrayList
{
if (distance(nodeList.get(i), event.getPoint()) < SIZE)//if the distance between a specific node and the pointer is less than the radius of the node (SIZE)
nodeList.remove(i);//then remove that node
}
for (int i=0; i<edgeList.size(); i++)
{
if (distance(edgeList.get(i).a, event.getPoint())+distance(event.getPoint(), edgeList.get(i).b)-distance(edgeList.get(i).a, edgeList.get(i).b) < SIZE)
//if the (distance between starting point of that edgelist index and the current point) plus (the distance between the current point and ending point of that edgeList index)
//subtracted by the (distance of the starting and ending point of that edgeList index)(AB+BC-AC) is all less than the size
edgeList.remove(i);//then remove that index
}
}
}
public void mousePressed (MouseEvent event)//when the mouse is pressed down
{
if (mode == true)//if the mode is true
{
point1 = event.getPoint();//you start that point
}
}
public void mouseDragged (MouseEvent event)//when the mouse is dragged while pressed down
{
if (mode == true)//if the mode is true
{
point2 = event.getPoint();//then you get the second point
repaint();//must use this method because you are making a change to the component
}
}
public void mouseReleased (MouseEvent event)//when the mouse is released after being pressed (not to be confused with a click)
{
if (mode == true)//if the mode is true
{
point2 = event.getPoint();//then you set point2 to where that point landed
if (point1.x != point2.x && point1.y != point2.y)//if the points are not in the same spots
{
edgeList.add(new Edge(point1,point2));//then you add a new edgeList element to the ArrayList, by definition of Edge() object
repaint();//must use this method because changes to component have been made
}
}
}
public void mouseMoved (MouseEvent event)// if the mouse is moved, without it being clicked (not to be confused by mouseDragged)
{
point3 = event.getPoint();//point three is set to be where the mouse is at the current time(used this to find the distance of original edge drawn, keeping here incase I want to add)
repaint();//must use this method because changes were made to component
}
// Empty definitions for unused event methods.
public void mouseEntered (MouseEvent event){}//when the mouse enters a component
public void mouseExited (MouseEvent event) {}//when the mouse exits a component
private int distance(Point p1, Point p2) //private distance formula but inside of this class so i was able to use it for determining how to erase nodes and edges
{
return (int)Math.sqrt((p1.x-p2.x)*(p1.x-p2.x)+(p1.y-p2.y)*(p1.y-p2.y));//square root of x cordinants of first point minus second point times its self plus the y coordinates
//of first point minus second point times itself
}
}
// Represents the graph edges
private class Edge //defines how edges are made
{
Point a, b;
public Edge(Point a, Point b)
{
this.a = a;
this.b = b;
}
}
private class ButtonListener implements ActionListener
{
public void actionPerformed (ActionEvent event)
{
if (event.getSource()== create)
{
mode = true;
mode1 = 1;
}
if (event.getSource()==delete)
{
mode = !true;
mode1 = 2;
}
// Initializes graph adjacency matrix
for (int i=0; i<nodeList.size(); i++)
for (int j=0; j<nodeList.size(); j++) a[i][j]=0;
// Includes the edges in the graph adjacency matrix
for (int i=0; i<edgeList.size(); i++)
{
for (int j=0; j<nodeList.size(); j++)
if (distance(nodeList.get(j),edgeList.get(i).a)<=SIZE+3)
for (int k=0; k<nodeList.size(); k++)
if (distance(nodeList.get(k),edgeList.get(i).b)<=SIZE+3)
{
System.out.println(j+"->"+k);
a[j][k]=1;
}
}
if (event.getSource()==print)
{
// Prints the graph adjacency matrix
for (int i=0; i<nodeList.size(); i++)
{
for (int j=0; j<nodeList.size(); j++)
System.out.print(a[i][j]+"\t");
System.out.println();
}
}
}
// Euclidean distance function
private int distance(Point p1, Point p2)
{
return (int)Math.sqrt((p1.x-p2.x)*(p1.x-p2.x)+(p1.y-p2.y)*(p1.y-p2.y));
}
}
private class ButtonListener1 implements ActionListener
{
public void actionPerformed (ActionEvent event)
{
if (event.getSource()== create)//if create button is selected
{
mode = true;//then the mode is set to true
mode1 = 1;//and mode1 is set to 1
}
}
}
private class ButtonListener2 implements ActionListener
{
public void actionPerformed (ActionEvent event)
{
if (event.getSource()== delete)//if create button is selected
{
mode = !true;//then the mode is set to true
mode1 = 2;//and mode1 is set to 2, which would change the string that is output
}
}
}
private class ButtonListener3 implements ActionListener
{
public void actionPerformed (ActionEvent event)
{
x1 = label.getText();
y1 = label1.getText();
x = Integer.parseInt(x1);
y = Integer.parseInt(y1);
System.out.println("Path from " +x+" to " +y);
System.out.println(findPath(x,y));
System.out.println(path);
}
}
You just never add the first node to path. You can do it when you check if path exists:
if (!found){
System.out.println("Path Does Not Exist");
} else {
path.add(0, x);
}
and then you also need to remove this block:
if (found) {
//System.out.println(x + " -> " + z);
path.add(0,z);
}
So complete method will look like
public static boolean findPath(int x, int y) //boolean method findpath that takes two ints as parameters
{
boolean found = false;
if (a[x][y]==1) //base case
{
//System.out.println(x + " -> " + y);//prints node -> node
found = true;//path is found
path.add(0,y);
}
else
{
int z;
for (z=0; !found& z<a.length; z++)
{
if (a[x][z]==1)
{
a[x][z]=2;
found = findPath(z,y);
}
/*if (found)
{
//System.out.println(x + " -> " + z);
path.add(0,z);
}*/
}
}
if (!found){
System.out.println("Path Does Not Exist");
} else {
path.add(0,x);
}
return found;
}
I also suggest you to look at Dijkstra's algorithm of finding the shortest path between nodes
I was able to find it, and of course it was the smallest mistake I missed, adding this because in case anyone else has the same problem I hope this helps. The main differences are in my buttonlistener3 class but I'll add the whole thing again just incase this helps anyone out.
import java.util.ArrayList;//all of these are the required to get classes from other packages
import javax.swing.*;
import java.awt.*;
import java.awt.event.*;
import javax.swing.JPanel;
public class GraphPanel extends JPanel
{
private final int SIZE = 10; // radius of each node
private double alpha; // double variable to define later
private JButton print, create, delete, path1; //allpath;//button variables
private JTextField label, label1;
boolean mode = true;//sets boolean for entire class to true, until later when I define it as false for certain methods to be complete
String x1, y1;
String str1 = new String("Mode Chosen Is : create" );//when you choose create it will print this instead
String str2 = new String("Mode Chosen Is : delete" );//when you choose delete it will print this
int x, y;
int mode1 = 1; // int mode1 variable, will comment why i need this when it is later defined/changed
private Point point1 = null, point2 = null, point3 = null;//makes pointers null, i added a third pointer to put the distance from the first drawn string at the top right, but no longer needed it
//keeping it incase I wanted to add it again
private ArrayList<Point> nodeList; // Graph nodes
private ArrayList<Edge> edgeList; // Graph edges
static private int[][] a = new int[100][100]; // Graph adjacency matrix
static ArrayList<Integer> path = new ArrayList<Integer>();
public GraphPanel()
{
nodeList = new ArrayList<Point>();//declares ArrayList<Point>
edgeList = new ArrayList<Edge>();//declares ArrayList<Edge>
GraphListener listener = new GraphListener();//assigns 'listener' to a Graphlistener object
addMouseListener (listener);//accepts and reads mouseListener methods to the GraphListener object
addMouseMotionListener (listener);//accepts and reads MouseMotionListener methods to the GraphListener object
label = new JTextField (1);
label1 = new JTextField (1);
//allpath = new JButton ("Find All Paths");
path1 = new JButton ("Find Path");//path button
create = new JButton("Create");//declares 'create' as a JButton object with text on it that says 'create'
delete = new JButton("Delete");//declares 'delete' as a JButton object with text that says 'delete'
JButton print = new JButton("Print adjacency matrix");//declares 'print' as a JButton object with text that reads 'print adjacency matrix'
print.addActionListener (new ButtonListener());//adds an action listener to the print button from the buttonlistener object
create.addActionListener (new ButtonListener1());//adds an actionListener to the create button from the buttonlistener1 object
delete.addActionListener (new ButtonListener2());//adds an actionliseneer to the delete button from the buttonlistener2 object
path1.addActionListener (new ButtonListener3());//adds button that will find path of nodes
//allpath.addActionListener (new ButtonListener4());
setBackground (Color.black);//sets backround as black
setPreferredSize (new Dimension(400, 300));//this is preferred size of graphpanel
add(print);//these next six add()'s add each button to GraphPanel
add(delete);//
add(create);//
//add(allpath);
add(path1);
add(label);
add(label1);
}
public static boolean findPath(int x, int y) //boolean method findpath that takes two ints as parameters
{
boolean found = false;
if (a[x][y]==1) //base case
{
//System.out.println(x + " -> " + y);//prints node -> node
found = true;//path is found
path.add(0,y);
}
else
{
int z;
for (z=0; !found& z<a.length; z++)
{
if (a[x][z]==1)
{
a[x][z]=2;
found = findPath(z,y);
}
if (found)
{
//System.out.println(x + " -> " + z);
path.add(0,z);
}
}
}
return found;
}
// public static ArrayList findPath1(int x, int y, ArrayList path)
// {
// System.out.println("CALL FIND: " + path);
// ArrayList path2 = null;
// if (a[x][y]==1)
// {
// ArrayList path3 = (ArrayList)path.clone();
// path3.add(y);
// System.out.println("EXIT BASE: " + path3);
// path2 = path3;
// }
// else
// {
// int z;
// for (z=0; z<a.length; z++)
// if (a[x][z]==1)
// {
// ArrayList path3 = (ArrayList)path.clone();
// path3.add(z);
// path3 = findPath1(z,y,path3);
// System.out.println("EXIT RECU: " + path);
// path2 = path3;
// }
// }
// return path2;
// }
// Draws the graph
public void paintComponent (Graphics page)
{
super.paintComponent(page);//using parent methods
// Draws the edge that is being dragged
page.setColor (Color.green);//setting color of what will be drawn to green
if (mode1 == 1)//if mode1 is set to 1, which is when the create button is selected
{
page.drawString(str1, 125, 75);//then this is printed
}
if (mode1 == 2)//if mode1 is set to 2, which is when the delete button is selected
{
page.drawString(str2, 125, 75);//then this is printed
}
if (point1 != null && point2 != null)//if and only if both points are not null
{
page.drawLine (point1.x, point1.y, point2.x, point2.y);//then it draws the line from between point1(x, y cords), and point2(x, y cords)
page.fillOval (point2.x-3, point2.y-3, 6, 6);//fills this oval by the rectangle it is specified from
}
// Draws the nodes
for (int i=0; i<nodeList.size(); i++) //for loop going through the nodeList ArrayList
{
page.setColor (Color.green);//color is set to green
page.fillOval (nodeList.get(i).x-SIZE, nodeList.get(i).y-SIZE, SIZE*2, SIZE*2);//fills oval by subtracting the SIZE from the x, y and setting the height and width as two times the SIZE
page.setColor (Color.black);//sets next line to black
page.drawString (String.valueOf(i), nodeList.get(i).x-SIZE/2, nodeList.get(i).y+SIZE/2);//writes inside the node what number index it is from the ArrayList with black text
}
// Draws the edges
for (int i=0; i<edgeList.size(); i++) // for loop going through the edgeList ArrayList
{
page.setColor (Color.green);//sets the next line to green
page.drawLine (edgeList.get(i).a.x, edgeList.get(i).a.y,edgeList.get(i).b.x, edgeList.get(i).b.y);//draws the line from x and y cords of where it starts to end
//page.fillOval (edgeList.get(i).b.x-3, edgeList.get(i).b.y-3, 6, 6);
//page.drawString (String.valueOf(point1.x*(point2.y-point3.y)+point2.x*(point3.y-point1.y)+point3.x*(point1.y-point2.y)), 5, 15);
alpha = Math.atan((double)(edgeList.get(i).b.y-edgeList.get(i).a.y)/(edgeList.get(i).b.x-edgeList.get(i).a.x));//alpha = b.y-a.y cords/b.x-a.x chords as a double variable
if (edgeList.get(i).a.x > edgeList.get(i).b.x)//if the x chord of a is greater than the x cordinate of b
{
alpha = alpha + Math.PI;//then alpha = previously defined alpha multiplied by PI
}
if (edgeList.get(i).a.x < edgeList.get(i).b.x && edgeList.get(i).a.y > edgeList.get(i).b.y)//if a.x is less than b.x and a.y is less than b.y
{
alpha = alpha + 2*Math.PI;//then alpha becomes the previously defined alpha multiplied by 2 PI
}
arrow(page,edgeList.get(i).b.x,edgeList.get(i).b.y,0,1.57-alpha);//arrow method, taking a x(edgeList.get(i).b.x, y(edgeList.get(i).b.y, length = 0 so arrow is at the
// very end of the drawn line subtracted by a double aplha
}
}
// arrow method to call when making an arrow
private void arrow(Graphics page, int x, int y, int len, double alpha)//arrow method, taking a x, y, length and double parameter
{
page.setColor (Color.green);//sets arrow to green
int x1 = x+(int)(len*Math.sin(alpha));//x1 is set to x plus length *sin of alpha
int y1 = y+(int)(len*Math.cos(alpha));//y1 is set to y plus the length * cosin of alpha
page.drawLine (x, y, x1, y1);//drawa the x and y, and then previously defined x1, and y1
page.drawLine (x1, y1, x1+(int)(20*Math.sin(alpha+2.5)), y1+(int)(20*Math.cos(alpha+2.5)));//arithmatic to draw one side of the line for the arrow
page.drawLine (x1, y1, x1+(int)(20*Math.sin(alpha+3.7)), y1+(int)(20*Math.cos(alpha+3.7)));//arithmatic to draw the corresponding line for the arrow to complete the arrow
}
// The listener for mouse events.
private class GraphListener implements MouseListener, MouseMotionListener//GraphListener which implements MouseListener and MouseMotionListener, meaning it takes methods from both of those classes
{
public void mouseClicked (MouseEvent event)//when mouse is clicked and released
{
if (mode == true)//if the mode is true
{
nodeList.add(event.getPoint());//the current point to the nodeList
repaint();//whenever you change the look of a componenet you call this method
}
if (mode == false)//if boolean mode is false
{
for (int i=0; i<nodeList.size(); i++)//for loop to go through the nodeList ArrayList
{
if (distance(nodeList.get(i), event.getPoint()) < SIZE)//if the distance between a specific node and the pointer is less than the radius of the node (SIZE)
nodeList.remove(i);//then remove that node
}
for (int i=0; i<edgeList.size(); i++)
{
if (distance(edgeList.get(i).a, event.getPoint())+distance(event.getPoint(), edgeList.get(i).b)-distance(edgeList.get(i).a, edgeList.get(i).b) < SIZE)
//if the (distance between starting point of that edgelist index and the current point) plus (the distance between the current point and ending point of that edgeList index)
//subtracted by the (distance of the starting and ending point of that edgeList index)(AB+BC-AC) is all less than the size
edgeList.remove(i);//then remove that index
}
}
}
public void mousePressed (MouseEvent event)//when the mouse is pressed down
{
if (mode == true)//if the mode is true
{
point1 = event.getPoint();//you start that point
}
}
public void mouseDragged (MouseEvent event)//when the mouse is dragged while pressed down
{
if (mode == true)//if the mode is true
{
point2 = event.getPoint();//then you get the second point
repaint();//must use this method because you are making a change to the component
}
}
public void mouseReleased (MouseEvent event)//when the mouse is released after being pressed (not to be confused with a click)
{
if (mode == true)//if the mode is true
{
point2 = event.getPoint();//then you set point2 to where that point landed
if (point1.x != point2.x && point1.y != point2.y)//if the points are not in the same spots
{
edgeList.add(new Edge(point1,point2));//then you add a new edgeList element to the ArrayList, by definition of Edge() object
repaint();//must use this method because changes to component have been made
}
}
}
public void mouseMoved (MouseEvent event)// if the mouse is moved, without it being clicked (not to be confused by mouseDragged)
{
point3 = event.getPoint();//point three is set to be where the mouse is at the current time(used this to find the distance of original edge drawn, keeping here incase I want to add)
repaint();//must use this method because changes were made to component
}
// Empty definitions for unused event methods.
public void mouseEntered (MouseEvent event){}//when the mouse enters a component
public void mouseExited (MouseEvent event) {}//when the mouse exits a component
private int distance(Point p1, Point p2) //private distance formula but inside of this class so i was able to use it for determining how to erase nodes and edges
{
return (int)Math.sqrt((p1.x-p2.x)*(p1.x-p2.x)+(p1.y-p2.y)*(p1.y-p2.y));//square root of x cordinants of first point minus second point times its self plus the y coordinates
//of first point minus second point times itself
}
}
// Represents the graph edges
private class Edge //defines how edges are made
{
Point a, b;
public Edge(Point a, Point b)
{
this.a = a;
this.b = b;
}
}
private class ButtonListener implements ActionListener
{
public void actionPerformed (ActionEvent event)
{
if (event.getSource()== create)
{
mode = true;
mode1 = 1;
}
if (event.getSource()==delete)
{
mode = !true;
mode1 = 2;
}
// Initializes graph adjacency matrix
for (int i=0; i<nodeList.size(); i++)
for (int j=0; j<nodeList.size(); j++) a[i][j]=0;
// Includes the edges in the graph adjacency matrix
for (int i=0; i<edgeList.size(); i++)
{
for (int j=0; j<nodeList.size(); j++)
if (distance(nodeList.get(j),edgeList.get(i).a)<=SIZE+3)
for (int k=0; k<nodeList.size(); k++)
if (distance(nodeList.get(k),edgeList.get(i).b)<=SIZE+3)
{
System.out.println(j+"->"+k);
a[j][k]=1;
}
}
if (event.getSource()==print)
{
// Prints the graph adjacency matrix
for (int i=0; i<nodeList.size(); i++)
{
for (int j=0; j<nodeList.size(); j++)
System.out.print(a[i][j]+"\t");
System.out.println();
}
}
}
// Euclidean distance function
private int distance(Point p1, Point p2)
{
return (int)Math.sqrt((p1.x-p2.x)*(p1.x-p2.x)+(p1.y-p2.y)*(p1.y-p2.y));
}
}
private class ButtonListener1 implements ActionListener
{
public void actionPerformed (ActionEvent event)
{
if (event.getSource()== create)//if create button is selected
{
mode = true;//then the mode is set to true
mode1 = 1;//and mode1 is set to 1
}
}
}
private class ButtonListener2 implements ActionListener
{
public void actionPerformed (ActionEvent event)
{
if (event.getSource()== delete)//if create button is selected
{
mode = !true;//then the mode is set to true
mode1 = 2;//and mode1 is set to 2, which would change the string that is output
}
}
}
private class ButtonListener3 implements ActionListener
{
public void actionPerformed (ActionEvent event)
{
x1 = label.getText();
y1 = label1.getText();
x = Integer.parseInt(x1);
y = Integer.parseInt(y1);
System.out.println("Path from " +x+" to " +y);
System.out.println(findPath(x,y));
path.add(0,x);
System.out.println(path);
path.clear();
}
}
// private class ButtonListener4 implements ActionListener
// {
// public void actionPerformed (ActionEvent event)
// {
// x1 = label.getText();
// y1 = label1.getText();
// x = Integer.parseInt(x1);
// y = Integer.parseInt(y1);
// System.out.println("Path from " +x+" to " +y);
// System.out.println(findPath1(x,y,path));
// System.out.println(path);
// }
// }
}

javafx, dragging optimisation - various ideas

I have an app with lots of draggable Nodes, which can get a bit slow. In particular when:
dragging a Node containing other nodes or
dragging Node inside another IF it results in widening of the parent's Bounds [almost as if it was caching some range-related data, common for double values inside, and needed to do extra processing if range changed] (EDIT: this has now been explained to be the parent's layout pass)
Q: Is there anything to gain by doing some of the operations (the few for which I can control types) on integers (converting to ints first) and then re-assigning to doubles?
(EDIT: this has now been answered)
Q1: How else could I speed it up?
==========UPDATE:
Thanks to advice so far I determined my main problem is panning a Region container inside a Region root, as it triggers a layout pass in the container Region, which in turn (I think) ping-pongs to do a layout pass in all the (ca. 40) children Nodes (which are complex in their own right - they contain textfields, buttons, etc.).
So far the fastest solution I found is panning a Group in a Group (already a lot faster), where the Groups are additionally modified (subclass, override compute... methods) to make sure the parent/root will not need to be resized during dragging (sadly, not so noticeable).
class FastGroup extends Group {
//'stop asking about size' functionality
double widthCache;
double heightCache;
protected double computePrefWidth(double height) {
return widthCache != 0 ? widthCache : super.computePrefWidth(height);
}
protected double computePrefHeight(double width) {
return heightCache != 0 ? heightCache : super.computePrefHeight(width);
}
public void initDragging(){
//if a child of this Group goes into drag
//add max margins to Group's size
double newW = getBoundsInLocal().getWidth() + TestFXApp03.scene.getWidth()*2;
double newH = getBoundsInLocal().getHeight() + TestFXApp03.scene.getHeight()*2;
//cache size
widthCache = newW;
heightCache = newH;
}
public void endDragging(){
widthCache = 0;
heightCache = 0;
}
}
However Group creates problems of its own.
Q3: why can't I achieve the same with Pane (tried that as a 3rd option)? According to the Docs, a Pane:
'does not perform layout beyond resizing resizable children to their
preferred size'
...while a Group:
'will "auto-size" its managed resizable children to their preferred
sizes during the layout pass'
'is not directly resizable'
...which sounds quite the same to me, and yet the Pane used as root results in very slow panning of the contained Group.
Ok... I spent some time in checking the performance of mouse-drags with many objects....
I did it on a Mac Pro (4 core, 8GB RAM).
A mouse drag using setTranslateX() and setTranslateY() in the setOnMouseDragged() handler is called at an interval of 17ms (60Hz).
If I add small circles to the drag-group, the drag keeps its pace up to 25.000 circle dragged along.
At 100.000 circles the drag interval is about 50ms (and shows some stops during drag)
At 1.000.000 circles the drag interval is about 500ms
If I implement the drag as image drag using the snapshot() function, the snapshot of 1.000.000 circles takes 600ms. Then the drag is smooth and fast.
Here the full code with and without using an image during drag:
public class DragMany extends Application {
Point2D lastXY = null;
Scene myScene;
long lastDrag;
ImageView dragImage;
void fill(Group box) {
Color colors[] = new Color[]{Color.RED, Color.BLUE, Color.GREEN, Color.YELLOW, Color.PINK, Color.MAGENTA };
for (int i = 0; i < 100000; i++) {
Circle c = new Circle(2);
box.getChildren().add(c);
c.setFill(colors[(i/17) % colors.length]);
c.setLayoutX(i % 30 * 3);
c.setLayoutY((i/20) % 30*3);
}
box.setLayoutX(40);
box.setLayoutY(40);
}
void drag1(Pane pane, Group box) {
box.setOnMousePressed(event -> {
lastXY = new Point2D(event.getSceneX(), event.getSceneY());
lastDrag = System.currentTimeMillis();
});
box.setOnMouseDragged(event -> {
event.setDragDetect(false);
Node on = box;
double dx = event.getSceneX() - lastXY.getX();
double dy = event.getSceneY() - lastXY.getY();
on.setLayoutX(on.getLayoutX()+dx);
on.setLayoutY(on.getLayoutY()+dy);
lastXY = new Point2D(event.getSceneX(), event.getSceneY());
event.consume();
});
box.setOnMouseReleased(d -> lastXY = null);
}
void drag3(Pane pane, Group box) {
box.setOnMousePressed(event -> {
long now = System.currentTimeMillis();
lastXY = new Point2D(event.getSceneX(), event.getSceneY());
SnapshotParameters params = new SnapshotParameters();
params.setFill(Color.TRANSPARENT);
WritableImage image = box.snapshot(params, null);
dragImage = new ImageView(image);
dragImage.setLayoutX(box.getLayoutX());
dragImage.setLayoutY(box.getLayoutY());
dragImage.setTranslateX(box.getTranslateX());
dragImage.setTranslateY(box.getTranslateY());
pane.getChildren().add(dragImage);
dragImage.setOpacity(0.5);
box.setVisible(false);
System.out.println("Snap "+(System.currentTimeMillis()-now)+"ms");
pane.setOnMouseDragged(e -> {
if (dragImage == null) return;
Node on = dragImage;
double dx = e.getSceneX() - lastXY.getX();
double dy = e.getSceneY() - lastXY.getY();
on.setTranslateX(on.getTranslateX()+dx);
on.setTranslateY(on.getTranslateY()+dy);
lastXY = new Point2D(e.getSceneX(), e.getSceneY());
e.consume();
});
pane.setOnMouseReleased(e -> {
if (dragImage != null) {
box.setTranslateX(dragImage.getTranslateX());
box.setTranslateY(dragImage.getTranslateY());
pane.getChildren().remove(dragImage);
box.setVisible(true);
dragImage = null;
}
lastXY = null;
e.consume();
});
lastDrag = System.currentTimeMillis();
event.consume();
});
}
public void start(Stage primaryStage) {
Pane mainPane = new Pane();
myScene = new Scene(mainPane, 500, 500);
primaryStage.setScene(myScene);
primaryStage.show();
Group all = new Group();
fill(all);
mainPane.getChildren().add(all);
drag3(mainPane, all);
}
public static void main(String[] args) {
launch(args);
}
Using ints or floats instead of doubles won't give you much performance boost.
Instead, you should try optimizing when you change the position of nodes. For example, when you are dragging, instead of changing the position of all nodes, you could just render all the nodes that are being dragged at an offset and only change position of one node. Once you are done dragging, you could recalculate positions of all nodes you dragged.
Here is my guess of how you do it right now:
void drag(float x, float y) {
setPosition(x, y); // I'm gueessing this is where your bottle neck is
for (Node node : nodes) {
node.drag(x, y);
}
}
void render() {
screen.draw(this.x, this.y);
for (Node node : nodes) {
node.render();
}
}
Here is how I would optimize it by introducing render offset, so that you only set position of all the nodes when you are finished dragging:
float xo, yo; // render offsets
void drag(float x, float y) {
xo = x;
yo = y;
}
void dragEnd(float x, float y) {
setPosition(x, y);
for (Node node : nodes) {
node.dragEnd(x, y);
}
}
void render(float xo, float yo) {
xo += this.xo;
yo += this.yo;
screen.draw(this.x + xo, this.y + yo); // render with at offset
for (Node node : nodes) {
node.render(xo, yo);
}
}

Categories