I'm struggeling with the Java FX BarChart.. My own implementation of the chart is a class that extends the Java FX GridPane and holds a BarChart as a member variable.
If I initialize the whole thing everything works perfect, but if I change the data dynamically (add one or remove one data) the layout will be destroyed.
Speaking in pictures this means: (sorry i can't upload picture at the moment)
pic1 - initialization
after adding one element
So the 1st pictures shows the chart after initalization, the 2nd after one element has been added and after deleting one element the categories aren't shown anymore. (I Ccan't upload a picture of this)
So here's my code:
import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
import javafx.scene.chart.BarChart;
import javafx.scene.chart.CategoryAxis;
import javafx.scene.chart.NumberAxis;
import javafx.scene.chart.XYChart.Data;
import javafx.scene.chart.XYChart.Series;
import javafx.scene.control.Tooltip;
import javafx.scene.layout.GridPane;
import javafx.scene.layout.Priority;
import someCompanyThings.IMyBarChart;
import someCompanyThings.LocaleService;
import someCompanyThings.INlsKey;
/**
* A Chart with vertical or horizontal bars. It is assumed that the Bars represent positive integer numbers.
* Data may be added or removed dynamically but on the first intent it should display static.
*/
public class MyBarChart extends GridPane implements IMyBarChart {
/*
* Due to data binding problems with a generic bar chart, we hold the two possible bar charts as member variables.
* Also each of them get's a list of Series<?, ?>
*/
private BarChart<String, Number> _barChartVertical;
private BarChart<Number, String> _barChartHorizontal;
private final ObservableList<Series<String, Number>> _dataVertical = FXCollections.observableArrayList();
private final ObservableList<Series<Number, String>> _dataHorizontal = FXCollections.observableArrayList();
private long _maxValue = 0;
private boolean _numberAxisInPercent = false;
private boolean _horizontal = false;
public MyBarChart(INlsKey pTitle, INlsKey pXLabel, INlsKey pYLabel, boolean pNumberAxisInPercent, boolean pHorizontal) {
super();
CategoryAxis categoryAxis = new CategoryAxis();
categoryAxis.setId("bar-chart-category-axis");
NumberAxis numberAxis = new NumberAxis(0.0, 1.0, 1.0);
numberAxis.setId("bar-chart-number-axis");
// create bar chart
// horizontal means that the x-axis is a number axis and the y-axis is a category axis
if (pHorizontal) {
categoryAxis.setLabel(LocaleService.getMessage(pYLabel));
numberAxis.setLabel(LocaleService.getMessage(pXLabel));
_barChartHorizontal = new BarChart<Number, String>(numberAxis, categoryAxis);
_barChartHorizontal.setData(_dataHorizontal);
_barChartHorizontal.setTitle(LocaleService.getMessage(pTitle));
getChildren().add(_barChartHorizontal);
}
else {
categoryAxis.setLabel(LocaleService.getMessage(pXLabel));
numberAxis.setLabel(LocaleService.getMessage(pYLabel));
_barChartVertical = new BarChart<String, Number>(categoryAxis, numberAxis);
_barChartVertical.setData(_dataVertical);
_barChartVertical.setTitle(LocaleService.getMessage(pTitle));
getChildren().add(_barChartVertical);
}
_numberAxisInPercent = pNumberAxisInPercent;
_horizontal = pHorizontal;
/*
* layout
*/
setHgrow(getChildren().get(0), Priority.ALWAYS);
setVgrow(getChildren().get(0), Priority.ALWAYS);
}
#Override
public IMyBarChart addSeries(INlsKey pSeriesName, ObservableList<Data<String, Number>> pDataSet) {
final Series<String, Number> series = new Series<String, Number>(LocaleService.getMessage(pSeriesName), pDataSet);
_dataVertical.add(series);
// iterate over the whole data segment and add it to the series
for (final Data<String, Number> data : pDataSet) {
Tooltip tooltip = new Tooltip();
tooltip.setText(data.getXValue());
Tooltip.install(data.getNode(), tooltip);
if (data.getYValue().longValue() > _maxValue) {
_maxValue = data.getYValue().longValue();
}
}
setNumberAxisScale();
return this;
}
#Override
public IMyBarChart addSeriesHorizontal(INlsKey pSeriesName, ObservableList<Data<Number, String>> pDataSet) {
final Series<Number, String> series = new Series<Number, String>(LocaleService.getMessage(pSeriesName), pDataSet);
_dataHorizontal.add(series);
// iterate over the whole data segment and add it to the series
for (final Data<Number, String> data : pDataSet) {
Tooltip tooltip = new Tooltip();
tooltip.setText(data.getYValue());
Tooltip.install(data.getNode(), tooltip);
if (data.getXValue().longValue() > _maxValue) {
_maxValue = data.getXValue().longValue();
}
}
setNumberAxisScale();
return this;
}
private void setNumberAxisScale() {
NumberAxis numberAxis = getNumberAxis();
// set the number axis as a percent axis
if (_numberAxisInPercent) {
numberAxis.setUpperBound(100);
numberAxis.setTickUnit(10);
}
else {
numberAxis.setUpperBound(_maxValue + 1);
numberAxis.setTickUnit(1);
}
}
#Override
public void setLegendVisible(boolean pVisible) {
if (_barChartHorizontal != null) {
_barChartHorizontal.setLegendVisible(pVisible);
}
else {
_barChartVertical.setLegendVisible(pVisible);
}
}
#Override
public void setCategories(ObservableList<String> pCategories) {
getCategoryAxis().getCategories().setAll(pCategories);
}
/**
*
* #return the category axis of the used bar chart
*/
private CategoryAxis getCategoryAxis() {
if (_horizontal) {
return (CategoryAxis)_barChartHorizontal.getYAxis();
}
else {
return (CategoryAxis)_barChartVertical.getXAxis();
}
}
/**
*
* #return the number axis of the used bar chart
*/
private NumberAxis getNumberAxis() {
if (_horizontal) {
return (NumberAxis)_barChartHorizontal.getXAxis();
}
else {
return (NumberAxis)_barChartVertical.getYAxis();
}
}
}
The initialization process:
final IMyBarChart tablespacesChart = MyFactory.createBarChart(NlsKeys.tablespacesTitle, NlsKeys.tablespacesXAxis,
NlsKeys.tablespacesYAxis, true, true);
// first bool -> numberAxisInPercent, second bool -> horizontal ortientation
tablespacesChart.setLegendVisible(false);
tablespacesChart.setCategories(model.getListCategories());
tablespacesChart.addSeriesHorizontal(NlsKeys.tablespacesLegendYAxis, model.getListDataUsedMax());
The data changes are realised by another class that just uses
model.getCategories().setAll(MyNewCatList); // or
model.getListDataUsedMax().setAll(MyNewList);
Well, i also tried to implement the chart with just one member variable (like BarChart _barChart) but this didn't work.
Now i have those layout issues and i dunno where they come from. So i hope you can give me a hint :-)
Here's my solution:
First, create a subclass of bar chart to access the private method updateAxisRange:
class MyBarChart<X, Y> extends BarChart<X, Y> {
public MyBarChart(Axis xAxis, Axis yAxis) {
super(xAxis, yAxis);
}
public void relayout() {
updateAxisRange();
}
}
Next, instantiate your bar chart as MyBarChart:
MyBarChart<String, Number> barChart = new MyBarChart<String, Number>(xAxis, yAxis);
And Lastly, you need to listen to resize events on the parent containing the chart, and when they occur, invoke the relayout of the chart.
For example:
BorderPane pane = new BorderPane(barChart);
pane.widthProperty().addListener(new ChangeListener<Number>() {
#Override
public void changed(ObservableValue<? extends Number> arg0, Number arg1, Number arg2) {
barChart.relayout();
}
});
Related
My question is about a strange behavior of a copound in a tableView.
The aim is to display an list of players participating to a match in a tableView. The informations displayed are the name of the player, his score, his number of successive busts and an indicator to know if it is his turn to play.
This indicator is a RadioButton as it looks better than a checkBox. When a turn comes to a player, the RadioButton will be setSelected(true), else, it'll be setSelected(false). The true or false information is given by the player's information used in the tableView. Of course, the RadioButton is in "read-only" mode.
Here is my code for the tableView :
TableView<PlayerProgressInformations> playersProgressTable = new TableView<PlayerProgressInformations>();
defineColumns(playersProgressTable);
playersProgressTable.setItems(playersAtThisTable);
for the defineColumns method :
TableColumn<PlayerProgressInformations, Boolean> colPlaying = new TableColumn<PlayerProgressInformations, Boolean>("Tour");
colPlaying.setPrefWidth(70);
TableCell<PlayerProgressInformations, Boolean>>) new RadioButton());
colPlaying.setCellValueFactory(new Callback<CellDataFeatures<PlayerProgressInformations, Boolean>, ObservableValue<Boolean>>() {
public ObservableValue<Boolean> call(CellDataFeatures<PlayerProgressInformations, Boolean> p) {
return new SimpleBooleanProperty(p.getValue().isPlaying());
}
});
colPlaying.setCellFactory(new Callback<TableColumn<PlayerProgressInformations, Boolean>, TableCell<PlayerProgressInformations, Boolean>>() {
#Override
public TableCell<PlayerProgressInformations, Boolean> call( TableColumn<PlayerProgressInformations, Boolean> param) {
RadioButtonCell<PlayerProgressInformations, Boolean> radioButtonCell =
new RadioButtonCell<PlayerProgressInformations, Boolean>();
return radioButtonCell;
}
});
And the RadioButtoCell class :
private class RadioButtonCell<S, T> extends TableCell<S,T> {
public RadioButtonCell () {
}
#Override
protected void updateItem (T item, boolean empty) {
System.out.println("Count value : "+count); //Indicator to check how many times is used the method "updateItem"
count+=1;
if (item instanceof Boolean) {
Boolean myBoolean = (Boolean) item;
if (!empty) {
System.out.println("Valeur du boolean : "+item);
RadioButton radioButton = new RadioButton();
radioButton.setDisable(true);
radioButton.setSelected(myBoolean);
radioButton.setStyle("-fx-opacity: 1");
setGraphic(radioButton);
}
}
}
}
The stranges behaviors are the ones bellow :
Problem 1 : When I join a first player to the game's table, the updateItem methode is called 17 times.
If a second one is joining, this number for the first player increase to 57, or 60 and 17 for the second player.
Finally, if a third one is joining, it is 90 times for the first player, 57 or 60 for the second one and 17 for the third one.
Why does this method is so often called ? And why those specific numbers ?
More over, after this "initialization" the method is called 2 times more after each turn as I expected : one time to unselect a RadioButton and one time to select the next one.
Problem 2 : When a first player join the table, he is of course the first to play and, on his screen, the RadioButton is selected.
When a second player join the table, this second player see a RadioButton selected for the first player and a RadioButton unselected for himself. That's the behavior expected. But for the first player, the 2 RadioButtons are unselected.
And if a 3rd player join the table, he'll see the RadioButton selected for the first player and unselected for himself and the 2nd player. This is also the result expected. But, for the second and the first players, all of the 3 RadioButtons are unselected.
Why this strange behavior ? More over, after a first turn, all the RadioButtons appears selected or unselected as expected as if the bug disappeared.
Could you help me to understand what is happening and how to solve those bugs ?
Thank you very much
Sample Code
Here is a table implementation that works. I guess you could compare it to your implementation to see what the differences are. Probably the main "fix" is the updateItem handling of empty and null values.
import javafx.application.Application;
import javafx.beans.property.*;
import javafx.collections.*;
import javafx.scene.Scene;
import javafx.scene.control.*;
import javafx.scene.control.cell.PropertyValueFactory;
import javafx.stage.Stage;
public class PlayerViewer extends Application {
private final ObservableList<Player> data =
FXCollections.observableArrayList(
new Player("Jacob", true),
new Player("Isabella", false),
new Player("Ethan", true)
);
public static void main(String[] args) {
launch(args);
}
#Override
public void start(Stage stage) {
TableView<Player> table = new TableView<>(data);
table.setPrefHeight(130);
table.setPrefWidth(150);
TableColumn<Player, String> handleCol = new TableColumn<>("Handle");
handleCol.setCellValueFactory(new PropertyValueFactory<>("handle"));
table.getColumns().add(handleCol);
TableColumn<Player, Boolean> playingCol = new TableColumn<>("Playing");
playingCol.setCellValueFactory(new PropertyValueFactory<>("playing"));
playingCol.setCellFactory(param -> new TableCell<>() {
RadioButton indicator = new RadioButton();
{
indicator.setDisable(true);
indicator.setStyle("-fx-opacity: 1");
setContentDisplay(ContentDisplay.GRAPHIC_ONLY);
}
#Override
protected void updateItem(Boolean isPlaying, boolean empty) {
super.updateItem(isPlaying, empty);
if (empty || isPlaying == null) {
setGraphic(null);
} else {
indicator.setSelected(isPlaying);
setGraphic(indicator);
}
}
});
table.getColumns().add(playingCol);
stage.setScene(new Scene(table));
stage.show();
}
public static class Player {
private final SimpleStringProperty handle;
private final SimpleBooleanProperty playing;
private Player(String handle, boolean playing) {
this.handle = new SimpleStringProperty(handle);
this.playing = new SimpleBooleanProperty(playing);
}
public SimpleStringProperty handleProperty() {
return handle;
}
public String getHandle() {
return handle.get();
}
public void setHandle(String handle) {
this.handle.set(handle);
}
public SimpleBooleanProperty playingProperty() {
return playing;
}
public boolean isPlaying() {
return playing.get();
}
public void setPlaying(boolean playing) {
this.playing.set(playing);
}
}
}
Additional comments on your question
In terms "Problem 1", of how many times updateItem is called, that's an internal toolkit thing, your code shouldn't really care about that, it just needs to make sure that whenever it is called, that it does the right thing.
Regarding your "Problem 2", regarding the interaction of multiple views for multiple players, who knows? Impossible to say without further additional code, which would probably end up making this question too broad anyway. If you have a specific question about how to handle the interaction of displays for multiple players you will need to expand and clarify your question (likely as a new question with a mcve).
For your implementation, I would advise restyling the radio button (via CSS), so that it doesn't look like a standard user-selectable radio button (because you have disabled the selection capability and then removed the default disabled opacity setting). Or, you could use a custom indicator control such as the Bulb class from this answer, which might be preferred.
Here's another sample that works. (I was almost done with it when #jewelsea posted, so figured I would go ahead and post it anyway. It is similar but the differences between the two might be useful.)
The main issues in your code that I can see are:
Your cell value factory returns a new BooleanProperty every time it is invoked. This means the cell can't observe the correct property, and has no opportunity to update itself when the value changes. You should use JavaFX properties in your model class so that the cell can observe a single property.
Your cell implementation doesn't call the superclass implementation of updateItem(...). This means basic functionality won't happen, so updates may or may not occur when they are needed, and selection won't work, etc.
Your cell implementation doesn't deal with empty cells, so those will not necessarily display correctly.
In this implementation I used a Game class to keep the "current player" and gave each player a reference to the (same) game instance. The playing property in the Player is exposed as a read-only property and its value is bound to the game's current player. This means that only one player can have playing==true, without lots of "wiring" between the players.
The buttons allow for testing adding new players (who are automatically set as the "current" player) or moving the current player to the next player.
import java.util.Iterator;
import java.util.List;
import javafx.application.Application;
import javafx.beans.property.ObjectProperty;
import javafx.beans.property.ReadOnlyBooleanProperty;
import javafx.beans.property.ReadOnlyBooleanWrapper;
import javafx.beans.property.SimpleObjectProperty;
import javafx.beans.property.SimpleStringProperty;
import javafx.geometry.Insets;
import javafx.geometry.Pos;
import javafx.scene.Scene;
import javafx.scene.control.Button;
import javafx.scene.control.RadioButton;
import javafx.scene.control.TableCell;
import javafx.scene.control.TableColumn;
import javafx.scene.control.TableView;
import javafx.scene.layout.BorderPane;
import javafx.scene.layout.HBox;
import javafx.stage.Stage;
public class PlayerTable extends Application {
#Override
public void start(Stage primaryStage) {
TableView<Player> table = new TableView<>();
TableColumn<Player, String> playerCol = new TableColumn<>("Name");
playerCol.setCellValueFactory(cellData -> new SimpleStringProperty(cellData.getValue().getName()));
TableColumn<Player, Boolean> playingColumn = new TableColumn<>("Playing");
playingColumn.setCellValueFactory(cellData -> cellData.getValue().playingProperty());
playingColumn.setCellFactory(tc -> new RadioButtonTableCell<>());
table.getColumns().add(playerCol);
table.getColumns().add(playingColumn);
Game game = new Game();
Button newPlayerButton = new Button("New Player");
newPlayerButton.setOnAction(e -> addNewPlayer(game, table.getItems()));
Button nextPlayerButton = new Button("Next player");
nextPlayerButton.setOnAction(e -> selectNextPlayer(game, table.getItems()));
HBox controls = new HBox(5, newPlayerButton, nextPlayerButton);
controls.setAlignment(Pos.CENTER);
controls.setPadding(new Insets(5));
BorderPane root = new BorderPane();
root.setCenter(table);
root.setBottom(controls);
Scene scene = new Scene(root);
primaryStage.setScene(scene);
primaryStage.show();
}
private void addNewPlayer(Game game, List<Player> players) {
int playerNumber = players.size() + 1 ;
Player newPlayer = new Player(game, "Player "+playerNumber);
game.setCurrentPlayer(newPlayer);
players.add(newPlayer);
}
private void selectNextPlayer(Game game, List<Player> players) {
if (players.isEmpty()) return ;
for (Iterator<Player> i = players.iterator() ; i.hasNext() ;) {
if (i.next() == game.getCurrentPlayer()) {
if (i.hasNext()) {
game.setCurrentPlayer(i.next());
} else {
game.setCurrentPlayer(players.get(0));
}
return ;
}
}
game.setCurrentPlayer(players.get(0));
}
public static class RadioButtonTableCell<S> extends TableCell<S, Boolean> {
private RadioButton radioButton ;
public RadioButtonTableCell() {
radioButton = new RadioButton();
radioButton.setDisable(true);
}
#Override
protected void updateItem(Boolean item, boolean empty) {
super.updateItem(item, empty);
if (empty) {
setGraphic(null);
} else {
radioButton.setSelected(item);
setGraphic(radioButton);
}
}
};
public static class Game {
private final ObjectProperty<Player> currentPlayer = new SimpleObjectProperty<>() ;
public ObjectProperty<Player> currentPlayerProperty() {
return currentPlayer ;
}
public Player getCurrentPlayer() {
return currentPlayerProperty().get();
}
public void setCurrentPlayer(Player player) {
currentPlayerProperty().set(player);
}
}
public static class Player {
private final String name ;
private final ReadOnlyBooleanWrapper playing ;
public Player(Game game, String name) {
this.name = name ;
playing = new ReadOnlyBooleanWrapper() ;
playing.bind(game.currentPlayerProperty().isEqualTo(this));
}
public String getName() {
return name ;
}
public ReadOnlyBooleanProperty playingProperty() {
return playing.getReadOnlyProperty() ;
}
public boolean isPlaying() {
return playingProperty().get();
}
}
public static void main(String[] args) {
launch(args);
}
}
This is the first time I've had to work with JavaFX (and hopefully the last) so I don't exactly understand how everything works. I'll try to sum where I am briefly
I am trying to make my table highlight duplicate cells on a specific column
I need editable cells and no TableCell extensions I've come across work, I've been spending most of today trying to fix their bugs to no avail. I've given up on that approach.
I found TextFieldTableCell but that does not allow me to extend and override functions like updateItem. At this point I have no interest in re-implementing any of this functionality.
Currently what I do is the following:
CollectionName.setCellValueFactory(new PropertyValueFactory<>("CollectionName"));
CollectionName.setCellFactory(EditingCell.<Item>forTableColumn(this)); //At the moment this just passes though TextFieldTableCell, the parameter is totally inconsequential
CollectionName.setOnEditCommit((CellEditEvent<Item, String> t) ->
{
((Item) t.getTableView().getItems().get(
t.getTablePosition().getRow())
).setCollectionName(t.getNewValue());
System.out.println("Set on edit commit");
if(isDuplicateName(t.getNewValue()))
{
t.getTableView().getColumns().get(t.getTablePosition().getColumn()).getStyleClass().add("duplicate-cell");
System.out.println("Duplicate");
}
else
{
t.getTableView().getColumns().get(t.getTablePosition().getColumn()).getStyleClass().remove("duplicate-cell");
System.out.println("Not duplicate");
}
});
This functions as intended but highlights the entire column. I need it to highlight only the specific cell. I wish there was a way to simply call myTable.getCell(x,y).getStyleClass().add("duplicate-cell") or something. I mean it is a table after all...
The solution to any problem involving changing the appearance of table cells based on certain state of the cell's item, and other data, is always to use a cell factory which returns a cell that updates its appearance accordingly.
The problem with the approach you are trying is that you are overlooking the fact that the table view reuses cells. For example, if the table contains a large amount of data and the user scrolls, new cells will not be created but cells that are scrolled out of view will be reused for the new items that scroll into view. Since you don't update the style of the cell when this happens, scrolling will make the wrong cells highlighted.
Here the logic is a little tricky as each cell essentially has to observe all values in the column (whether they are currently displayed or not). I think the easiest solution here is to independently maintain an ObservableSet that keeps a list of duplicate entries, and have the cell observe that. Here's an implementation. You can probably factor this out into a separate class for the cell factory (or something convenient) to make it more elegant and reusable.
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.stream.Collectors;
import javafx.application.Application;
import javafx.beans.Observable;
import javafx.beans.binding.Bindings;
import javafx.beans.binding.BooleanBinding;
import javafx.beans.property.SimpleIntegerProperty;
import javafx.beans.property.SimpleStringProperty;
import javafx.beans.property.StringProperty;
import javafx.collections.FXCollections;
import javafx.collections.ListChangeListener.Change;
import javafx.collections.ObservableList;
import javafx.collections.ObservableSet;
import javafx.css.PseudoClass;
import javafx.scene.Scene;
import javafx.scene.control.TableColumn;
import javafx.scene.control.TableView;
import javafx.scene.control.cell.TextFieldTableCell;
import javafx.stage.Stage;
import javafx.util.StringConverter;
public class HighlightDuplicateTableCells extends Application {
// create an observable list that fires events if the dataProperty of any elements change:
private final ObservableList<Item> items =
FXCollections.observableArrayList(item -> new Observable[]{item.dataProperty()});
// collection of strings that are duplicated in the data properties of all the items:
private final ObservableSet<String> duplicateData = FXCollections.observableSet();
private static final PseudoClass DUPLICATE_PC = PseudoClass.getPseudoClass("duplicate");
private final StringConverter<String> identityStringConverter = new StringConverter<String>() {
#Override
public String toString(String object) {
return object;
}
#Override
public String fromString(String string) {
return string;
}
};
#Override
public void start(Stage primaryStage) {
// listener to maintain collection of duplicates:
items.addListener((Change<? extends Item> change) -> updateDuplicateData());
TableView<Item> table = new TableView<>();
table.setEditable(true);
table.setItems(items);
TableColumn<Item, Number> idColumn = new TableColumn<>("Id");
idColumn.setCellValueFactory(cellData -> new SimpleIntegerProperty(cellData.getValue().getId()));
TableColumn<Item, String> dataColumn = new TableColumn<>("Data");
dataColumn.setCellValueFactory(cellData -> cellData.getValue().dataProperty());
dataColumn.setCellFactory(tc -> {
TextFieldTableCell<Item, String> cell = new TextFieldTableCell<Item, String>(identityStringConverter) {
// boolean binding that indicates if the current item is contained in the duplicateData set:
private BooleanBinding duplicate = Bindings.createBooleanBinding(
() -> duplicateData.contains(getItem()),
duplicateData, itemProperty());
// anonymous constructor just updates CSS pseudoclass if above binding changes:
{
duplicate.addListener((obs, wasDuplicate, isNowDuplicate) ->
pseudoClassStateChanged(DUPLICATE_PC, isNowDuplicate));
}
};
return cell ;
});
table.getColumns().add(idColumn);
table.getColumns().add(dataColumn);
// note best to minimize changes to items.
// creating a temp list and using items.setAll(...) achieves this:
List<Item> tmp = new ArrayList<>();
for (int i = 1 ; i <= 70; i++) {
char c = (char)('#' + (i % 60));
String data = Character.toString(c) ;
tmp.add(new Item(i, data));
}
items.setAll(tmp);
Scene scene = new Scene(table, 600, 600);
scene.getStylesheets().add("duplicate-cell-example.css");
primaryStage.setScene(scene);
primaryStage.show();
}
private void updateDuplicateData() {
// TODO: may not be most efficient implementation
// all data:
List<String> data = items.stream().map(Item::getData).collect(Collectors.toList());
// unique data:
Set<String> uniqueData = new HashSet<>(data);
// remove unique values from data:
uniqueData.forEach(data::remove);
// remaining values are duplicates: replace contents of duplicateData with these:
duplicateData.clear();
duplicateData.addAll(data);
}
public static class Item {
private final int id ;
private final StringProperty data = new SimpleStringProperty();
public Item(int id, String data) {
this.id = id ;
setData(data);
}
public final StringProperty dataProperty() {
return this.data;
}
public final String getData() {
return this.dataProperty().get();
}
public final void setData(final String data) {
this.dataProperty().set(data);
}
public int getId() {
return id ;
}
}
public static void main(String[] args) {
launch(args);
}
}
and the duplicate-cell-example.css:
.table-cell:duplicate {
-fx-background-color: -fx-background ;
-fx-background: red ;
}
This is basically James_D's approach, but it improves the time required for updates from Ω(n²) worst case (n = list size) to O(m) where m is the number of changes (1 for updates of a property; the number of elements added/removed on a list update).
This performance is achieved by storing the number of occurances in a ObservableMap<String, Integer>:
private final ObservableMap<String, Integer> valueOccuranceCounts = FXCollections.observableHashMap();
private final ChangeListener<String> changeListener = (observable, oldValue, newValue) -> {
valueOccuranceCounts.computeIfPresent(oldValue, REMOVE_UPDATER);
valueOccuranceCounts.merge(newValue, 1, ADD_MERGER);
};
private static final BiFunction<Integer, Integer, Integer> ADD_MERGER = (oldValue, newValue) -> oldValue + 1;
private static final BiFunction<String, Integer, Integer> REMOVE_UPDATER = (key, value) -> {
int newCount = value - 1;
// remove mapping, if the value would become 0
return newCount == 0 ? null : newCount;
};
private final ListChangeListener<Item> listChangeListener = (ListChangeListener.Change<? extends Item> c) -> {
while (c.next()) {
if (c.wasRemoved()) {
for (Item r : c.getRemoved()) {
// decrease count and remove listener
this.valueOccuranceCounts.computeIfPresent(r.getData(), REMOVE_UPDATER);
r.dataProperty().removeListener(this.changeListener);
}
}
if (c.wasAdded()) {
for (Item a : c.getAddedSubList()) {
// increase count and add listener
this.valueOccuranceCounts.merge(a.getData(), 1, ADD_MERGER);
a.dataProperty().addListener(this.changeListener);
}
}
}
};
private final ObservableList<Item> items;
{
items = FXCollections.observableArrayList();
items.addListener(listChangeListener);
}
private static final PseudoClass DUPLICATE = PseudoClass.getPseudoClass("duplicate");
private static final String FIRST_COLUMN_CLASS = "first-column";
#Override
public void start(Stage primaryStage) throws Exception {
TableView<Item> tableView = new TableView<>(items);
// tableView.getSelectionModel().setCellSelectionEnabled(true);
tableView.setEditable(true);
TableColumn<Item, String> column = new TableColumn<>("data");
column.setCellValueFactory(cellData -> cellData.getValue().dataProperty());
column.setCellFactory(col -> new TextFieldTableCell<Item, String>() {
// boolean binding that indicates if the current item is contained in the duplicateData set:
private final BooleanBinding duplicate = Bindings.createBooleanBinding(
() -> valueOccuranceCounts.getOrDefault(getItem(), 1) >= 2,
valueOccuranceCounts, itemProperty());
// anonymous constructor just updates CSS pseudoclass if above binding changes:
{
duplicate.addListener((observable, oldValue, newValue)
-> pseudoClassStateChanged(DUPLICATE, newValue));
}
});
TableColumn<Item, Number> idColumn = new TableColumn<>("id");
idColumn.setCellValueFactory(cellData -> new SimpleIntegerProperty(cellData.getValue().getId()));
tableView.getColumns().addAll(idColumn, column);
tableView.getColumns().addListener((Observable observable) -> {
// keep style class marking the cells of the column as
// belonging to the first column up to date
if (tableView.getColumns().get(0) == column) {
if (!column.getStyleClass().contains(FIRST_COLUMN_CLASS)) {
column.getStyleClass().add(FIRST_COLUMN_CLASS);
}
} else {
column.getStyleClass().remove(FIRST_COLUMN_CLASS);
}
});
// note best to minimize changes to items.
// creating a temp list and using items.setAll(...) achieves this:
final int count = 70;
List<Item> tmp = Arrays.asList(new Item[count]);
for (int i = 0; i < count; i++) {
tmp.set(i, new Item(Integer.toString(i % 60)));
}
items.setAll(tmp);
Scene scene = new Scene(tableView);
scene.getStylesheets().add(getClass().getResource("style.css").toExternalForm());
primaryStage.setScene(scene);
primaryStage.show();
}
public static class Item {
private static int counter = 0;
private final StringProperty data;
private final int id = counter++;
public Item(String data) {
this.data = new SimpleStringProperty(data);
}
public final StringProperty dataProperty() {
return this.data;
}
public final String getData() {
return this.dataProperty().get();
}
public final void setData(final String data) {
this.dataProperty().set(data);
}
public int getId() {
return id ;
}
}
style.css
.table-row-cell:filled .table-cell:duplicate {
-fx-background: yellow;
-fx-background-color: -fx-table-cell-border-color, -fx-background;
}
.table-view:focused .table-row-cell:filled .table-cell:duplicate:focused {
-fx-background-color: -fx-background, -fx-cell-focus-inner-border, -fx-background;
}
/* keep use the same background colors normally used for focused table rows */
.table-view:focused .table-row-cell:filled:focused .table-cell:duplicate {
-fx-background-color: -fx-background, -fx-cell-focus-inner-border, -fx-background;
/* frame only at top & bottom sides */
-fx-background-insets: 0, 1 0 1 0, 2 0 2 0;
}
.table-view:focused .table-row-cell:filled:focused .table-cell.first-column:duplicate {
/* frame only for top, left and bottom sides*/
-fx-background-insets: 0, 1 0 1 1, 2 0 2 2;
}
.table-row-cell:filled .table-cell:duplicate:selected,
.table-row-cell:filled:selected .table-cell:duplicate {
-fx-background: turquoise;
}
Note that some parts (creating & filling the table, creating the column) are copied from #James_D's answer, since it's simply best practice to do it this way.
How do i plot the straight line of best fit and the product-moment correlation coefficient on a Scatter diagram using JavaFX library? I've tried to Google a few examples but none were precise or even similar to what i'm trying to do. I'm new to JavaFX so any help is appreciated. There were a few examples on the internet but all were for completely different libraries which is of no help to me.
I have the following code which displays a Scatter diagram (just an example):
import javafx.application.Application;
import javafx.scene.Scene;
import javafx.scene.chart.NumberAxis;
import javafx.scene.chart.ScatterChart;
import javafx.scene.chart.XYChart;
import javafx.stage.Stage;
public class Scatter extends Application {
#Override
public void start(Stage stage) {
final NumberAxis xAxis = new NumberAxis(0, 100, 20);
final NumberAxis yAxis = new NumberAxis(0, 100, 20);
final ScatterChart<Number,Number> sc = new
ScatterChart<Number,Number>(xAxis,yAxis);
xAxis.setLabel("Average across all exams");
yAxis.setLabel("Spring Term test marks");
sc.setTitle("Students marks");
XYChart.Series plots = new XYChart.Series();
plots.getData().add(new XYChart.Data(10,15));
plots.getData().add(new XYChart.Data(15,20));
plots.getData().add(new XYChart.Data(77,77));
plots.getData().add(new XYChart.Data(55,13));
plots.getData().add(new XYChart.Data(44,22));
plots.getData().add(new XYChart.Data(45,43));
sc.getData().add(plots);
Scene scene = new Scene(sc, 600, 600);
stage.setScene(scene);
stage.show();
}
public static void main(String[] args) {
launch(args);
}
}
The actual formulae computation of the line of best fit and the correlation coefficient can be easily found elsewhere (and sound a bit like a homework problem), so I will omit those; it sounds like you just want to know how to add nodes (e.g. the actual line) to the chart.
The basic idea is to subclass ScatterChart and override the layoutPlotChildren method. You can use CSS to color each best fit line the same color as the data in the corresponding series by referencing the looked-up-colors CHART_COLOR_N for N=1...8.
Here is an example (I just use dummy values for the formula for the line, you can replace with the real calculations):
import java.util.ArrayList;
import java.util.List;
import javafx.scene.chart.NumberAxis;
import javafx.scene.chart.ScatterChart;
import javafx.scene.shape.Line;
public class ScatterPlotWithBestFitLine extends ScatterChart<Number, Number> {
private final NumberAxis xAxis ;
private final NumberAxis yAxis ;
private final List<Line> lines = new ArrayList<>();
public ScatterPlotWithBestFitLine(NumberAxis xAxis, NumberAxis yAxis) {
super(xAxis, yAxis);
this.xAxis = xAxis ;
this.yAxis = yAxis ;
getStylesheets().add("best-fit-line.css");
}
#Override
protected void layoutPlotChildren() {
getPlotChildren().removeAll(lines);
lines.clear();
super.layoutPlotChildren();
int index = 0 ;
for (Series<Number, Number> series : getData()) {
Line line = new Line();
line.setStartX(xAxis.getDisplayPosition(xAxis.getLowerBound()));
line.setEndX(xAxis.getDisplayPosition(xAxis.getUpperBound()));
int count = (index % 8) + 1 ;
line.getStyleClass().add("best-fit-line");
line.getStyleClass().add("best-fit-line-"+count);
// TODO compute actual line of best fit...
// can iterate through values with:
// for (Data<Number, Number> d : series.getData()) {
// double x = d.getXValue().doubleValue();
// double y = d.getYValue().doubleValue();
// }
// just dummy values:
double m = 0 ;
double b = (getData().size() - index) * yAxis.getLowerBound() + (index + 1) * yAxis.getUpperBound() / 2 ;
line.setStartY(yAxis.getDisplayPosition(m * xAxis.getLowerBound() + b));
line.setEndY(yAxis.getDisplayPosition(m * xAxis.getUpperBound() + b));
getPlotChildren().add(line);
lines.add(line);
index++ ;
}
}
}
with best-fit-line.css:
.best-fit-line {
-fx-stroke-width: 2 ;
}
.best-fit-line-1 {
-fx-stroke: CHART_COLOR_1 ;
}
.best-fit-line-2 {
-fx-stroke: CHART_COLOR_2 ;
}
.best-fit-line-3 {
-fx-stroke: CHART_COLOR_3 ;
}
.best-fit-line-4 {
-fx-stroke: CHART_COLOR_4 ;
}
.best-fit-line-5 {
-fx-stroke: CHART_COLOR_5 ;
}
.best-fit-line-6 {
-fx-stroke: CHART_COLOR_6 ;
}
.best-fit-line-7 {
-fx-stroke: CHART_COLOR_7 ;
}
.best-fit-line-8 {
-fx-stroke: CHART_COLOR_8 ;
}
and a demo:
import javafx.application.Application;
import javafx.scene.Scene;
import javafx.scene.chart.NumberAxis;
import javafx.scene.chart.XYChart.Data;
import javafx.scene.chart.XYChart.Series;
import javafx.stage.Stage;
public class ScatterPlotTest extends Application {
#Override
public void start(Stage primaryStage) {
ScatterPlotWithBestFitLine plot = new ScatterPlotWithBestFitLine(new NumberAxis(), new NumberAxis());
plot.getData().add(createSeries("Data", new double[] {
{10,15},
{15,20},
{77,77},
{55,13},
{44,22},
{45,43}
}));
Scene scene = new Scene(plot, 600, 600);
primaryStage.setScene(scene);
primaryStage.show();
}
private Series<Number, Number> createSeries(String name, double[][] values) {
Series<Number, Number> series = new Series<>();
series.setName("Data");
for (double[] point : values) {
series.getData().add(new Data<>(point[0],point[1]));
}
return series ;
}
public static void main(String[] args) {
launch(args);
}
}
You didn't really specify what you wanted to do in terms of displaying the correlation coefficient. You could create a label (or multiple labels in the case of multiple series in your plot) and add them to the chart (somewhere) in the same manner. Alternatively, you could include the correlation coefficient in the name of the series, so it appears in the legend. Using a binding between the nameProperty() and the data would make sure this stayed up to date if the data changes:
private Series<Number, Number> createSeries(String name, double[][] values) {
Series<Number, Number> series = new Series<>();
ObservableList<Data<Number, Number>> data = FXCollections.observableArrayList(
d -> new Observable[] {d.XValueProperty(), d.YValueProperty()});
for (double[] point : values) {
series.getData().add(new Data<>(point[0],point[1]));
}
series.nameProperty().bind(Bindings.createStringBinding(() ->
String.format("%s (r=%.3f)", name, computeCorrelation(data)),
data);
return series ;
}
private double computeCorrelation(List<Data<Number, Number>> data) {
//TODO compute correlation from data...
return 0 ;
}
thanks for reading my question.
I'm currently working with JavaFX-8, SceneBuilder and Eclipse.
I want to do a scatter chart with four quadrants, that has two fixed number axis (the data position is not relevant, I only need to display the dots on each quadrant... only matters in which quadrant a dot is). Each quadrant must have a background with a specific color.
I found this question, so I tried to extend ScatterChart with the aim of overriding the method layoutPlotChildren(). I tried a minimum implementation to see if it will run with my FXML (I did import the new component to the FXML). This was my minimum implementation:
public class ScatterQuadrantChart<X,Y> extends ScatterChart<X,Y> {
public ScatterQuadrantChart(Axis<X> xAxis, Axis<Y> yAxis) {
super(xAxis, yAxis);
} }
And then, I get the NotSuchMethodError init error. I found a similar error but from someone extending LineChart here, but I'm not quite sure of what I need to do on my own class.
I tried adding a no-parameters constructor, but I need to call super and cant because I can't call the "getXAxis()" method either. What should I do here?
Plus, the other issue that remains is, once I solve this, what should the layoutPlotChildren() method do?
Thanks for reading.
The problem you are seeing is arising because the default mechanism for the FXMLLoader to instantiate a class is to call the no-argument constructor. Your ScatterQuadrantChart has no no-argument constructor, hence the NoSuchMethodError.
Prior to Java 8, the only way to fix this was to create a builder class for your class, as in the post you linked. JavaFX 8 introduced (but failed to document) a mechanism to specify values for constructor parameters that would be recognized by the FXMLLoader, using the #NamedArg annotation).
So, in Java 8, you can modify your ScatterQuadrantChart:
public class ScatterQuadrantChart<X,Y> extends ScatterChart<X,Y> {
public ScatterQuadrantChart(#NamedArg("xAxis")Axis<X> xAxis,
#NamedArg("yAxis)Axis<Y> yAxis) {
super(xAxis, yAxis);
}
}
and then your FXML will look like
<ScatterQuadrantChart>
<xAxis>
<NumberAxis ... />
</xAxis>
<yAxis>
<NumberAxis ... />
</yAxis>
</ScatterQuadrantChart>
I have no idea if or how SceneBuilder will interact with this, but the FXML will work.
As for the implementation, you will need to add some nodes to the plot to represent your quadrants. I would probably just use plain regions for these. Create them in the constructor and call getPlotChildren().add(...) to add them. Then in the layoutPlotChildren() method, first call the superclass method (which will lay out the scatter chart nodes), and then resize and reposition the quadrants. You can use getXAxis().getDisplayPosition(...) to figure out the location from the actual divider value.
In real life, you should add style classes to the quadrants so you can style them externally with css, etc, but a very basic implementation might look like
import javafx.beans.NamedArg;
import javafx.beans.property.Property;
import javafx.beans.property.SimpleObjectProperty;
import javafx.beans.value.ChangeListener;
import javafx.scene.chart.Axis;
import javafx.scene.chart.ScatterChart;
import javafx.scene.layout.Region;
public class ScatterQuadrantChart<X,Y> extends ScatterChart<X,Y> {
private final Property<X> xQuadrantDivider = new SimpleObjectProperty<>();
private final Property<Y> yQuadrantDivider = new SimpleObjectProperty<>();
private final Region nwQuad ;
private final Region neQuad ;
private final Region swQuad ;
private final Region seQuad ;
public ScatterQuadrantChart(#NamedArg("xAxis") Axis<X> xAxis,
#NamedArg("yAxis") Axis<Y> yAxis) {
super(xAxis, yAxis);
nwQuad = new Region();
neQuad = new Region();
swQuad = new Region();
seQuad = new Region();
nwQuad.setStyle("-fx-background-color: lightsalmon ;");
neQuad.setStyle("-fx-background-color: antiquewhite ;");
swQuad.setStyle("-fx-background-color: aqua ;");
seQuad.setStyle("-fx-background-color: lightskyblue ;");
getPlotChildren().addAll(nwQuad, neQuad, swQuad, seQuad);
ChangeListener<Object> quadListener = (obs, oldValue, newValue) -> layoutPlotChildren();
xQuadrantDivider.addListener(quadListener);
yQuadrantDivider.addListener(quadListener);
}
#Override
public void layoutPlotChildren() {
super.layoutPlotChildren();
X x = xQuadrantDivider.getValue();
Y y = yQuadrantDivider.getValue();
if (x != null && y != null) {
Axis<X> xAxis = getXAxis();
Axis<Y> yAxis = getYAxis();
double xPixels = xAxis.getDisplayPosition(x);
double yPixels = yAxis.getDisplayPosition(y);
double totalWidth = xAxis.getWidth();
double totalHeight = yAxis.getHeight();
nwQuad.resizeRelocate(0, 0, xPixels, yPixels);
swQuad.resizeRelocate(0, yPixels, xPixels, totalHeight - yPixels);
neQuad.resizeRelocate(xPixels, 0, totalWidth - xPixels, yPixels);
seQuad.resizeRelocate(xPixels, yPixels, totalWidth - xPixels, totalHeight - yPixels);
}
}
public final Property<X> xQuadrantDividerProperty() {
return this.xQuadrantDivider;
}
public final X getXQuadrantDivider() {
return this.xQuadrantDividerProperty().getValue();
}
public final void setXQuadrantDivider(final X xQuadrantDivider) {
this.xQuadrantDividerProperty().setValue(xQuadrantDivider);
}
public final Property<Y> yQuadrantDividerProperty() {
return this.yQuadrantDivider;
}
public final Y getYQuadrantDivider() {
return this.yQuadrantDividerProperty().getValue();
}
public final void setYQuadrantDivider(final Y yQuadrantDivider) {
this.yQuadrantDividerProperty().setValue(yQuadrantDivider);
}
}
Test code:
import java.util.Random;
import java.util.stream.Stream;
import javafx.application.Application;
import javafx.scene.Scene;
import javafx.scene.chart.NumberAxis;
import javafx.scene.chart.XYChart.Data;
import javafx.scene.chart.XYChart.Series;
import javafx.scene.layout.BorderPane;
import javafx.stage.Stage;
public class ScatterQuadrantChartTest extends Application {
#Override
public void start(Stage primaryStage) {
final Random rng = new Random();
ScatterQuadrantChart<Number, Number> chart = new ScatterQuadrantChart<>(new NumberAxis(), new NumberAxis());
Series<Number, Number> series = new Series<>();
for (int i=0; i<20; i++) {
series.getData().add(new Data<>(rng.nextDouble() * 100, rng.nextDouble() * 100));
}
chart.getData().add(series);
chart.setXQuadrantDivider(50);
chart.setYQuadrantDivider(50);
BorderPane root = new BorderPane(chart);
Scene scene = new Scene(root, 600, 600);
primaryStage.setScene(scene);
primaryStage.show();
}
public static void main(String[] args) {
launch(args);
}
}
I have written the below JavaFX program in which two rectangle nodes are in translate transition:
public class Test extends Application{
public static void main(String[] args) {
launch(args);
}
#Override
public void start(Stage primaryStage) throws Exception {
BorderPane borderPane = new BorderPane();
borderPane.setStyle("-fx-background-color: green;");
Rectangle rect1 = new Rectangle(20,20,50, 50);
rect1.setArcHeight(15);
rect1.setArcWidth(15);
rect1.setFill(Color.RED);
Rectangle rect2 = new Rectangle(20,20,30, 30);
rect2.setArcHeight(15);
rect2.setArcWidth(15);
rect2.setFill(Color.RED);
TranslateTransition translateTransition1 = new TranslateTransition(Duration.millis(2000), rect1);
translateTransition1.setFromX(0);
translateTransition1.setToX(300);
translateTransition1.setToY(300);
translateTransition1.setCycleCount(Timeline.INDEFINITE);
translateTransition1.setAutoReverse(true);
translateTransition1.play();
TranslateTransition translateTransition2 = new TranslateTransition(Duration.millis(2000), rect2);
translateTransition2.setFromX(300);
translateTransition2.setToX(0);
translateTransition2.setToY(300);
translateTransition2.setCycleCount(Timeline.INDEFINITE);
translateTransition2.setAutoReverse(true);
translateTransition2.play();
borderPane.getChildren().add(rect1);
borderPane.getChildren().add(rect2);
primaryStage.setScene(new Scene(borderPane, 500, 500));
primaryStage.show();
}
}
How can I implement collision detection of the two rectangle nodes which are in Translate Transition?
With rectangles it's pretty easy; just get their bounds in the parent and see if they intersect. The only drawback with this is it doesn't take into account the curved corners: you may need to compute that by hand if you want that level of accuracy. For non-rectangular shapes you can also just observe the bounds in parent properties, but you'd need to do the computation by hand to see if the shapes intersect.
ObservableBooleanValue colliding = Bindings.createBooleanBinding(new Callable<Boolean>() {
#Override
public Boolean call() throws Exception {
return rect1.getBoundsInParent().intersects(rect2.getBoundsInParent());
}
}, rect1.boundsInParentProperty(), rect2.boundsInParentProperty());
colliding.addListener(new ChangeListener<Boolean>() {
#Override
public void changed(ObservableValue<? extends Boolean> obs,
Boolean oldValue, Boolean newValue) {
if (newValue) {
System.out.println("Colliding");
} else {
System.out.println("Not colliding");
}
}
});
TranslateTransition isn't meant to support Collision Detection. It simply moves A to B without any regards to the state of anything but its node.
You would need a Transition mechanism that is aware of the other objects on the board.
The good news is that creating a Transition isn't too hard. You can create a class that inherits Transition and simply implement the interpolate() method.
From the JavaDoc:
Below is a simple example. It creates a small animation that updates
the text property of a Text node. It starts with an empty String and
adds gradually letter by letter until the full String was set when the
animation finishes.
final String content = "Lorem ipsum";
final Text text = new Text(10, 20, "");
final Animation animation = new Transition() {
{
setCycleDuration(Duration.millis(2000));
}
protected void interpolate(double frac) {
final int length = content.length();
final int n = Math.round(length * (float) frac);
text.setText(content.substring(0, n));
}
};
The bad news is that having a successful collision detection mechanism is a bit harder. I'm really no expert on the subject, but I would probably have a ObservableList of Nodes that have collision, pass it to the Transition and on the interpolate method I would do a intersection check of the node that's moving against all the other nodes and leave it still if he cannot move.
If you want anything better than that, you'll probably want to look into a 2D Game Framework like Slick2D.
EDIT: Made a few simple alterations and went with a State based approach, code has been updated.
Well my approach is different that all the above ...
NOTE: I'm using 1.8 source
I created a Collidable interface:
import javafx.beans.property.BooleanProperty;
import javafx.beans.property.SimpleBooleanProperty;
import javafx.scene.shape.Shape;
public interface Collidable{
public enum CollisionState{
WAITING,
TOUCHING;
}
ObjectProperty<CollisionState> state = new SimpleObjectProperty<>(CollisionState.WAITING);
public default ReadOnlyObjectProperty<CollisionState> collisionStateProperty(){return state;}
public default CollisionState getCollisionState(){return state.get();}
BooleanProperty collided = new SimpleBooleanProperty(false){{
addListener((ObservableValue<? extends Boolean> observable1, Boolean oldValue, Boolean touching) -> {
if(touching){
state.set(CollisionState.TOUCHING);
}else{
state.set(CollisionState.WAITING);
}
});
}};
public default boolean hasCollided(){return collided.get();}
public default BooleanProperty collidedProperty(){return collided;}
public default void checkCollision(Shape src, Shape other){
if(Shape.intersect(src, other).getBoundsInLocal().getWidth() > -1 && !getCollisionState().equals(CollisionState.TOUCHING)){
collided.set(true);
handleCollision(other);
}else if(Shape.intersect(src, other).getBoundsInLocal().getWidth() <= 0){
collided.set(false);
}
}
public void handleCollision(Shape other);
}
And a simple implementation:
import javafx.animation.Animation;
import javafx.animation.ParallelTransition;
import javafx.animation.TranslateTransition;
import javafx.application.Application;
import javafx.beans.Observable;
import javafx.scene.Group;
import javafx.scene.Scene;
import javafx.scene.paint.Color;
import javafx.scene.paint.Paint;
import javafx.scene.shape.Circle;
import javafx.scene.shape.Rectangle;
import javafx.scene.shape.Shape;
import javafx.stage.Stage;
import javafx.util.Duration;
/**
*
* #author Dub-Laptop
*/
public class CollisionTesting extends Application {
private TranslateTransition cAnim;
#Override
public void start(Stage primaryStage) {
Group root = new Group();
Scene scene = new Scene(root);
primaryStage.setTitle("Collision Testing");
primaryStage.setScene(scene);
primaryStage.show();
Rectangle r = new Rectangle(100,50, Color.AQUA);
r.setLayoutX(10);
r.setLayoutY(200);
CollidableCircle c = new CollidableCircle(50, Color.GREEN);
c.setLayoutX(800);
c.setLayoutY(200);
/* can change this to anything you like
I used translateXProperty for simplicity
*/
c.translateXProperty().addListener((Observable observable) -> {
c.checkCollision(c, r);
});
root.getChildren().addAll(r, c);
TranslateTransition rAnim = new TranslateTransition();
rAnim.setToX(600);
rAnim.setAutoReverse(true);
rAnim.setCycleCount(Animation.INDEFINITE);
rAnim.setDuration(Duration.seconds(5));
rAnim.setNode(r);
cAnim = new TranslateTransition();
cAnim.setToX(-590);
cAnim.setAutoReverse(true);
cAnim.setCycleCount(Animation.INDEFINITE);
cAnim.setDuration(Duration.seconds(5));
cAnim.setNode(c);
rAnim.play();
cAnim.play();
}
/**
* #param args the command line arguments
*/
public static void main(String[] args) {
launch(args);
}
private class CollidableCircle extends Circle implements Collidable{
public CollidableCircle(double radius, Paint fill) {
super(radius, fill);
new AnimationTimer(){
#Override
public void handle(long now) {
root.getChildren().filtered((Node n)->{
return !n.equals(CollidableCircle.this) && n instanceof Shape;
}).forEach(other ->{
checkCollision(CollidableCircle.this, (Shape)other);
});
}
}.start();
// I added this for local property changes to this node
collisionStateProperty().addListener((ObservableValue<? extends CollisionState> observable, CollisionState oldValue, CollisionState newValue) -> {
if(newValue.equals(CollisionState.TOUCHING)){
setScaleX(1.25);
setScaleY(1.25);
setFill(Color.GREENYELLOW);
cAnim.pause();
}else if(newValue.equals(CollisionState.WAITING)){
setScaleX(1.0);
setScaleY(1.0);
setFill(Color.GREEN);
cAnim.play();
}
});
}
#Override
public void handleCollision(Shape other) {
// handle updates that affect other node here
System.out.println("Collided with : " + other.getClass().getSimpleName());
}
}
}
IMHO rather than using Bounds for checking Shape collisions, use the boolean :
(Shape.intersect(s1,s2).getBoundsInLocal().getWidth() > -1)
This approach is more accurate for Shapes as it will check for non-null pixels within the Shape Bounds, rather than the normal rectangular Bounds.
Though if you really want to use Bounds, this should work also:
if(sourceShape.getBoundsInLocal().intersects(otherShape.getBoundsInParent()){
Shape intersect = Shape.intersect(sourceShape, otherShape);
if(intersect.getBoundsInLocal().getWidth > -1){
// handle code here
}
}
though, as you can see it's more verbose and virtually the same as my other method.
Hope this helps.