-1

I have the following class in which I am trying to implement the prototype pattern:

    public class Element extends Group implements Cloneable{

    private final double ELEMENT_WIDTH = 50;
    private final double ELEMENT_HEIGHT = 70;

    private final double CIRCLE_RADIUS = 1;

    private int minimalNumberOfInputs;

    private Shape body = new Rectangle(ELEMENT_WIDTH, ELEMENT_HEIGHT, Color.WHITE);
    private Circle output = new Circle(CIRCLE_RADIUS);

    private Line outputLine = new Line(ELEMENT_WIDTH, ELEMENT_HEIGHT / 2, ELEMENT_WIDTH + 15, ELEMENT_HEIGHT / 2);

    private ArrayList<Circle> inputs;
    private ArrayList<Line> inputLines;
    private Circle inversionDesignation; 
    private Text symbol; 
    private Integer identifier;

    private double bodyCorX; 
    private double bodyCorY;

    private double corX = 0; 
    private double corY = 0;

    private double mouseX = 0; 
    private double mouseY = 0;

    private boolean dragging = false;
    
    public Integer getIdentifier() {
        return identifier;
    }

    public ArrayList<Circle> getInputs() {
        return inputs;
    }

    public Circle getOutput() {
        return output;
    }

    public Circle getInversionDesignation() {
        return inversionDesignation;
    }

    public double getELEMENT_WIDTH() {
        return ELEMENT_WIDTH;
    }

    public double getELEMENT_HEIGHT() {
        return ELEMENT_HEIGHT;
    }

    public double getCIRCLE_RADIUS() {
        return CIRCLE_RADIUS;
    }

    public int getMinimalNumberOfInputs() {
        return minimalNumberOfInputs;
    }

    public Shape getBody() {
        return body;
    }

    public Line getOutputLine() {
        return outputLine;
    }

    public ArrayList<Line> getInputLines() {
        return inputLines;
    }

    public Text getSymbol() {
        return symbol;
    }

    public double getBodyCorX() {
        return bodyCorX;
    }

    public double getBodyCorY() {
        return bodyCorY;
    }

    public double getCorX() {
        return corX;
    }

    public double getCorY() {
        return corY;
    }

    public double getMouseX() {
        return mouseX;
    }

    public double getMouseY() {
        return mouseY;
    }

    public boolean isDragging() {
        return dragging;
    }
 
    public Element(){
        
    }
    
    public Element(Circle inversionDesignation, Text symbol, int minimalNumberOfInputs) {
        this.minimalNumberOfInputs = minimalNumberOfInputs;
        this.body.setStroke(Color.BLACK);
        this.body.setStrokeType(StrokeType.INSIDE);
        this.body.setStrokeWidth(2.5);
        this.output.setFill(Color.BLACK);
        this.output.toFront();
        this.inversionDesignation = inversionDesignation;
        this.symbol = symbol;
        this.inputs = new ArrayList<>();
        this.inputLines = new ArrayList<>();
        this.identifier = this.hashCode();
        this.outputLine.setStrokeWidth(2);
        this.createStartInputs();
        this.configureInputPoints();
        this.bindGraphicalElements();
        elementMovementEvents();
        elementEnteredEvents();
    }

    private void bindGraphicalElements() {
        this.getChildren().add(body);
        this.getChildren().add(output);
        this.getChildren().add(outputLine);
        this.getChildren().addAll(inputs);
        this.getChildren().addAll(inputLines);

        if (this.symbol != null) {
            this.getChildren().add(symbol);
            symbol.relocate((ELEMENT_WIDTH / 2) - symbol.getTabSize() / 2, ELEMENT_HEIGHT / 8);
            symbol.setFont(new Font("Consolas", 14));
        }
        if (this.inversionDesignation != null) {
            this.getChildren().add(inversionDesignation);
            this.inversionDesignation.setStrokeType(StrokeType.INSIDE);
            this.inversionDesignation.setStrokeWidth(1);
            this.inversionDesignation.setStroke(Color.BLACK);
            this.inversionDesignation.relocate((this.bodyCorX + this.ELEMENT_WIDTH) - (this.inversionDesignation.getRadius() + 1), (this.bodyCorY + this.ELEMENT_HEIGHT / 2) - this.inversionDesignation.getRadius());
            inversionDesignation.toFront();
        }
    }

    private void addGraphicalElement(Shape shape) {
        this.getChildren().add(shape);
    }

    private void createStartInputs() {
        for (int i = 0; i < this.minimalNumberOfInputs; i++) {
            inputs.add(new Circle(CIRCLE_RADIUS));
            inputs.get(i).setFill(Color.BLACK);
            inputs.get(i).toFront();
        }

        configureInputPoints();
        for (int i = 0; i < inputs.size(); i++) {
            Line line = new Line(inputs.get(i).getLayoutX() - 15, inputs.get(i).getLayoutY(), inputs.get(i).getLayoutX(), inputs.get(i).getLayoutY());
            line.setStrokeWidth(2);
            inputLines.add(line);
        }
    }

    private void addNewInput() {
        Circle newCircle = new Circle(CIRCLE_RADIUS);
        newCircle.setFill(Color.BLACK);
        this.inputs.add(newCircle);
        this.configureInputPoints();
        this.addGraphicalElement(newCircle);
    }

    private void configureInputPoints() {
        this.output.relocate((this.bodyCorX + this.ELEMENT_WIDTH) - (output.getRadius() + 1), (this.bodyCorY + this.ELEMENT_HEIGHT / 2) - output.getRadius());
        int distance = (int) ELEMENT_HEIGHT / (inputs.size() + 1); //Растояние между точками входа.
        for (int i = 0; i < inputs.size(); i++) {
            inputs.get(i).relocate(this.bodyCorX - (CIRCLE_RADIUS - 1), this.bodyCorY + (distance * (i + 1) - CIRCLE_RADIUS));
        }
    }

    private void elementMovementEvents() {
        onMousePressedProperty().set(new EventHandler<MouseEvent>() {
            @Override
            public void handle(MouseEvent event) {
                mouseX = event.getSceneX();
                mouseY = event.getSceneY();

                corX = getLayoutX();
                corY = getLayoutY();
            }
        });

        onMouseDraggedProperty().set(new EventHandler<MouseEvent>() {
            @Override
            public void handle(MouseEvent event) {
                double offsetX = event.getSceneX() - mouseX; //смещение по X
                double offsetY = event.getSceneY() - mouseY;

                corX += offsetX;
                corY += offsetY;

                double scaledX = corX;
                double scaledY = corY;

                setLayoutX(scaledX);
                setLayoutY(scaledY);

                dragging = true;

                mouseX = event.getSceneX();
                mouseY = event.getSceneY();

                event.consume();
            }
        });

        onMouseClickedProperty().set(new EventHandler<MouseEvent>() {
            @Override
            public void handle(MouseEvent event) {
                dragging = false;
            }
        });
    }

    private void testEvent() {
        this.addEventHandler(MouseEvent.MOUSE_CLICKED, event -> {
            this.addNewInput();
        });
    }

    private void elementEnteredEvents() {
        onMouseClickedProperty().set(new EventHandler<MouseEvent>() {
            @Override
            public void handle(MouseEvent event) {
                if (event.getTarget() instanceof Circle) {
                    System.out.println("Circle!");
                }
            }
        });
    }
    
    @Override
    public Element clone() throws CloneNotSupportedException{
        return (Element)super.clone();
    }
    
    @Override
    public String toString() {
        return "Element " + this.hashCode() + ": location = " + this.getLayoutX() + ": output = " + this.minimalNumberOfInputs;
    }

    @Override
    public boolean equals(Object obj) {
        if(!(obj instanceof Element)) return false;
        Element element = (Element) obj;
        return element.getBodyCorX() == bodyCorX && element.getBodyCorY() == bodyCorY &&  element.getBody() == body;
    }  
}

I am trying to implement a pattern using the following method:

@Override
    public Element clone() throws CloneNotSupportedException{
        return (Element)super.clone();
    }

I have a controller like this:

public class FXMLController {

    @FXML
    private AnchorPane anchorPane;
    @FXML
    private AnchorPane workPane;

    //prototypes
    private Element AndPrototype = new Element(null, new Text("&"), 2);
    private Element OrPrototype = new Element(null, new Text("1"), 2);
    private Element NotPrototype = new Element(new Circle(5, Color.WHITE), null, 1);
    private Element AndNotPrototype = new Element(new Circle(5, Color.WHITE), new Text("&"), 2);
    private Element OrNotPrototype = new Element(new Circle(5, Color.WHITE), new Text("1"), 2);

    @FXML
    public void initialize() {
    }

    @FXML
    private void method() throws CloneNotSupportedException {
        workPane.getChildren().add(AndPrototype.clone());
    }
}

In this method, I am trying to make a clone and add it to the AnchorPane

@FXML
    private void method() throws CloneNotSupportedException {
        workPane.getChildren().add(AndPrototype.clone());
    }

As a result, when I click on the button, I get an error of the following content:

Exception in thread "JavaFX Application Thread" java.lang.IndexOutOfBoundsException: Index -1 out of bounds for length 8
    at java.base/jdk.internal.util.Preconditions.outOfBounds(Preconditions.java:64)
    at java.base/jdk.internal.util.Preconditions.outOfBoundsCheckIndex(Preconditions.java:70)
    at java.base/jdk.internal.util.Preconditions.checkIndex(Preconditions.java:266)
    at java.base/java.util.Objects.checkIndex(Objects.java:359)
    at java.base/java.util.ArrayList.get(ArrayList.java:427)
    at javafx.base/com.sun.javafx.collections.ObservableListWrapper.get(ObservableListWrapper.java:89)
    at javafx.base/com.sun.javafx.collections.VetoableListDecorator.get(VetoableListDecorator.java:305)
    at javafx.graphics/javafx.scene.Parent.updateCachedBounds(Parent.java:1704)
    at javafx.graphics/javafx.scene.Parent.recomputeBounds(Parent.java:1648)
    at javafx.graphics/javafx.scene.Parent.doComputeGeomBounds(Parent.java:1501)
    at javafx.graphics/javafx.scene.Parent$1.doComputeGeomBounds(Parent.java:115)
    at javafx.graphics/com.sun.javafx.scene.ParentHelper.computeGeomBoundsImpl(ParentHelper.java:84)
    at javafx.graphics/com.sun.javafx.scene.NodeHelper.computeGeomBounds(NodeHelper.java:115)
    at javafx.graphics/javafx.scene.Node.updateGeomBounds(Node.java:3847)
    at javafx.graphics/javafx.scene.Node.getGeomBounds(Node.java:3809)
    at javafx.graphics/javafx.scene.Node.doComputeLayoutBounds(Node.java:3657)
    at javafx.graphics/javafx.scene.Node$1.doComputeLayoutBounds(Node.java:449)
    at javafx.graphics/com.sun.javafx.scene.NodeHelper.computeLayoutBoundsImpl(NodeHelper.java:166)
    at javafx.graphics/com.sun.javafx.scene.GroupHelper.computeLayoutBoundsImpl(GroupHelper.java:63)
    at javafx.graphics/com.sun.javafx.scene.NodeHelper.computeLayoutBounds(NodeHelper.java:106)
    at javafx.graphics/javafx.scene.Node$13.computeBounds(Node.java:3509)
    at javafx.graphics/javafx.scene.Node$LazyBoundsProperty.get(Node.java:9782)
    at javafx.graphics/javafx.scene.Node$LazyBoundsProperty.get(Node.java:9752)
    at javafx.graphics/javafx.scene.Node.getLayoutBounds(Node.java:3524)
    at javafx.graphics/javafx.scene.layout.AnchorPane.computeWidth(AnchorPane.java:272)
    at javafx.graphics/javafx.scene.layout.AnchorPane.computeMinWidth(AnchorPane.java:248)
    at javafx.graphics/javafx.scene.Parent.minWidth(Parent.java:1048)
    at javafx.graphics/javafx.scene.layout.Region.minWidth(Region.java:1553)
    at javafx.graphics/javafx.scene.layout.Region.computeChildPrefAreaWidth(Region.java:2012)
    at javafx.graphics/javafx.scene.layout.AnchorPane.computeChildWidth(AnchorPane.java:315)
    at javafx.graphics/javafx.scene.layout.AnchorPane.layoutChildren(AnchorPane.java:353)
    at javafx.graphics/javafx.scene.Parent.layout(Parent.java:1207)
    at javafx.graphics/javafx.scene.Scene.doLayoutPass(Scene.java:576)
    at javafx.graphics/javafx.scene.Scene$ScenePulseListener.pulse(Scene.java:2476)
    at javafx.graphics/com.sun.javafx.tk.Toolkit.lambda$runPulse$2(Toolkit.java:413)
    at java.base/java.security.AccessController.doPrivileged(AccessController.java:391)
    at javafx.graphics/com.sun.javafx.tk.Toolkit.runPulse(Toolkit.java:412)
    at javafx.graphics/com.sun.javafx.tk.Toolkit.firePulse(Toolkit.java:439)
    at javafx.graphics/com.sun.javafx.tk.quantum.QuantumToolkit.pulse(QuantumToolkit.java:563)
    at javafx.graphics/com.sun.javafx.tk.quantum.QuantumToolkit.pulse(QuantumToolkit.java:543)
    at javafx.graphics/com.sun.javafx.tk.quantum.QuantumToolkit.pulseFromQueue(QuantumToolkit.java:536)
    at javafx.graphics/com.sun.javafx.tk.quantum.QuantumToolkit.lambda$runToolkit$11(QuantumToolkit.java:342)
    at javafx.graphics/com.sun.glass.ui.InvokeLaterDispatcher$Future.run(InvokeLaterDispatcher.java:96)
    at javafx.graphics/com.sun.glass.ui.win.WinApplication._runLoop(Native Method)
    at javafx.graphics/com.sun.glass.ui.win.WinApplication.lambda$runLoop$3(WinApplication.java:174)
    at java.base/java.lang.Thread.run(Thread.java:831)

I have absolutely no idea what this error may be connected with and in which direction they are moving.

  • 1
    You simply can't implement `clone()` this way for a subclass of `Group`. The default `clone()` method will return a shallow copy of, for example, the `Group`'s list of child nodes, i.e. `getChildren()` invoked on all `Element`s cloned from the same "prototype" will return the same list. This is going to cause all kinds of mayhem. Just don't use `clone()` here. Quoting Bloch, "Effective Java": *"the `clone` architecture is incompatible with the normal use of final fields referring to mutable objects"*. – James_D Oct 27 '21 at 12:26

1 Answers1

1

Usually this is caused because you modified the scene graph or an attribute of a scene graph node off of the JavaFX thread.

Similar stack traces all caused by threading errors:

If it is a multi-threading issue, usually it can be fixed by either removing unnecessary threading. Or if multi-threading is unavoidable, using tools like the javafx.concurrent package or Platform.runLater to ensure nodes in the active scene graph are only modified on the JavaFX thread.

However, if you don’t have any multi-threading going on, it might be down to the weird cloning stuff you have going on which may be ill-advised. JavaFX nodes can only occur once in the scene and the clones may cause glitches in the framework.

jewelsea
  • 150,031
  • 14
  • 366
  • 406