0

I've made a program that creates a simple visual quadtree - basically starts with a grid of 4 rectangles which can each be divided into a smaller version of a grid containing 4 smaller rectangles. PROBLEM: Sometimes though, one of the original 4 squares refuses to divide, while the others continue working. The issue only occurs after I've tried right-clicking several times when the grid is already combined to one base square that cannot be combined any further.

I've attached a .setOnMouseClicked() handler on each square that calls my divide() method on left-click and combine() on right-click. Furthermore the handler method are set to check if the square that's been clicked belongs to a grid of squares before it tries to call the combine method, and this is where I struggle to understand what goes wrong.

My theories so far:

  • Perhaps the issue relates to how javafx handles visual objects, but I hoped that using .getChildren().remove(Object o); was the right way to remove something from the scene. It seems to work most of the time though.
  • Maybe the way I'm changing what's displayed and not isn't really thread safe, and that using an eventHandler interrupts the object removal - but the problem occurs when I'm clicking slowly as well as rapidly.

The class hierarchy consists of one abstract Cell class that can either be a Grid or a Square. Excuse me for dropping a lot of code here, but javafx debugging confuses me a lot some times, so honestly I got no idea which part of the code is responsible, thus I pasted the entire thing.

Cell.java:

public abstract class Cell {
    int x, y, w, h, id; //id is a reference the index in a grid this cell belongs to - it's used when a square replaces itself with a grid
    Pane pane;
    Cell parent;

    public Cell(Pane pane, Cell parent, int x, int y, int w, int h, int id) {
        this.x = x;
        this.y = y;
        this.w = w;
        this.h = h;
        this.id = id;
        this.pane = pane;
        this.parent = parent;
    }

    public void divide(){}
    public abstract void remove();
}

Grid.java

public class Grid extends Cell {
    Cell[] grid = new Cell[4];

    public Grid(Pane pane, Cell parent, int x, int y, int w, int h, int id) {
        //The constructor works as it should, just some unnecessary maths to avoid confusion over rounding
        super(pane, parent, x, y, w, h, id);
        int ver = x + w/2;
        int hor = y + h/2;
        grid[0] = new Square(pane, this, x, y, w/2, h/2, 0);
        grid[1] = new Square(pane, this, ver+1, y, x+w-1-ver, h/2, 1);
        grid[2] = new Square(pane, this, x, hor+1, w/2, y+h-1-hor, 2);
        grid[3] = new Square(pane, this, ver+1, hor+1, x+w-1-ver, y+h-1-hor, 3);
    }

    public void combine(){
        remove();
        parent = new Square(pane, parent, x, y, w, h, id);
    }

    public void remove(){
        for(Cell c : grid){
            c.remove();
        }
    }
}

Square.java:

public class Square extends Cell {
    Rectangle rect;

    public Square(Pane pane, Cell parent, int x, int y, int w, int h, int id) {
        super(pane, parent, x, y, w, h, id);
        rect = new Rectangle(x, y, w, h);
        rect.setFill(Color.RED);
        pane.getChildren().add(rect);
        rect.setOnMouseClicked(e ->{
            if(e.getButton().equals(MouseButton.PRIMARY))
                divide();
            else if(e.getButton().equals(MouseButton.SECONDARY) && parent != null && parent.getClass().equals(Grid.class))
                ((Grid)parent).combine();
        });
    }

    @Override
    public void divide(){
        if(w > 3 && h > 3){
            remove();
            if(parent != null && parent.getClass().equals(Grid.class))
                ((Grid)parent).grid[id] = new Grid(pane, parent, x, y, w, h, id);
            else
                parent = new Grid(pane, parent, x, y, w, h, id);
        }
    }

    public void remove(){
        pane.getChildren().remove(rect);
    }
}

In the main file, the parent is initialized as null for the base Cell to signalize that it cannot be divided from the mouse handler:

@Override
public void start(Stage primaryStage) throws Exception {
    Pane pane = new Pane();
    pane.setBackground(new Background(new BackgroundFill(Color.BLACK, null, null)));
    cell = new Square(pane, null, 50, 50, 700, 700, 0);
    cell.divide(); //cell is a Cell type instance variable
    primaryStage.setTitle("SquareSception");
    primaryStage.setScene(new Scene(pane, 800, 800));
    primaryStage.show();
}

The outlined squares refuses to divide despite many left-clicks The outlined squares refuses to divide despite many left-clicks The top right square refuses to divide, the others would The top right square refuses to divide, the others would

Chexxor
  • 406
  • 6
  • 17
  • I should also mention than in the pictures I was holding my mouse in the top right area as I was right-clicking, seems like a correlation between where I right-click and were left-clicking stops responding... – Chexxor Jul 25 '16 at 10:56

1 Answers1

1

First of all you should change the type of parent to Grid.This avoids the unnecessary type checking. (Every parent is a Grid after all.)

The real issue however is, that you never properly update the parent grid in Grid.combine. You need to do this update in order for the remove method to properly find all child squares:

public void combine(){
    remove();
    Square newParent = new Square(pane, parent, x, y, w, h, id);
    if (parent != null) {
        parent.setChild(newParent);
    }
}

public void setChild(Cell newChild) {
    grid[newChild.id] = newChild;
}
fabian
  • 80,457
  • 12
  • 86
  • 114
  • Your answer is dead on, but I need the type checking as I set the parent to be a new Square during combine().. I could've removed it all if I didn't have to check for the base case where there is no parent, or in other words I have to combine the outer grid to be just one square... Setting the parents array at index id doesn't make sense then. – Chexxor Aug 03 '16 at 10:02
  • @Chexxor However a `Square` does not truely serve as parent. `parent = new Square(pane, parent, x, y, w, h, id);` won't accomplish anything but setting the field. Instead of really using it as parent of the current node, you're replacing the current node with a square in this case of the `combine` method. Also note that after removing the grid you won't have any way to interact with it. – fabian Aug 03 '16 at 10:09
  • Nevermind, just saw a mistake in my adaption of your code :O It should work indeed – Chexxor Aug 03 '16 at 11:55
  • But shouldn't parent and parent.parent.grid[id] serve as a reference to the exact same memory location? Such that replacing the contents of parent would also replace the content of the parent.parent.grid[id], I mean like in C++ where two pointer variables / references can point to the same content, which in turn can be replaced? – Chexxor Aug 03 '16 at 12:02
  • @Chexxor Indeed pointers and java (non-primitive) variables are similar, but you'll always have one "root" which does not have a parent. But: when do you use a `Square` stored as `parent` except for checking the type? So if you do not treat a parent that is a `Square` different to a parent that is `null`, why bother and store a `Square` in the `parent` field? Furthermore look at the equivalent structure in the JavaFX scene graph: `Node` has a `parent` property of type `Parent` and `Parent`s have `children` of type `Node` and `Parent extends Node`. – fabian Aug 03 '16 at 12:35
  • @Chexxor BTW: i assume you're not confusing pointers and references in C++. – fabian Aug 03 '16 at 12:40
  • I assume that as well, I suppose I was just hoping that the parent and parent.parent.grid[id] worked like c++ reference variables, thus changing one changed the other implicitly... But I suppose they work like pointers, so replacing one only replaces that reference, and not the reference contents. – Chexxor Aug 03 '16 at 12:43