JavaFX: Coordinates of nodes are wrong for no obvious reason - java

I'm trying to visualize a graph in JavaFX. The nodes shall be interactive. At the moment I have a VBox and place as many HBoxes into it as I need levels in my graph. All the HBoxes are set to position their childrens centered. The childrens represent the individual nodes and are Buttons.
The VBox itself is placed in a StackPane with a Canvas as another children of the StackPane.
This StackPane is then placed into the Scene.
I want to use the Canvas to draw the joining Edges between the nodes. To get The coordinates of the nodes i use:
System.out.println(button.localToScene(button.getBoundsInLocal().getMinX(),
button.getBoundsInLocal().getMinY()));
But no matter where the button is positioned, the resulting coordinate remains:
Point2D [x = 0.0, y = 108.0]
for all the buttons. Every single Button has apparently the same coordinate though it's displayed correctly!
Please help me, im quite desperate right now...
Edit:
Here is the minimal example that works:
Main.java
public class Main extends Application {
#Override
public void start(Stage primaryStage) throws Exception {
VBox root = new VBox();
HBox buttons = new HBox();
TabPane tabs = new TabPane();
buttons.getChildren().addAll(
new Button("1"),
new Button("2"),
new Button("3"));
tabs.getTabs().addAll(
new TabBoxes("____A____"),
new TabBoxes("____B____"),
new TabBoxes("____C____")
);
root.getChildren().addAll(buttons, tabs);
primaryStage.setScene(new Scene(root, 500, 400));
primaryStage.show();
}
public static void main(String[] args) {
launch(args);
}
}
TabBoxes.java
public class TabBoxes extends Tab {
private VBox vBox = new VBox();
private ArrayList<HBox> hBoxes;
private Canvas canvas = new Canvas();
private StackPane stack = new StackPane();
private static final int V_INIT_SPACING = 60;
private static final int V_SPACING = 60;
private static final int H_SPACING = 60;
TabBoxes(String s){
super(s);
this.setContent(stack);
setClosable(false);
stack.getChildren().add(canvas);
stack.getChildren().add(vBox);
canvas.setWidth(2000);
canvas.setHeight(1080);
hBoxes = new ArrayList<>();
vBox.setSpacing(V_SPACING);
vBox.translateYProperty().set(V_INIT_SPACING);
for (int i = 0; i < 5; i++) {
hBoxes.add(new HBox() {
{
setSpacing(H_SPACING);
setAlignment(Pos.CENTER);
}
});
}
for (int i = 0; i < 5; i++) {
for (int j = 0; j <= i; j++) {
hBoxes.get(i).getChildren().add(new Button(i + "||" + j){
{
setPrefSize(100,60);
setOnAction(e-> System.out.println("My coordinates are: " + localToScene(getBoundsInLocal().getMinX(),getBoundsInLocal().getMinY())));
}
});
}
}
vBox.getChildren().addAll(hBoxes);
}
}

boundsInLocal are coordinates in the Button's own coordinate system. It's not surprising, that these values are the same.
If you want to calculate the coordinates in the Canvas coordinate system, transform the coordinates to the StackPane coordinates and then transform the coordinates to the Canvas coordinates:
Node n = button;
Point2D p = new Point2D(button.getBoundsInLocal().getMinX(), button.getBoundsInLocal().getMinY());
while (n != stackPane) {
p = n.localToParent(p);
n = n.getParent();
}
p = canvas.parentToLocal(p);

Related

How can I fill a combobox with the selection from another combobox? JavaFX

I've started creating a GUI for a March Madness bracket generator by displaying all 64 teams for round1 as Labels and now I'm trying to create a ComboBox dropdown menu for each match.
I've created a ComboBox for 2 matches and now I want to create a new ComboBox that pulls its options from the other two ComboBox's before it. So in the example diagram below, the new ComboBox should have the options Duke and VCU for the user to choose from.
(2 combo boxes) (new combo box)
Duke------
Duke ---
ND St. ---
X
VCU -----
VCU ---
UCF -----
How can I do so?
public class ControlPanel extends Application
{
#Override
public void start(Stage primaryStage) {
primaryStage.setTitle("March Madness 2019 Generator");
BorderPane componentLayout = new BorderPane();
componentLayout.setPadding(new Insets(20,0,20,20));
final FlowPane choicePane = new FlowPane();
choicePane.setHgap(100);
Label choiceLbl = new Label("Match1");
ArrayList<Team> round1 = new ArrayList<Team>();
round1.add(new Team("Duke", 0.670, 1)); //0
round1.add(new Team("North Dakota St", 0.495, 16));
round1.add(new Team("VCU", 0.609, 8));
round1.add(new Team("UCF", 0.606, 9));
//The choicebox is populated from an observableArrayList
ChoiceBox r2Match1 = new ChoiceBox(FXCollections.observableArrayList( match(round1, 0, 1) ));
//Add the label and choicebox to the flowpane
choicePane.getChildren().add(choiceLbl);
choicePane.getChildren().add(r2Match1);
//put the flowpane in the top area of the BorderPane
componentLayout.setTop(choicePane);
//Add the BorderPane to the Scene
Scene appScene = new Scene(componentLayout,500,500);
//Add the Scene to the Stage
primaryStage.setScene(appScene);
primaryStage.show();
}
private ArrayList<Team> match(ArrayList<Team> roundPullFrom, int team1, int team2) {
ArrayList<Team> temp = new ArrayList<Team>();
temp.add(roundPullFrom.get(team1));
temp.add(roundPullFrom.get(team2));
return temp;
}
}
Combine ComboBoxes pairwise using the approach posted in my previous answer until you're left with a single ComboBox.
The following code layouts the nodes in something that resembles a tree structure too, but you could easily decouple the layout by keeping every round in a data structure instead of overwriting the values of a single array. (Since you'll want to access the data, you should store the combos in a proper data structure anyways.)
private static ComboBox<String> createCombo(double x, double y, double width) {
ComboBox<String> comboBox = new ComboBox<>();
comboBox.setLayoutX(x);
comboBox.setLayoutY(y);
comboBox.setMaxWidth(Region.USE_PREF_SIZE);
comboBox.setMinWidth(Region.USE_PREF_SIZE);
comboBox.setPrefWidth(width);
return comboBox;
}
private static Label createLabel(String text, double maxWidth) {
Label label = new Label(text);
label.setMaxWidth(maxWidth);
return label;
}
#Override
public void start(Stage primaryStage) {
String[] teams = new String[64];
for (int i = 0; i < teams.length; i++) {
teams[i] = Integer.toString(i);
}
final double offsetY = 30;
final double offsetX = 100;
final double width = 90;
Pane root = new Pane();
// array storing the comboboxes
// combos for previous round are at the lowest indices
ComboBox<String>[] combos = new ComboBox[teams.length / 2];
// create initial team labels & comboboxes
for (int i = 0, offsetTeams = 0; i < combos.length; i++, offsetTeams += 2) {
Label label = createLabel(teams[offsetTeams], width);
double y = offsetTeams * offsetY;
label.setLayoutY(y);
root.getChildren().add(label);
label = createLabel(teams[offsetTeams+1], width);
label.setLayoutY(y+offsetY);
ComboBox<String> comboBox = createCombo(offsetX, y + offsetY / 2, width);
comboBox.getItems().addAll(teams[offsetTeams], teams[offsetTeams+1]);
combos[i] = comboBox;
root.getChildren().addAll(label, comboBox);
}
double x = 2 * offsetX;
int count = combos.length / 2; // combos still left for the next round
for (; count > 0; count /= 2, x += offsetX) { // for each round
// create comboboxes combining the combos from previous round pairwise
for (int i = 0, ci = 0; i < count; i++, ci+=2) {
// get combos pairwise
ComboBox<String> c1 = combos[ci];
ComboBox<String> c2 = combos[ci+1];
ComboBox<String> combo = createCombo(x, (c1.getLayoutY() + c2.getLayoutY()) / 2, width) ;
// combine data from previous round
ChangeListener<String> listener = (o, oldValue, newValue) -> {
final List<String> items = combo.getItems();
int index = items.indexOf(oldValue);
if (index >= 0) {
if (newValue == null) {
items.remove(index);
} else {
items.set(index, newValue);
}
} else if (newValue != null) {
items.add(newValue);
}
};
c1.valueProperty().addListener(listener);
c2.valueProperty().addListener(listener);
root.getChildren().add(combo);
combos[i] = combo;
}
}
primaryStage.setScene(new Scene(new ScrollPane(root), 600, 400));
primaryStage.show();
}
The structure of your problem is a tree. So you might want to have your solution support that structure. Either you use a Binary Tree data structure to resemble the tournament or you create such a structure by e.g. having classes like:
class Team {
String name;
}
class Match {
Team teamA;
Team teamB;
String where;
Date when;
public Team selectWinner() {
...
}
}
class Tournament {
List<Team> teams;
List<Match> getMatches(int round,List<Team> teams) {
List<Match> matches=new ArrayList<Match>)();
if (round==1) {
for (teamIndex=1;teamIndex<=teams.size();teamIndex+=2) {
Match match=new Match(teams[teamIndex-1],teams(teamIndex)];
matches.add(match);
}
} else {
List<Team> winners=new ArrayList<Team>();
for (Match match:getMatches(round-1)) {
winners.add(match.selectWinner());
}
return getMatches(1,winners);
}
}
}
From this structure you can then derive the necessary gui components to make the selection dynamic and let the GUI components take their values from the Tournament, Match and Team classes.

Misalignment of two synced ScrollPanes

I try to align the vertical scroll position of two javafx.scene.control.ScrollPanes via
sp1.vvalueProperty().bindBidirectional(sp2.vvalueProperty());
The problem is that one of these ScrollPanes may have a horizontal scroll bar. So the more I scroll down the ScrollPanes, the more they get misaligned (see screenshot). How can I handle this?
It's impossible to do this with 2 ScrollPanes and contents of equal height unless you display the scrollbar in both ScrollPanes:
Consider the case where the content fits the viewport of the left ScrollPane exactly. The viewPort of the right ScrollPane can be scrolled by the ScrollBar height. Modifying the left ScrollPane is not possible.
Since the expected result seems to be some kind of scale, you could simply use a Pane with a child you apply transformY to and a clip. The formula to calculate the pixel to be placed at the top, use
top = vvalue * (contentHeight - viewportHeight)
Example
private static Label createLabel(int num, boolean mark) {
Label label = new Label(Integer.toString(num));
label.setPrefSize(50, 50);
label.setMaxSize(Double.MAX_VALUE, Region.USE_PREF_SIZE);
label.setStyle(mark ? "-fx-background-color: #FFFFFF" : "-fx-background-color: #BBBBBB;");
return label;
}
#Override
public void start(Stage primaryStage) throws Exception {
VBox scale = new VBox();
scale.setMinHeight(Region.USE_PREF_SIZE);
GridPane content = new GridPane();
for (int i = 0; i < 40; i++) {
boolean b = ((i % 2) == 0);
scale.getChildren().add(createLabel(i, !b));
for (int j = 0; j < 10; j++) {
content.add(createLabel(i * 10 + j, b), j, i);
}
}
AnchorPane scaleContainer = new AnchorPane(scale);
scaleContainer.setMinWidth(30);
scaleContainer.setMinHeight(0);
AnchorPane.setLeftAnchor(scale, 0d);
AnchorPane.setRightAnchor(scale, 0d);
Rectangle clip = new Rectangle();
scaleContainer.setClip(clip);
clip.widthProperty().bind(scaleContainer.widthProperty());
clip.heightProperty().bind(scaleContainer.heightProperty());
ScrollPane scroll = new ScrollPane(content);
scale.translateYProperty().bind(Bindings.createDoubleBinding(() -> {
double contentHeight = content.getHeight();
double viewportHeight = scroll.getViewportBounds().getHeight();
if (contentHeight <= viewportHeight) {
return 0d;
} else {
return -scroll.getVvalue() * (contentHeight - viewportHeight);
}
}, scroll.viewportBoundsProperty(), scroll.vvalueProperty(), content.heightProperty()));
HBox root = new HBox(scaleContainer, scroll);
root.setPadding(new Insets(10));
Scene scene = new Scene(root, 400, 400);
primaryStage.setScene(scene);
primaryStage.show();
}

JavaFX 8 Z-buffer issue

My problem is with Z-Buffer in JavaFX 3D, it does not seem to work as intended on my machine.
I'am aware of questions:
Overlapping shapes
and
...Z Order...
However I do have Z-Buffer enabled, and nodes are still being rendered in the order they are added to the scenegraph.
Maybe I'm missing some dependencies or whatsoever?
I'm posting the code, I hope someone can help me. I'm creating a transition that moves the node around another on an elliptic path.
Thank you in advance!
public class OrbitExp extends Application {
Group root = new Group();
Scene scene = new Scene(root, 800, 600, true, SceneAntialiasing.BALANCED);
PerspectiveCamera camera = new PerspectiveCamera();
#Override
public void start(Stage primaryStage) {
root.setDepthTest(DepthTest.ENABLE);
//Tried to set Depthtest explicitly. Assumed maybe it did not inherit:S
System.out.println(
"3D supported? " +
Platform.isSupported(ConditionalFeature.SCENE3D)
); // returns true
System.out.println("root z-buffer: " + root.getDepthTest());
initCamera();
Box
box1 = new Box(50,50,50),
box2 = new Box(10,10,10);
root.setTranslateX(scene.getWidth()/2);
root.setTranslateY(scene.getHeight()/2);
PhongMaterial
pmat = new PhongMaterial(Color.BLUE),
pmat2 = new PhongMaterial(Color.RED);
box1.setMaterial(pmat);
box2.setMaterial(pmat2);
scene.setFill(Color.LIGHTGREEN);
root.getChildren().addAll(box1,box2);
SequentialTransition sqt = orbit(box1, box2, 40, 40, Duration.seconds(3), 360);
sqt.play();
scene.setOnMouseClicked(click->{
Node node = (Node)(click.getPickResult().getIntersectedNode());
System.out.println("Tx: "+node.getTranslateX());
System.out.println("Ty: "+node.getTranslateY());
System.out.println("Tz: "+node.getTranslateZ());
});
// just for debugging, but coords does seem to be alright
primaryStage.setScene(scene);
primaryStage.show();
}
public static void main(String[] args) {
launch(args);
}
private void initCamera() {
camera.setTranslateZ(-50);
camera.setTranslateY(20);
camera.setFarClip(5000);
camera.setNearClip(0);
scene.setCamera(camera);
}
SequentialTransition orbit(Node node1, Node node2,double a, double b, Duration totalDuration, int N) {
SequentialTransition sqt = new SequentialTransition();
Duration dur = new Duration(totalDuration.toMillis()*(1.0d/N));
node2.setTranslateX(a+node1.getTranslateX());
node2.setTranslateZ(node1.getTranslateZ());
for (int i = 1; i < N; i++) {
TranslateTransition tt = new TranslateTransition(dur, node2);
double
angle = i*(360.0d/N),
toX = (Math.cos(Math.toRadians(angle))*a)+node1.getTranslateX(),
toZ = (Math.sin(Math.toRadians(angle))*b)+node1.getTranslateZ();
tt.setToX(toX);
tt.setToZ(toZ);
tt.setInterpolator(Interpolator.LINEAR);
sqt.getChildren().add(tt);
System.out.println("angle = " + angle + "\nangle in rads: " + Math.toRadians(angle) + "\ntoX = " + toX + "\ntoZ = " + toZ);
}
sqt.setCycleCount(Timeline.INDEFINITE);
return sqt;
}
}
This was my first post by the way:)
If you check the code on the link you provided using rectangles, depth buffer works fine.
Changing the rectangles to use 3D boxes works as well.
The issue is how you define the rotation of one of the boxes related to the other, so instead of using a RotateTransition or a SequentialTransition of TranslateTransition like you do, I've applied a Rotate transform to the red box setting a pivot in the center of the blue one, and used an AnimationTimer to modify the angle of that rotation to create the 'orbit' effect.
You can even use transparency on the big box (since 8u60) to see the small one beneath it.
private final Group shapes = new Group();
private long lastTimerCall;
private AnimationTimer timeline;
#Override
public void start(Stage stage) throws Exception {
Scene scene = new Scene(createRotatingShapes(), 400, 300,
true, SceneAntialiasing.BALANCED);
scene.setFill(Color.LIGHTGREEN);
final PerspectiveCamera camera = new PerspectiveCamera();
camera.setRotationAxis(Rotate.X_AXIS);
camera.setRotate(10);
camera.setTranslateZ(200);
scene.setCamera(camera);
stage.setScene(scene);
stage.show();
}
private Group createRotatingShapes() {
final Box box1 = new Box(50, 50, 50);
// Transparency in box1: last node of the group
box1.setMaterial(new PhongMaterial(Color.web("#0000FF80")));
box1.setTranslateZ(50);
final Box box2 = new Box(10, 10, 10);
box2.setMaterial(new PhongMaterial(Color.RED));
box2.setTranslateZ(-50);
shapes.getChildren().addAll(box2, box1);
shapes.setTranslateX(200);
shapes.setTranslateY(150);
rotateAroundYAxis(box2);
return shapes;
}
private int count = 0;
private void rotateAroundYAxis(Node node) {
Rotate r = new Rotate(0, 0, 0, 100, Rotate.Y_AXIS);
node.getTransforms().add(r);
lastTimerCall = System.nanoTime();
timeline = new AnimationTimer() {
#Override public void handle(long now) {
if (now > lastTimerCall + 100_000_000l) {
r.setAngle((count++)%360);
}
}
};
timeline.start();
}
#Override
public void stop() {
timeline.stop();
}
In front of the box:
Behind the blue box:
EDIT
If you have a look to the Camera JavaDoc for nearClip:
Specifies the distance from the eye of the near clipping plane of
this Camera in the eye coordinate space.
Objects closer to the eye than nearClip are not drawn.
nearClip is specified as a value greater than zero. A value less
than or equal to zero is treated as a very small positive number.
(bold is mine).
So the problem with your code was this line:
camera.setNearClip(0);
Just change it to:
camera.setNearClip(0.01);
and it will work as you expected.

JavaFx path transition in gridPane

I can not understand how moveTo method work in javafx. here is my example
I have a GridPane that consist of 4 columns and 1 row. All the columns h has a StackPane and the last Stackpane also has an empty label.
public class PathTransitionTExample extends Application {
StackPane pane;
GridPane grid;
Label label;
Scene scene;
#Override
public void start(Stage primaryStage) {
label = new Label();
label.setPrefSize(Double.MAX_VALUE, Double.MAX_VALUE);
grid = new GridPane();
grid.getStyleClass().add("gridPane");
for (int x = 0; x < 4; x++) {
grid.add(pane = new StackPane(), x, 0);
pane.setPrefSize(50, 50);
pane.getStyleClass().add("stackPane");
}
pane = (StackPane) grid.getChildren().get(3);
pane.getChildren().add(label);
scene = new Scene(grid, 260, 50);
scene.getStylesheets().add(PathTransitionTest.class.getResource("pathCSS.css").toExternalForm());
scene.setOnMouseClicked(me -> pathTransition());
primaryStage.setTitle("Path Transition");
primaryStage.setScene(scene);
primaryStage.show();
}
//pathCSS.css
.gridPane{
-fx-background-color: red;
-fx-hGap: 20;
}
.stackPane{
-fx-background-color: orange;
}
.label {
-fx-background-color: blue;
}
I want to move the label in the grid pane from 0.3 to 0.0 however when I am trying to do that the whole transition slips.
private void pathTransition() {
//the Coordinates of the label
double oldMinX = grid.getChildren().get(3).getLayoutX();
double oldMinY = grid.getChildren().get(3).getLayoutY();
//the coordinates of the stack pane where i want the label move
double newMinX = grid.getChildren().get(1).getLayoutX();
double newMinY = grid.getChildren().get(1).getLayoutY();
Path path = new Path();
path.getElements().add(new MoveTo(oldMinX, oldMinY ));
path.getElements().add(new LineTo(newMinX,newMinY ));
PathTransition pathTransition = new PathTransition();
pathTransition.setDuration(new Duration(500));
pathTransition.setPath(path);
pathTransition.setNode(label);
pathTransition.play();
}
If I change the arguments of MoveTo and LineTo by the following I can achieve the animation I want but I cant understand why. :\
double oldMinX = grid.getChildren().get(3).getLayoutX() -185;
double oldMinY = grid.getChildren().get(3).getLayoutY()+ grid.getChildren().get(3).getBoundsInLocal().getHeight()/2 ;
double newMinX = grid.getChildren().get(1).getLayoutX()-255 ;
double newMinY = grid.getChildren().get(1).getLayoutY() + grid.getChildren().get(0).getBoundsInLocal().getHeight()/2 ;
I guess It is because transitions use different coordinate systems than scenes but I cant really find anything that explains well :( Could someone give me some hints how It is working?
Thank you so much in advance.
I realized that i shouldn't use GridPane. If i do it without using containers the transition is working fine.

StackedAreaChart's default coloring policy after 8 colors on subsequent data repopulations

I am trying to understand how the StackedAreaChart colors its series, in order to color series consistently in my application when data is replaced with entirely new data. Initially, I thought StackedAreaChart cycles through 8 default colors. In other words, data series are colored according to their index in getData(), mod 8. But I am encountering unexpected behavior:
The output above comes from the application below, which clears the StackedAreaChart's data and repopulates it with 10 new series every time the window is clicked. As you can see, only the first 8 colors are consistent across clicks/repopulations.
public class TestChartColors extends Application {
private int clickCount = 0;
#Override
public void start(Stage primaryStage) {
NumberAxis xAxis = new NumberAxis(0, 10, 1);
NumberAxis yAxis = new NumberAxis(0, 10, 1);
final StackedAreaChart<Number,Number> chart = new StackedAreaChart<>(xAxis,yAxis);
chart.setLegendVisible(false);
chart.setCreateSymbols(false);
chart.setAnimated(false);
chart.setOnMouseClicked((MouseEvent event) -> {
clickCount++;
chart.getData().clear();
for(int i=0; i<10; i++){
chart.getData().add(flatSeries());
}
primaryStage.setTitle("After " + clickCount + " clicks.");
});
StackPane root = new StackPane();
root.getChildren().add(chart);
Scene scene = new Scene(root, 300, 250);
primaryStage.setScene(scene);
primaryStage.setTitle("Click the window.");
primaryStage.show();
}
private Series<Number,Number> flatSeries(){
Series<Number,Number> s = new Series<>();
ObservableList<XYChart.Data<Number, Number>> d = s.getData();
d.add(new XYChart.Data<>(0, 1));
d.add(new XYChart.Data<>(10, 1));
return s;
}
/**
* #param args the command line arguments
*/
public static void main(String[] args) {
launch(args);
}
}
Workaround found:
Insert
while(chart.getData().size() % 8 != 0){
final XYChart.Series<Number,Number> series = new XYChart.Series<>();
series.getData().add(new XYChart.Data<>(0,0));
chart.getData().add(series);
}
before chart.getData().clear();.
StackedAreaChart seems to continue the numbering where it left off, except, strangely, for the first 8 colors.

Categories

Resources