package com.faforever.client.chat;
import com.faforever.client.achievements.AchievementItemController;
import com.faforever.client.achievements.AchievementService;
import com.faforever.client.api.AchievementDefinition;
import com.faforever.client.api.PlayerAchievement;
import com.faforever.client.api.PlayerEvent;
import com.faforever.client.api.RatingType;
import com.faforever.client.domain.RatingHistoryDataPoint;
import com.faforever.client.events.EventService;
import com.faforever.client.i18n.I18n;
import com.faforever.client.preferences.PreferencesService;
import com.faforever.client.stats.StatisticsService;
import com.faforever.client.util.IdenticonUtil;
import com.faforever.client.util.RatingUtil;
import com.faforever.client.util.TimeService;
import com.neovisionaries.i18n.CountryCode;
import javafx.application.Platform;
import javafx.beans.InvalidationListener;
import javafx.beans.binding.Bindings;
import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
import javafx.fxml.FXML;
import javafx.scene.Node;
import javafx.scene.chart.LineChart;
import javafx.scene.chart.NumberAxis;
import javafx.scene.chart.PieChart;
import javafx.scene.chart.StackedBarChart;
import javafx.scene.chart.XYChart;
import javafx.scene.chart.XYChart.Data;
import javafx.scene.control.Label;
import javafx.scene.control.ScrollPane;
import javafx.scene.control.ToggleButton;
import javafx.scene.image.ImageView;
import javafx.scene.layout.Pane;
import javafx.scene.layout.Region;
import javafx.util.StringConverter;
import org.jetbrains.annotations.NotNull;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.context.ApplicationContext;
import javax.annotation.Resource;
import java.lang.invoke.MethodHandles;
import java.time.format.DateTimeFormatter;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.concurrent.CompletionStage;
import java.util.stream.Collectors;
import static com.faforever.client.api.AchievementState.UNLOCKED;
import static com.faforever.client.events.EventService.EVENT_AEON_PLAYS;
import static com.faforever.client.events.EventService.EVENT_AEON_WINS;
import static com.faforever.client.events.EventService.EVENT_BUILT_AIR_UNITS;
import static com.faforever.client.events.EventService.EVENT_BUILT_LAND_UNITS;
import static com.faforever.client.events.EventService.EVENT_BUILT_NAVAL_UNITS;
import static com.faforever.client.events.EventService.EVENT_BUILT_TECH_1_UNITS;
import static com.faforever.client.events.EventService.EVENT_BUILT_TECH_2_UNITS;
import static com.faforever.client.events.EventService.EVENT_BUILT_TECH_3_UNITS;
import static com.faforever.client.events.EventService.EVENT_CUSTOM_GAMES_PLAYED;
import static com.faforever.client.events.EventService.EVENT_CYBRAN_PLAYS;
import static com.faforever.client.events.EventService.EVENT_CYBRAN_WINS;
import static com.faforever.client.events.EventService.EVENT_RANKED_1V1_GAMES_PLAYED;
import static com.faforever.client.events.EventService.EVENT_SERAPHIM_PLAYS;
import static com.faforever.client.events.EventService.EVENT_SERAPHIM_WINS;
import static com.faforever.client.events.EventService.EVENT_UEF_PLAYS;
import static com.faforever.client.events.EventService.EVENT_UEF_WINS;
import static javafx.collections.FXCollections.observableList;
public class UserInfoWindowController {
private static final DateTimeFormatter DATE_FORMATTER = DateTimeFormatter.ofPattern("d MMM");
private static final Logger logger = LoggerFactory.getLogger(MethodHandles.lookup().lookupClass());
@FXML
Label lockedAchievementsHeaderLabel;
@FXML
Label unlockedAchievementsHeaderLabel;
@FXML
PieChart gamesPlayedChart;
@FXML
PieChart techBuiltChart;
@FXML
PieChart unitsBuiltChart;
@FXML
StackedBarChart factionsChart;
@FXML
Label gamesPlayedLabel;
@FXML
Label ratingLabelGlobal;
@FXML
Label ratingLabel1v1;
@FXML
ImageView avatarImageView;
@FXML
Pane unlockedAchievementsHeader;
@FXML
Pane lockedAchievementsHeader;
@FXML
ScrollPane achievementsPane;
@FXML
ImageView mostRecentAchievementImageView;
@FXML
Label mostRecentAchievementDescriptionLabel;
@FXML
Label loadingProgressLabel;
@FXML
Pane mostRecentAchievementPane;
@FXML
Label mostRecentAchievementNameLabel;
@FXML
Pane lockedAchievementsContainer;
@FXML
Pane unlockedAchievementsContainer;
@FXML
ToggleButton globalButton;
@FXML
ToggleButton ladder1v1Button;
@FXML
NumberAxis yAxis;
@FXML
NumberAxis xAxis;
@FXML
LineChart<Integer, Integer> ratingHistoryChart;
@FXML
Label usernameLabel;
@FXML
Label countryLabel;
@FXML
ImageView countryImageView;
@FXML
Pane userInfoRoot;
@Resource
StatisticsService statisticsService;
@Resource
CountryFlagService countryFlagService;
@Resource
AchievementService achievementService;
@Resource
EventService eventService;
@Resource
PreferencesService preferencesService;
@Resource
ApplicationContext applicationContext;
@Resource
I18n i18n;
@Resource
Locale locale;
@Resource
TimeService timeService;
private PlayerInfoBean playerInfoBean;
private Map<String, AchievementItemController> achievementItemById;
private Map<String, AchievementDefinition> achievementDefinitionById;
private int earnedExperiencePoints;
public UserInfoWindowController() {
achievementItemById = new HashMap<>();
achievementDefinitionById = new HashMap<>();
}
private static boolean isUnlocked(PlayerAchievement playerAchievement) {
return UNLOCKED == playerAchievement.getState();
}
@FXML
void initialize() {
loadingProgressLabel.managedProperty().bind(loadingProgressLabel.visibleProperty());
achievementsPane.managedProperty().bind(achievementsPane.visibleProperty());
mostRecentAchievementPane.managedProperty().bind(mostRecentAchievementPane.visibleProperty());
unlockedAchievementsHeader.managedProperty().bind(unlockedAchievementsHeader.visibleProperty());
unlockedAchievementsHeader.visibleProperty().bind(unlockedAchievementsContainer.visibleProperty());
unlockedAchievementsContainer.managedProperty().bind(unlockedAchievementsContainer.visibleProperty());
unlockedAchievementsContainer.visibleProperty().bind(Bindings.createBooleanBinding(
() -> !unlockedAchievementsContainer.getChildren().isEmpty(), unlockedAchievementsContainer.getChildren()));
lockedAchievementsHeader.managedProperty().bind(lockedAchievementsHeader.visibleProperty());
lockedAchievementsHeader.visibleProperty().bind(lockedAchievementsContainer.visibleProperty());
lockedAchievementsContainer.managedProperty().bind(lockedAchievementsContainer.visibleProperty());
lockedAchievementsContainer.visibleProperty().bind(Bindings.createBooleanBinding(
() -> !lockedAchievementsContainer.getChildren().isEmpty(), lockedAchievementsContainer.getChildren()));
lockedAchievementsContainer.getChildren().addListener((InvalidationListener) observable ->
lockedAchievementsHeaderLabel.setText(i18n.get("achievements.locked", lockedAchievementsContainer.getChildren().size()))
);
unlockedAchievementsContainer.getChildren().addListener((InvalidationListener) observable ->
unlockedAchievementsHeaderLabel.setText(i18n.get("achievements.unlocked", unlockedAchievementsContainer.getChildren().size()))
);
getRoot().sceneProperty().addListener((observable, oldValue, newValue) -> {
if (newValue != null) {
newValue.getWindow().showingProperty().addListener((observable11, oldValue11, newValue11) -> {
if (!newValue11) {
// Fixes #241
userInfoRoot.getChildren().clear();
}
});
}
});
}
public Region getRoot() {
return userInfoRoot;
}
private void displayAvailableAchievements(List<AchievementDefinition> achievementDefinitions) {
ObservableList<Node> children = lockedAchievementsContainer.getChildren();
Platform.runLater(children::clear);
achievementDefinitions.forEach(achievementDefinition -> {
AchievementItemController controller = applicationContext.getBean(AchievementItemController.class);
controller.setAchievementDefinition(achievementDefinition);
achievementDefinitionById.put(achievementDefinition.getId(), achievementDefinition);
achievementItemById.put(achievementDefinition.getId(), controller);
Platform.runLater(() -> children.add(controller.getRoot()));
});
}
public void setPlayerInfoBean(PlayerInfoBean playerInfoBean) {
this.playerInfoBean = playerInfoBean;
usernameLabel.setText(playerInfoBean.getUsername());
countryImageView.setImage(countryFlagService.loadCountryFlag(playerInfoBean.getCountry()));
avatarImageView.setImage(IdenticonUtil.createIdenticon(playerInfoBean.getId()));
gamesPlayedLabel.setText(String.format(locale, "%d", playerInfoBean.getNumberOfGames()));
ratingLabelGlobal.setText(String.format(locale, "%d", RatingUtil.getRoundedGlobalRating(playerInfoBean)));
ratingLabel1v1.setText(String.format(locale, "%d", RatingUtil.getLeaderboardRating(playerInfoBean)));
CountryCode countryCode = CountryCode.getByCode(playerInfoBean.getCountry());
if (countryCode != null) {
// Country code is unknown to CountryCode, like A1 or A2 (from GeoIP)
countryLabel.setText(countryCode.getName());
} else {
countryLabel.setText(playerInfoBean.getCountry());
}
globalButton.fire();
globalButton.setSelected(true);
loadAchievements();
eventService.getPlayerEvents(playerInfoBean.getUsername()).thenAccept(events -> {
plotFactionsChart(events);
plotUnitsByCategoriesChart(events);
plotTechBuiltChart(events);
plotGamesPlayedChart(events);
});
}
private void loadAchievements() {
enterAchievementsLoadingState();
achievementService.getAchievementDefinitions()
.exceptionally(throwable -> {
// TODO display to user
logger.warn("Could not load achievement definitions", throwable);
return Collections.emptyList();
})
.thenAccept(this::displayAvailableAchievements)
.thenCompose(aVoid -> achievementService.getPlayerAchievements(playerInfoBean.getUsername()))
.thenAccept(playerAchievements -> {
updatePlayerAchievements(playerAchievements);
enterAchievementsLoadedState();
})
.exceptionally(throwable -> {
// TODO tell the user
logger.warn("Player achievements could not be loaded", throwable);
return null;
});
}
@SuppressWarnings("unchecked")
private void plotFactionsChart(Map<String, PlayerEvent> playerEvents) {
int aeonPlays = playerEvents.containsKey(EVENT_AEON_PLAYS) ? playerEvents.get(EVENT_AEON_PLAYS).getCount() : 0;
int cybranPlays = playerEvents.containsKey(EVENT_CYBRAN_PLAYS) ? playerEvents.get(EVENT_CYBRAN_PLAYS).getCount() : 0;
int uefPlays = playerEvents.containsKey(EVENT_UEF_PLAYS) ? playerEvents.get(EVENT_UEF_PLAYS).getCount() : 0;
int seraphimPlays = playerEvents.containsKey(EVENT_SERAPHIM_PLAYS) ? playerEvents.get(EVENT_SERAPHIM_PLAYS).getCount() : 0;
int aeonWins = playerEvents.containsKey(EVENT_AEON_WINS) ? playerEvents.get(EVENT_AEON_WINS).getCount() : 0;
int cybranWins = playerEvents.containsKey(EVENT_CYBRAN_WINS) ? playerEvents.get(EVENT_CYBRAN_WINS).getCount() : 0;
int uefWins = playerEvents.containsKey(EVENT_UEF_WINS) ? playerEvents.get(EVENT_UEF_WINS).getCount() : 0;
int seraphimWins = playerEvents.containsKey(EVENT_SERAPHIM_WINS) ? playerEvents.get(EVENT_SERAPHIM_WINS).getCount() : 0;
XYChart.Series<String, Integer> winsSeries = new XYChart.Series<>();
winsSeries.setName(i18n.get("userInfo.wins"));
winsSeries.getData().add(new XYChart.Data<>("Aeon", aeonWins));
winsSeries.getData().add(new XYChart.Data<>("Cybran", cybranWins));
winsSeries.getData().add(new XYChart.Data<>("UEF", uefWins));
winsSeries.getData().add(new XYChart.Data<>("Seraphim", seraphimWins));
XYChart.Series<String, Integer> lossSeries = new XYChart.Series<>();
lossSeries.setName(i18n.get("userInfo.losses"));
lossSeries.getData().add(new XYChart.Data<>("Aeon", aeonPlays - aeonWins));
lossSeries.getData().add(new XYChart.Data<>("Cybran", cybranPlays - cybranWins));
lossSeries.getData().add(new XYChart.Data<>("UEF", uefPlays - uefWins));
lossSeries.getData().add(new XYChart.Data<>("Seraphim", seraphimPlays - seraphimWins));
Platform.runLater(() -> factionsChart.getData().addAll(winsSeries, lossSeries));
}
@SuppressWarnings("unchecked")
private void plotUnitsByCategoriesChart(Map<String, PlayerEvent> playerEvents) {
int airBuilt = playerEvents.containsKey(EVENT_BUILT_AIR_UNITS) ? playerEvents.get(EVENT_BUILT_AIR_UNITS).getCount() : 0;
int landBuilt = playerEvents.containsKey(EVENT_BUILT_LAND_UNITS) ? playerEvents.get(EVENT_BUILT_LAND_UNITS).getCount() : 0;
int navalBuilt = playerEvents.containsKey(EVENT_BUILT_NAVAL_UNITS) ? playerEvents.get(EVENT_BUILT_NAVAL_UNITS).getCount() : 0;
Platform.runLater(() -> unitsBuiltChart.setData(FXCollections.observableArrayList(
new PieChart.Data(i18n.get("stats.air"), airBuilt),
new PieChart.Data(i18n.get("stats.land"), landBuilt),
new PieChart.Data(i18n.get("stats.naval"), navalBuilt)
)));
}
@SuppressWarnings("unchecked")
private void plotTechBuiltChart(Map<String, PlayerEvent> playerEvents) {
int tech1Built = playerEvents.containsKey(EVENT_BUILT_TECH_1_UNITS) ? playerEvents.get(EVENT_BUILT_TECH_1_UNITS).getCount() : 0;
int tech2Built = playerEvents.containsKey(EVENT_BUILT_TECH_2_UNITS) ? playerEvents.get(EVENT_BUILT_TECH_2_UNITS).getCount() : 0;
int tech3Built = playerEvents.containsKey(EVENT_BUILT_TECH_3_UNITS) ? playerEvents.get(EVENT_BUILT_TECH_3_UNITS).getCount() : 0;
Platform.runLater(() -> techBuiltChart.setData(FXCollections.observableArrayList(
new PieChart.Data(i18n.get("stats.tech1"), tech1Built),
new PieChart.Data(i18n.get("stats.tech2"), tech2Built),
new PieChart.Data(i18n.get("stats.tech3"), tech3Built)
)));
}
@SuppressWarnings("unchecked")
private void plotGamesPlayedChart(Map<String, PlayerEvent> playerEvents) {
int tech1Built = playerEvents.containsKey(EVENT_CUSTOM_GAMES_PLAYED) ? playerEvents.get(EVENT_CUSTOM_GAMES_PLAYED).getCount() : 0;
int tech2Built = playerEvents.containsKey(EVENT_RANKED_1V1_GAMES_PLAYED) ? playerEvents.get(EVENT_RANKED_1V1_GAMES_PLAYED).getCount() : 0;
Platform.runLater(() -> gamesPlayedChart.setData(FXCollections.observableArrayList(
new PieChart.Data(i18n.get("stats.custom"), tech1Built),
new PieChart.Data(i18n.get("stats.ranked1v1"), tech2Built)
)));
}
private void enterAchievementsLoadingState() {
loadingProgressLabel.setVisible(true);
achievementsPane.setVisible(false);
}
private void updatePlayerAchievements(List<? extends PlayerAchievement> playerAchievements) {
PlayerAchievement mostRecentPlayerAchievement = null;
int unlockedAchievements = 0;
ObservableList<Node> children = unlockedAchievementsContainer.getChildren();
Platform.runLater(children::clear);
for (PlayerAchievement playerAchievement : playerAchievements) {
AchievementItemController achievementItemController = achievementItemById.get(playerAchievement.getAchievementId());
achievementItemController.setPlayerAchievement(playerAchievement);
if (isUnlocked(playerAchievement)) {
unlockedAchievements++;
earnedExperiencePoints += achievementDefinitionById.get(playerAchievement.getAchievementId()).getExperiencePoints();
Platform.runLater(() -> children.add(achievementItemController.getRoot()));
if (mostRecentPlayerAchievement == null
|| playerAchievement.getUpdateTime().compareTo(mostRecentPlayerAchievement.getUpdateTime()) > 0) {
mostRecentPlayerAchievement = playerAchievement;
}
}
}
if (mostRecentPlayerAchievement == null) {
mostRecentAchievementPane.setVisible(false);
} else {
mostRecentAchievementPane.setVisible(true);
AchievementDefinition mostRecentAchievement = achievementDefinitionById.get(mostRecentPlayerAchievement.getAchievementId());
String mostRecentAchievementName = mostRecentAchievement.getName();
String mostRecentAchievementDescription = mostRecentAchievement.getDescription();
Platform.runLater(() -> {
mostRecentAchievementNameLabel.setText(mostRecentAchievementName);
mostRecentAchievementDescriptionLabel.setText(mostRecentAchievementDescription);
mostRecentAchievementImageView.setImage(achievementService.getRevealedIcon(mostRecentAchievement));
});
}
}
private void enterAchievementsLoadedState() {
loadingProgressLabel.setVisible(false);
achievementsPane.setVisible(true);
}
@FXML
void ladder1v1ButtonClicked() {
loadStatistics(RatingType.LADDER_1V1);
}
private CompletionStage<Void> loadStatistics(RatingType type) {
return statisticsService.getRatingHistory(type, playerInfoBean.getId())
.thenAccept(ratingHistory -> Platform.runLater(() -> plotPlayerRatingGraph(ratingHistory)))
.exceptionally(throwable -> {
// FIXME display to user
logger.warn("Statistics could not be loaded", throwable);
return null;
});
}
@SuppressWarnings("unchecked")
private void plotPlayerRatingGraph(List<RatingHistoryDataPoint> dataPoints) {
List<XYChart.Data<Integer, Integer>> values = dataPoints.stream()
.map(datapoint -> new Data<>(dataPoints.indexOf(datapoint), RatingUtil.getRating(datapoint)))
.collect(Collectors.toList());
xAxis.setTickLabelFormatter(ratingLabelFormatter(dataPoints));
XYChart.Series<Integer, Integer> series = new XYChart.Series<>(observableList(values));
series.setName(i18n.get("userInfo.ratingOverTime"));
ratingHistoryChart.getData().setAll(series);
}
@NotNull
private StringConverter<Number> ratingLabelFormatter(final List<RatingHistoryDataPoint> dataPoints) {
return new StringConverter<Number>() {
@Override
public String toString(Number object) {
int number = object.intValue();
int numberOfDataPoints = dataPoints.size();
int dataPointIndex = number >= numberOfDataPoints ? numberOfDataPoints - 1 : number;
return DATE_FORMATTER.format(dataPoints.get(dataPointIndex).getDateTime());
}
@Override
public Number fromString(String string) {
return null;
}
};
}
@FXML
void globalButtonClicked() {
loadStatistics(RatingType.GLOBAL);
}
}