/*
* This file is part of Bitsquare.
*
* Bitsquare is free software: you can redistribute it and/or modify it
* under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or (at
* your option) any later version.
*
* Bitsquare is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public
* License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with Bitsquare. If not, see <http://www.gnu.org/licenses/>.
*/
package io.bitsquare.gui.main.market.offerbook;
import io.bitsquare.common.UserThread;
import io.bitsquare.common.util.Tuple4;
import io.bitsquare.gui.Navigation;
import io.bitsquare.gui.common.view.ActivatableViewAndModel;
import io.bitsquare.gui.common.view.FxmlView;
import io.bitsquare.gui.main.MainView;
import io.bitsquare.gui.main.offer.BuyOfferView;
import io.bitsquare.gui.main.offer.SellOfferView;
import io.bitsquare.gui.main.offer.offerbook.OfferBookListItem;
import io.bitsquare.gui.util.BSFormatter;
import io.bitsquare.gui.util.CurrencyListItem;
import io.bitsquare.gui.util.GUIUtil;
import io.bitsquare.locale.CurrencyUtil;
import io.bitsquare.trade.offer.Offer;
import javafx.beans.property.ReadOnlyObjectWrapper;
import javafx.beans.property.SimpleStringProperty;
import javafx.beans.property.StringProperty;
import javafx.beans.value.ChangeListener;
import javafx.beans.value.ObservableValue;
import javafx.collections.FXCollections;
import javafx.collections.ListChangeListener;
import javafx.collections.ObservableList;
import javafx.geometry.Insets;
import javafx.geometry.Pos;
import javafx.scene.chart.AreaChart;
import javafx.scene.chart.NumberAxis;
import javafx.scene.chart.XYChart;
import javafx.scene.control.*;
import javafx.scene.image.ImageView;
import javafx.scene.layout.HBox;
import javafx.scene.layout.Priority;
import javafx.scene.layout.VBox;
import javafx.util.Callback;
import javafx.util.StringConverter;
import org.fxmisc.easybind.EasyBind;
import org.fxmisc.easybind.Subscription;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import javax.inject.Inject;
import java.util.Collections;
@FxmlView
public class OfferBookChartView extends ActivatableViewAndModel<VBox, OfferBookChartViewModel> {
private static final Logger log = LoggerFactory.getLogger(OfferBookChartView.class);
private NumberAxis xAxis, yAxis;
private XYChart.Series seriesBuy, seriesSell;
private final Navigation navigation;
private final BSFormatter formatter;
private TableView<OfferListItem> buyOfferTableView;
private TableView<OfferListItem> sellOfferTableView;
private AreaChart<Number, Number> areaChart;
private ComboBox<CurrencyListItem> currencyComboBox;
private Subscription tradeCurrencySubscriber;
private final StringProperty volumeColumnLabel = new SimpleStringProperty();
private final StringProperty priceColumnLabel = new SimpleStringProperty();
private Button buyOfferButton;
private Button sellOfferButton;
private ChangeListener<Number> selectedTabIndexListener;
private SingleSelectionModel<Tab> tabPaneSelectionModel;
private Label buyOfferHeaderLabel, sellOfferHeaderLabel;
private ChangeListener<OfferListItem> sellTableRowSelectionListener, buyTableRowSelectionListener;
private HBox bottomHBox;
private ListChangeListener<OfferBookListItem> changeListener;
private ListChangeListener<CurrencyListItem> currencyListItemsListener;
///////////////////////////////////////////////////////////////////////////////////////////
// Constructor, lifecycle
///////////////////////////////////////////////////////////////////////////////////////////
@Inject
public OfferBookChartView(OfferBookChartViewModel model, Navigation navigation, BSFormatter formatter) {
super(model);
this.navigation = navigation;
this.formatter = formatter;
}
@Override
public void initialize() {
changeListener = c -> updateChartData();
currencyListItemsListener = c -> {
if (model.getSelectedCurrencyListItem().isPresent())
currencyComboBox.getSelectionModel().select(model.getSelectedCurrencyListItem().get());
};
currencyComboBox = new ComboBox<>();
currencyComboBox.setPromptText("Select currency");
currencyComboBox.setConverter(GUIUtil.getCurrencyListItemConverter("offers", model.preferences));
Label currencyLabel = new Label("Currency:");
HBox currencyHBox = new HBox();
currencyHBox.setSpacing(5);
currencyHBox.setPadding(new Insets(5, -20, -5, 20));
currencyHBox.setAlignment(Pos.CENTER_LEFT);
currencyHBox.getChildren().addAll(currencyLabel, currencyComboBox);
createChart();
Tuple4<TableView<OfferListItem>, VBox, Button, Label> tupleBuy = getOfferTable(Offer.Direction.BUY);
Tuple4<TableView<OfferListItem>, VBox, Button, Label> tupleSell = getOfferTable(Offer.Direction.SELL);
buyOfferTableView = tupleBuy.first;
sellOfferTableView = tupleSell.first;
buyOfferButton = tupleBuy.third;
sellOfferButton = tupleSell.third;
buyOfferHeaderLabel = tupleBuy.forth;
sellOfferHeaderLabel = tupleSell.forth;
bottomHBox = new HBox();
bottomHBox.setSpacing(20); //30
bottomHBox.setAlignment(Pos.CENTER);
HBox.setHgrow(tupleBuy.second, Priority.ALWAYS);
HBox.setHgrow(tupleSell.second, Priority.ALWAYS);
tupleBuy.second.setUserData("BUY");
tupleSell.second.setUserData("SELL");
bottomHBox.getChildren().addAll(tupleBuy.second, tupleSell.second);
root.getChildren().addAll(currencyHBox, areaChart, bottomHBox);
}
@Override
protected void activate() {
// root.getParent() is null at initialize
tabPaneSelectionModel = GUIUtil.getParentOfType(root, TabPane.class).getSelectionModel();
selectedTabIndexListener = (observable, oldValue, newValue) -> model.setSelectedTabIndex((int) newValue);
model.setSelectedTabIndex(tabPaneSelectionModel.getSelectedIndex());
tabPaneSelectionModel.selectedIndexProperty().addListener(selectedTabIndexListener);
currencyComboBox.setItems(model.getCurrencyListItems());
currencyComboBox.setVisibleRowCount(25);
if (model.getSelectedCurrencyListItem().isPresent())
currencyComboBox.getSelectionModel().select(model.getSelectedCurrencyListItem().get());
currencyComboBox.setOnAction(e -> {
CurrencyListItem selectedItem = currencyComboBox.getSelectionModel().getSelectedItem();
if (selectedItem != null) {
model.onSetTradeCurrency(selectedItem.tradeCurrency);
updateChartData();
}
});
model.currencyListItems.addListener(currencyListItemsListener);
model.getOfferBookListItems().addListener(changeListener);
tradeCurrencySubscriber = EasyBind.subscribe(model.selectedTradeCurrencyProperty,
tradeCurrency -> {
String code = tradeCurrency.getCode();
areaChart.setTitle("Offer book for " + formatter.getCurrencyNameAndCurrencyPair(code));
volumeColumnLabel.set("Amount in " + code);
xAxis.setTickLabelFormatter(new StringConverter<Number>() {
@Override
public String toString(Number object) {
final double doubleValue = (double) object;
if (CurrencyUtil.isCryptoCurrency(model.getCurrencyCode())) {
final String withPrecision3 = formatter.formatRoundedDoubleWithPrecision(doubleValue, 3);
if (withPrecision3.equals("0.000"))
return formatter.formatRoundedDoubleWithPrecision(doubleValue, 8);
else
return withPrecision3;
} else {
return formatter.formatRoundedDoubleWithPrecision(doubleValue, 2);
}
}
@Override
public Number fromString(String string) {
return null;
}
});
if (CurrencyUtil.isCryptoCurrency(code)) {
if (bottomHBox.getChildren().size() == 2 && bottomHBox.getChildren().get(0).getUserData().equals("BUY")) {
bottomHBox.getChildren().get(0).toFront();
reverseTableColumns();
}
buyOfferHeaderLabel.setText("Offers to sell " + code + " for BTC");
buyOfferButton.setText("I want to buy " + code + " (sell BTC)");
sellOfferHeaderLabel.setText("Offers to buy " + code + " with BTC");
sellOfferButton.setText("I want to sell " + code + " (buy BTC)");
priceColumnLabel.set("Price in BTC");
} else {
if (bottomHBox.getChildren().size() == 2 && bottomHBox.getChildren().get(0).getUserData().equals("SELL")) {
bottomHBox.getChildren().get(0).toFront();
reverseTableColumns();
}
buyOfferHeaderLabel.setText("Offers to buy BTC with " + code);
buyOfferButton.setText("I want to sell BTC for " + code);
sellOfferHeaderLabel.setText("Offers to sell BTC for " + code);
sellOfferButton.setText("I want to buy BTC with " + code);
priceColumnLabel.set("Price in " + code);
}
xAxis.setLabel(formatter.getPriceWithCurrencyCode(code));
seriesBuy.setName(buyOfferHeaderLabel.getText() + " ");
seriesSell.setName(sellOfferHeaderLabel.getText());
});
buyOfferTableView.setItems(model.getTopBuyOfferList());
sellOfferTableView.setItems(model.getTopSellOfferList());
buyTableRowSelectionListener = (observable, oldValue, newValue) -> {
model.preferences.setSellScreenCurrencyCode(model.getCurrencyCode());
navigation.navigateTo(MainView.class, SellOfferView.class);
};
sellTableRowSelectionListener = (observable, oldValue, newValue) -> {
model.preferences.setBuyScreenCurrencyCode(model.getCurrencyCode());
navigation.navigateTo(MainView.class, BuyOfferView.class);
};
buyOfferTableView.getSelectionModel().selectedItemProperty().addListener(buyTableRowSelectionListener);
sellOfferTableView.getSelectionModel().selectedItemProperty().addListener(sellTableRowSelectionListener);
updateChartData();
}
@Override
protected void deactivate() {
model.getOfferBookListItems().removeListener(changeListener);
tabPaneSelectionModel.selectedIndexProperty().removeListener(selectedTabIndexListener);
model.currencyListItems.removeListener(currencyListItemsListener);
tradeCurrencySubscriber.unsubscribe();
currencyComboBox.setOnAction(null);
buyOfferTableView.getSelectionModel().selectedItemProperty().removeListener(buyTableRowSelectionListener);
sellOfferTableView.getSelectionModel().selectedItemProperty().removeListener(sellTableRowSelectionListener);
}
private void createChart() {
xAxis = new NumberAxis();
xAxis.setForceZeroInRange(false);
xAxis.setAutoRanging(true);
yAxis = new NumberAxis();
yAxis.setForceZeroInRange(false);
yAxis.setAutoRanging(true);
yAxis.setLabel("Amount in BTC");
yAxis.setTickLabelFormatter(new NumberAxis.DefaultFormatter(yAxis, "", ""));
seriesBuy = new XYChart.Series<>();
seriesSell = new XYChart.Series<>();
areaChart = new AreaChart<>(xAxis, yAxis);
areaChart.setLegendVisible(false);
areaChart.setAnimated(false);
areaChart.setId("charts");
areaChart.setMinHeight(300);
areaChart.setPrefHeight(300);
areaChart.setPadding(new Insets(0, 30, 0, 0));
areaChart.getData().addAll(seriesBuy, seriesSell);
}
private void updateChartData() {
seriesBuy.getData().clear();
seriesSell.getData().clear();
seriesBuy.getData().addAll(model.getBuyData());
seriesSell.getData().addAll(model.getSellData());
}
private Tuple4<TableView<OfferListItem>, VBox, Button, Label> getOfferTable(Offer.Direction direction) {
TableView<OfferListItem> tableView = new TableView<>();
tableView.setMinHeight(109);
tableView.setPrefHeight(121);
tableView.setMinWidth(480); //530
// price
TableColumn<OfferListItem, OfferListItem> priceColumn = new TableColumn<>();
priceColumn.textProperty().bind(priceColumnLabel);
priceColumn.setMinWidth(115); //130
priceColumn.setMaxWidth(115); //130
priceColumn.setSortable(false);
priceColumn.setCellValueFactory((offer) -> new ReadOnlyObjectWrapper<>(offer.getValue()));
priceColumn.setCellFactory(
new Callback<TableColumn<OfferListItem, OfferListItem>, TableCell<OfferListItem, OfferListItem>>() {
@Override
public TableCell<OfferListItem, OfferListItem> call(TableColumn<OfferListItem, OfferListItem> column) {
return new TableCell<OfferListItem, OfferListItem>() {
private Offer offer;
ChangeListener<Number> listener = new ChangeListener<Number>() {
@Override
public void changed(ObservableValue<? extends Number> observable, Number oldValue, Number newValue) {
if (offer != null && offer.getPrice() != null) {
setText(formatter.formatPrice(offer.getPrice()));
model.priceFeedService.currenciesUpdateFlagProperty().removeListener(listener);
}
}
};
@Override
public void updateItem(final OfferListItem offerListItem, boolean empty) {
super.updateItem(offerListItem, empty);
if (offerListItem != null && !empty) {
if (offerListItem.offer.getPrice() == null) {
this.offer = offerListItem.offer;
model.priceFeedService.currenciesUpdateFlagProperty().addListener(listener);
setText("N/A");
} else {
setText(formatter.formatPrice(offerListItem.offer.getPrice()));
}
} else {
if (listener != null)
model.priceFeedService.currenciesUpdateFlagProperty().removeListener(listener);
this.offer = null;
setText("");
}
}
};
}
});
// volume
TableColumn<OfferListItem, OfferListItem> volumeColumn = new TableColumn<>();
volumeColumn.setMinWidth(115); //125
volumeColumn.setSortable(false);
volumeColumn.textProperty().bind(volumeColumnLabel);
volumeColumn.setCellValueFactory((offer) -> new ReadOnlyObjectWrapper<>(offer.getValue()));
volumeColumn.setCellFactory(
new Callback<TableColumn<OfferListItem, OfferListItem>, TableCell<OfferListItem, OfferListItem>>() {
@Override
public TableCell<OfferListItem, OfferListItem> call(TableColumn<OfferListItem, OfferListItem> column) {
return new TableCell<OfferListItem, OfferListItem>() {
private Offer offer;
ChangeListener<Number> listener = new ChangeListener<Number>() {
@Override
public void changed(ObservableValue<? extends Number> observable, Number oldValue, Number newValue) {
if (offer != null && offer.getPrice() != null) {
setText(formatter.formatVolume(offer.getOfferVolume()));
model.priceFeedService.currenciesUpdateFlagProperty().removeListener(listener);
}
}
};
@Override
public void updateItem(final OfferListItem offerListItem, boolean empty) {
super.updateItem(offerListItem, empty);
if (offerListItem != null && !empty) {
this.offer = offerListItem.offer;
if (offer.getPrice() == null) {
this.offer = offerListItem.offer;
model.priceFeedService.currenciesUpdateFlagProperty().addListener(listener);
setText("N/A");
} else {
setText(formatter.formatVolume(offer.getOfferVolume()));
}
} else {
if (listener != null)
model.priceFeedService.currenciesUpdateFlagProperty().removeListener(listener);
this.offer = null;
setText("");
}
}
};
}
});
// amount
TableColumn<OfferListItem, OfferListItem> amountColumn = new TableColumn<>("Amount in BTC");
amountColumn.setMinWidth(115); //125
amountColumn.setSortable(false);
amountColumn.setCellValueFactory((offer) -> new ReadOnlyObjectWrapper<>(offer.getValue()));
amountColumn.setCellFactory(
new Callback<TableColumn<OfferListItem, OfferListItem>, TableCell<OfferListItem, OfferListItem>>() {
@Override
public TableCell<OfferListItem, OfferListItem> call(TableColumn<OfferListItem, OfferListItem> column) {
return new TableCell<OfferListItem, OfferListItem>() {
@Override
public void updateItem(final OfferListItem offerListItem, boolean empty) {
super.updateItem(offerListItem, empty);
if (offerListItem != null && !empty)
setText(formatter.formatCoin(offerListItem.offer.getAmount()));
else
setText("");
}
};
}
});
// accumulated
TableColumn<OfferListItem, OfferListItem> accumulatedColumn = new TableColumn<>("Sum in BTC");
accumulatedColumn.setMinWidth(100);//130
accumulatedColumn.setSortable(false);
accumulatedColumn.setCellValueFactory((offer) -> new ReadOnlyObjectWrapper<>(offer.getValue()));
accumulatedColumn.setCellFactory(
new Callback<TableColumn<OfferListItem, OfferListItem>, TableCell<OfferListItem, OfferListItem>>() {
@Override
public TableCell<OfferListItem, OfferListItem> call(TableColumn<OfferListItem, OfferListItem> column) {
return new TableCell<OfferListItem, OfferListItem>() {
@Override
public void updateItem(final OfferListItem offerListItem, boolean empty) {
super.updateItem(offerListItem, empty);
if (offerListItem != null && !empty)
setText(formatter.formatRoundedDoubleWithPrecision(offerListItem.accumulated, 4));
else
setText("");
}
};
}
});
if (direction == Offer.Direction.BUY) {
tableView.getColumns().add(accumulatedColumn);
tableView.getColumns().add(volumeColumn);
tableView.getColumns().add(amountColumn);
tableView.getColumns().add(priceColumn);
} else {
tableView.getColumns().add(priceColumn);
tableView.getColumns().add(amountColumn);
tableView.getColumns().add(volumeColumn);
tableView.getColumns().add(accumulatedColumn);
}
tableView.setColumnResizePolicy(TableView.CONSTRAINED_RESIZE_POLICY);
Label placeholder = new Label("Currently there are no offers available");
placeholder.setWrapText(true);
tableView.setPlaceholder(placeholder);
Label titleLabel = new Label();
titleLabel.setStyle("-fx-font-weight: bold; -fx-font-size: 16; -fx-alignment: center");
UserThread.execute(() -> titleLabel.prefWidthProperty().bind(tableView.widthProperty()));
boolean isSellOffer = direction == Offer.Direction.SELL;
Button button = new Button();
ImageView iconView = new ImageView();
iconView.setId(isSellOffer ? "image-buy-white" : "image-sell-white");
button.setGraphic(iconView);
button.setGraphicTextGap(10);
button.setText(isSellOffer ? "I want to buy bitcoin" : "I want to sell bitcoin");
button.setMinHeight(40);
button.setId(isSellOffer ? "buy-button-big" : "sell-button-big");
button.setOnAction(e -> {
if (isSellOffer) {
model.preferences.setBuyScreenCurrencyCode(model.getCurrencyCode());
navigation.navigateTo(MainView.class, BuyOfferView.class);
} else {
model.preferences.setSellScreenCurrencyCode(model.getCurrencyCode());
navigation.navigateTo(MainView.class, SellOfferView.class);
}
});
VBox vBox = new VBox();
vBox.setSpacing(10);
vBox.setFillWidth(true);
vBox.setMinHeight(190);
vBox.getChildren().addAll(titleLabel, tableView, button);
button.prefWidthProperty().bind(vBox.widthProperty());
return new Tuple4<>(tableView, vBox, button, titleLabel);
}
private void reverseTableColumns() {
ObservableList<TableColumn<OfferListItem, ?>> columns = FXCollections.observableArrayList(buyOfferTableView.getColumns());
buyOfferTableView.getColumns().clear();
Collections.reverse(columns);
buyOfferTableView.getColumns().addAll(columns);
columns = FXCollections.observableArrayList(sellOfferTableView.getColumns());
sellOfferTableView.getColumns().clear();
Collections.reverse(columns);
sellOfferTableView.getColumns().addAll(columns);
}
}