package com.boardgamegeek.ui;
import android.content.Intent;
import android.database.Cursor;
import android.net.Uri;
import android.os.Build;
import android.os.Build.VERSION;
import android.os.Build.VERSION_CODES;
import android.os.Bundle;
import android.support.v4.app.Fragment;
import android.support.v4.app.LoaderManager;
import android.support.v4.content.ContextCompat;
import android.support.v4.content.CursorLoader;
import android.support.v4.content.Loader;
import android.support.v4.util.ArrayMap;
import android.support.v4.widget.ContentLoadingProgressBar;
import android.support.v7.app.AlertDialog;
import android.text.TextUtils;
import android.transition.AutoTransition;
import android.transition.Transition;
import android.transition.TransitionManager;
import android.util.SparseBooleanArray;
import android.util.SparseIntArray;
import android.view.LayoutInflater;
import android.view.View;
import android.view.View.OnClickListener;
import android.view.ViewGroup;
import android.widget.ImageView;
import android.widget.LinearLayout;
import android.widget.TableLayout;
import android.widget.TextView;
import com.boardgamegeek.R;
import com.boardgamegeek.auth.AccountUtils;
import com.boardgamegeek.provider.BggContract.Collection;
import com.boardgamegeek.provider.BggContract.Games;
import com.boardgamegeek.provider.BggContract.PlayPlayers;
import com.boardgamegeek.provider.BggContract.Plays;
import com.boardgamegeek.ui.widget.IntegerYAxisValueFormatter;
import com.boardgamegeek.ui.widget.PlayStatView;
import com.boardgamegeek.ui.widget.PlayStatView.Builder;
import com.boardgamegeek.ui.widget.PlayerStatView;
import com.boardgamegeek.ui.widget.ScoreGraphView;
import com.boardgamegeek.util.ActivityUtils;
import com.boardgamegeek.util.AnimationUtils;
import com.boardgamegeek.util.CursorUtils;
import com.boardgamegeek.util.PaletteUtils;
import com.boardgamegeek.util.PreferencesUtils;
import com.boardgamegeek.util.SelectionBuilder;
import com.boardgamegeek.util.StringUtils;
import com.boardgamegeek.util.UIUtils;
import com.github.mikephil.charting.animation.Easing.EasingOption;
import com.github.mikephil.charting.charts.HorizontalBarChart;
import com.github.mikephil.charting.data.BarData;
import com.github.mikephil.charting.data.BarDataSet;
import com.github.mikephil.charting.data.BarEntry;
import com.github.mikephil.charting.interfaces.datasets.IBarDataSet;
import java.text.DateFormat;
import java.text.DecimalFormat;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Calendar;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;
import java.util.concurrent.TimeUnit;
import butterknife.BindView;
import butterknife.BindViews;
import butterknife.ButterKnife;
import butterknife.OnClick;
import butterknife.Unbinder;
import timber.log.Timber;
public class GamePlayStatsFragment extends Fragment implements LoaderManager.LoaderCallbacks<Cursor> {
private static final DecimalFormat SCORE_FORMAT = new DecimalFormat("0.##");
private static final DateFormat FORMAT = new SimpleDateFormat("yyyy-MM-dd", Locale.US);
private int gameId;
private int playingTime;
private double personalRating;
private Stats stats;
private final SparseBooleanArray selectedItems = new SparseBooleanArray();
private Unbinder unbinder;
@BindView(R.id.progress) ContentLoadingProgressBar progressView;
@BindView(R.id.empty) View emptyView;
@BindView(R.id.data) View dataView;
@BindView(R.id.table_play_count) TableLayout playCountTable;
@BindView(R.id.chart_play_count) HorizontalBarChart playCountChart;
@BindView(R.id.card_score) View scoresCard;
@BindView(R.id.low_score) TextView lowScoreView;
@BindView(R.id.average_score) TextView averageScoreView;
@BindView(R.id.average_win_score) TextView averageWinScoreView;
@BindView(R.id.score_graph) ScoreGraphView scoreGraphView;
@BindView(R.id.high_score) TextView highScoreView;
@BindView(R.id.card_players) View playersCard;
@BindView(R.id.list_players) LinearLayout playersList;
@BindView(R.id.table_dates) TableLayout datesTable;
@BindView(R.id.table_play_time) TableLayout playTimeTable;
@BindView(R.id.card_locations) View locationsCard;
@BindView(R.id.table_locations) TableLayout locationsTable;
@BindView(R.id.table_advanced) TableLayout advancedTable;
@BindViews({
R.id.header_play_count,
R.id.header_scores,
R.id.header_players,
R.id.header_dates,
R.id.header_play_time,
R.id.header_locations,
R.id.header_advanced
}) List<TextView> colorizedHeaders;
@BindViews({
R.id.score_help
}) List<ImageView> colorizedIcons;
private Transition playerTransition;
private int headerColor;
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
final Intent intent = UIUtils.fragmentArgumentsToIntent(getArguments());
Uri uri = intent.getData();
gameId = Games.getGameId(uri);
headerColor = intent.getIntExtra(ActivityUtils.KEY_HEADER_COLOR, R.color.accent);
}
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
View rootView = inflater.inflate(R.layout.fragment_game_play_stats, container, false);
unbinder = ButterKnife.bind(this, rootView);
ButterKnife.apply(colorizedHeaders, PaletteUtils.rgbTextViewSetter, headerColor);
ButterKnife.apply(colorizedIcons, PaletteUtils.rgbIconSetter, headerColor);
playCountChart.setDrawGridBackground(false);
playCountChart.getAxisRight().setValueFormatter(new IntegerYAxisValueFormatter());
playCountChart.getAxisLeft().setEnabled(false);
playCountChart.getXAxis().setDrawGridLines(false);
playCountChart.setDescription(null);
if (VERSION.SDK_INT >= VERSION_CODES.KITKAT) {
playerTransition = new AutoTransition();
playerTransition.setDuration(150);
AnimationUtils.setInterpolator(getContext(), playerTransition);
}
return rootView;
}
@Override
public void onActivityCreated(Bundle savedInstanceState) {
super.onActivityCreated(savedInstanceState);
getLoaderManager().restartLoader(GameQuery._TOKEN, null, this);
}
@Override
public void onDestroyView() {
super.onDestroyView();
if (unbinder != null) unbinder.unbind();
}
@Override
public Loader<Cursor> onCreateLoader(int id, Bundle data) {
CursorLoader loader = null;
String playSelection = Plays.OBJECT_ID + "=? AND " + SelectionBuilder.whereZeroOrNull(Plays.DELETE_TIMESTAMP);
String[] selectionArgs = { String.valueOf(gameId) };
switch (id) {
case GameQuery._TOKEN:
loader = new CursorLoader(getActivity(), Collection.CONTENT_URI, GameQuery.PROJECTION, "collection." + Collection.GAME_ID + "=?", selectionArgs, null);
loader.setUpdateThrottle(5000);
break;
case PlayQuery._TOKEN:
loader = new CursorLoader(getActivity(), Plays.CONTENT_URI, PlayQuery.PROJECTION, playSelection, selectionArgs, Plays.DATE + " ASC");
loader.setUpdateThrottle(5000);
break;
case PlayerQuery._TOKEN:
loader = new CursorLoader(getActivity(), Plays.buildPlayersUri(), PlayerQuery.PROJECTION, playSelection, selectionArgs, null);
loader.setUpdateThrottle(5000);
break;
}
return loader;
}
@Override
public void onLoadFinished(Loader<Cursor> loader, Cursor cursor) {
if (getActivity() == null) return;
int token = loader.getId();
switch (token) {
case GameQuery._TOKEN:
if (cursor == null || !cursor.moveToFirst()) {
playingTime = 0;
personalRating = 0.0;
} else {
playingTime = cursor.getInt(GameQuery.PLAYING_TIME);
double ratingSum = 0;
int ratingCount = 0;
do {
double rating = cursor.getDouble(GameQuery.RATING);
if (rating > 0) {
ratingSum += rating;
ratingCount++;
}
} while (cursor.moveToNext());
if (ratingCount == 0) {
personalRating = 0.0;
} else {
personalRating = ratingSum / ratingCount;
}
}
getLoaderManager().restartLoader(PlayQuery._TOKEN, null, this);
break;
case PlayQuery._TOKEN:
if (cursor == null || !cursor.moveToFirst()) {
showEmpty();
return;
}
stats = new Stats(cursor, personalRating);
getLoaderManager().restartLoader(PlayerQuery._TOKEN, null, this);
break;
case PlayerQuery._TOKEN:
if (cursor != null && cursor.moveToFirst()) {
stats.addPlayerData(cursor);
}
stats.calculate();
bindUi(stats);
showData();
break;
default:
cursor.close();
break;
}
}
private void bindUi(Stats stats) {
playCountTable.removeAllViews();
datesTable.removeAllViews();
playTimeTable.removeAllViews();
advancedTable.removeAllViews();
if (!TextUtils.isEmpty(stats.getDollarDate())) {
addStatRow(playCountTable, new Builder().value(getString(R.string.play_stat_dollar)));
} else if (!TextUtils.isEmpty(stats.getHalfDollarDate())) {
addStatRow(playCountTable, new Builder().value(getString(R.string.play_stat_half_dollar)));
} else if (!TextUtils.isEmpty(stats.getQuarterDate())) {
addStatRow(playCountTable, new Builder().value(getString(R.string.play_stat_quarter)));
} else if (!TextUtils.isEmpty(stats.getDimeDate())) {
addStatRow(playCountTable, new Builder().value(getString(R.string.play_stat_dime)));
} else if (!TextUtils.isEmpty(stats.getNickelDate())) {
addStatRow(playCountTable, new Builder().value(getString(R.string.play_stat_nickel)));
}
addStatRow(playCountTable, new Builder().labelId(R.string.play_stat_play_count).value(stats.getPlayCount()));
if (stats.getPlayCountIncomplete() > 0) {
addStatRow(playCountTable, new Builder().labelId(R.string.play_stat_play_count_incomplete).value(stats.getPlayCountIncomplete()));
}
addStatRow(playCountTable, new Builder().labelId(R.string.play_stat_months_played).value(stats.getMonthsPlayed()));
if (stats.getPlayRate() > 0) {
addStatRow(playCountTable, new Builder().labelId(R.string.play_stat_play_rate).value(stats.getPlayRate()));
}
ArrayList<String> playersLabels = new ArrayList<>();
ArrayList<BarEntry> playCountValues = new ArrayList<>();
ArrayList<BarEntry> winValues = new ArrayList<>();
int index = 0;
for (int i = stats.getMinPlayerCount(); i <= stats.getMaxPlayerCount(); i++) {
playersLabels.add(String.valueOf(i));
playCountValues.add(new BarEntry(new float[] { stats.getWinnablePlayCount(i), stats.getPlayCount(i) - stats.getWinnablePlayCount(i) }, index));
winValues.add(new BarEntry(stats.getWinCount(i), index));
index++;
}
ArrayList<IBarDataSet> dataSets = new ArrayList<>();
BarDataSet playCountDataSet = new BarDataSet(playCountValues, getString(R.string.title_plays));
playCountDataSet.setDrawValues(false);
playCountDataSet.setHighlightEnabled(false);
playCountDataSet.setColors(new int[] { ContextCompat.getColor(getContext(), R.color.dark_blue), ContextCompat.getColor(getContext(), R.color.light_blue) });
playCountDataSet.setStackLabels(new String[] { getString(R.string.winnable), getString(R.string.all) });
dataSets.add(playCountDataSet);
BarDataSet winsDataSet = new BarDataSet(winValues, getString(R.string.title_wins));
winsDataSet.setDrawValues(false);
winsDataSet.setHighlightEnabled(false);
winsDataSet.setColor(ContextCompat.getColor(getContext(), R.color.orange));
dataSets.add(winsDataSet);
BarData data = new BarData(playersLabels, dataSets);
playCountChart.setData(data);
playCountChart.animateY(1000, EasingOption.EaseInOutBack);
if (stats.hasScores()) {
lowScoreView.setText(SCORE_FORMAT.format(stats.getLowScore()));
averageScoreView.setText(SCORE_FORMAT.format(stats.getAverageScore()));
averageWinScoreView.setText(SCORE_FORMAT.format(stats.getAverageWinningScore()));
highScoreView.setText(SCORE_FORMAT.format(stats.getHighScore()));
if (stats.getHighScore() > stats.getLowScore()) {
scoreGraphView.setLowScore(stats.getLowScore());
scoreGraphView.setAverageScore(stats.getAverageScore());
scoreGraphView.setAverageWinScore(stats.getAverageWinningScore());
scoreGraphView.setHighScore(stats.getHighScore());
scoreGraphView.setVisibility(View.VISIBLE);
}
scoresCard.setVisibility(View.VISIBLE);
} else {
scoresCard.setVisibility(View.GONE);
}
addStatRowMaybe(datesTable, new Builder().labelId(R.string.play_stat_first_play).valueAsDate(stats.getFirstPlayDate(), getActivity()));
addStatRowMaybe(datesTable, new Builder().labelId(R.string.play_stat_nickel).valueAsDate(stats.getNickelDate(), getActivity()));
addStatRowMaybe(datesTable, new Builder().labelId(R.string.play_stat_dime).valueAsDate(stats.getDimeDate(), getActivity()));
addStatRowMaybe(datesTable, new Builder().labelId(R.string.play_stat_quarter).valueAsDate(stats.getQuarterDate(), getActivity()));
addStatRowMaybe(datesTable, new Builder().labelId(R.string.play_stat_half_dollar).valueAsDate(stats.getHalfDollarDate(), getActivity()));
addStatRowMaybe(datesTable, new Builder().labelId(R.string.play_stat_dollar).valueAsDate(stats.getDollarDate(), getActivity()));
addStatRowMaybe(datesTable, new Builder().labelId(R.string.play_stat_last_play).valueAsDate(stats.getLastPlayDate(), getActivity()));
addStatRow(playTimeTable, new Builder().labelId(R.string.play_stat_hours_played).value((int) stats.getHoursPlayed()));
int average = stats.getAveragePlayTime();
if (average > 0) {
addStatRow(playTimeTable, new Builder().labelId(R.string.play_stat_average_play_time).valueInMinutes(average));
if (playingTime > 0) {
if (average > playingTime) {
addStatRow(playTimeTable, new Builder().labelId(R.string.play_stat_average_play_time_slower).valueInMinutes(average - playingTime));
} else if (playingTime > average) {
addStatRow(playTimeTable, new Builder().labelId(R.string.play_stat_average_play_time_faster).valueInMinutes(playingTime - average));
}
// don't display anything if the average is exactly as expected
}
}
int averagePerPlayer = stats.getAveragePlayTimePerPlayer();
if (averagePerPlayer > 0) {
addStatRow(playTimeTable, new Builder().labelId(R.string.play_stat_average_play_time_per_player).valueInMinutes(averagePerPlayer));
}
locationsTable.removeAllViews();
for (Entry<String, Integer> location : stats.getPlaysPerLocation()) {
locationsCard.setVisibility(View.VISIBLE);
addStatRow(locationsTable, new Builder().labelText(location.getKey()).value(location.getValue()));
}
playersList.removeAllViews();
int position = 0;
for (Entry<String, PlayerStats> playerStats : stats.getPlayerStats()) {
position++;
playersCard.setVisibility(View.VISIBLE);
PlayerStats ps = playerStats.getValue();
final PlayerStatView view = new PlayerStatView(getActivity());
view.setName(playerStats.getKey());
view.setWinInfo(ps.wins, ps.winnableGames);
view.setWinSkill(ps.getWinSkill());
view.setOverallLowScore(stats.getLowScore());
view.setOverallAverageScore(stats.getAverageScore());
view.setOverallAverageWinScore(stats.getAverageWinningScore());
view.setOverallHighScore(stats.getHighScore());
view.setLowScore(ps.getLowScore());
view.setAverageScore(ps.getAverageScore());
view.setAverageWinScore(ps.getAverageWinScore());
view.setHighScore(ps.getHighScore());
view.showScores(selectedItems.get(position, false));
if (stats.hasScores()) {
final int finalPosition = position;
view.setOnClickListener(new OnClickListener() {
@Override
public void onClick(View v) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
TransitionManager.beginDelayedTransition(playersList, playerTransition);
}
if (selectedItems.get(finalPosition, false)) {
selectedItems.delete(finalPosition);
view.showScores(false);
} else {
selectedItems.put(finalPosition, true);
view.showScores(true);
}
}
});
}
playersList.addView(view);
}
if (personalRating > 0) {
addStatRow(advancedTable, new Builder().labelId(R.string.play_stat_fhm).value(stats.calculateFhm()).infoId(R.string.play_stat_fhm_info));
addStatRow(advancedTable, new Builder().labelId(R.string.play_stat_hhm).value(stats.calculateHhm()).infoId(R.string.play_stat_hhm_info));
addStatRow(advancedTable, new Builder().labelId(R.string.play_stat_ruhm).value(stats.calculateRuhm()).infoId(R.string.play_stat_ruhm_info));
}
addStatRow(advancedTable, new Builder().labelId(R.string.play_stat_utilization).valueAsPercentage(stats.calculateUtilization()).infoId(R.string.play_stat_utilization_info));
int hIndexOffset = stats.getHIndexOffset();
if (hIndexOffset == -1) {
addStatRow(advancedTable, new Builder().labelId(R.string.play_stat_h_index_offset_in));
} else {
addStatRow(advancedTable, new Builder().labelId(R.string.play_stat_h_index_offset_out).value(hIndexOffset));
}
}
private void showEmpty() {
progressView.hide();
AnimationUtils.fadeOut(dataView);
AnimationUtils.fadeIn(emptyView);
}
private void showData() {
progressView.hide();
AnimationUtils.fadeOut(emptyView);
AnimationUtils.fadeIn(dataView);
}
private void addStatRowMaybe(ViewGroup container, Builder builder) {
if (builder.hasValue()) {
PlayStatView view = builder.build(getActivity());
container.addView(view);
}
}
private void addStatRow(ViewGroup container, Builder builder) {
PlayStatView view = builder.build(getActivity());
container.addView(view);
}
@Override
public void onLoaderReset(Loader<Cursor> loader) {
}
@OnClick(R.id.score_help)
public void onScoreHelpClick() {
AlertDialog.Builder builder = new AlertDialog.Builder(getContext());
builder.setTitle(R.string.title_scores).setView(R.layout.dialog_help_score);
builder.show();
}
@OnClick(R.id.low_score)
public void onLowScoreClick() {
AlertDialog.Builder builder = new AlertDialog.Builder(getContext());
builder.setTitle(R.string.title_low_scorers).setMessage(stats.getLowScorers());
builder.show();
}
@OnClick(R.id.high_score)
public void onHighScoreClick() {
AlertDialog.Builder builder = new AlertDialog.Builder(getContext());
builder.setTitle(R.string.title_high_scorers).setMessage(stats.getHighScorers());
builder.show();
}
private class PlayerStats {
private String username;
private int playCount;
private int wins;
private int winsWithScore;
private int winnableGames;
private int winsTimesPlayers;
private final SparseIntArray winsByPlayerCount = new SparseIntArray();
private final SparseIntArray playsByPlayerCount = new SparseIntArray();
private final SparseIntArray winnablePlaysByPlayerCount = new SparseIntArray();
private double totalScore;
private double winningScore;
private int totalScoreCount;
private double highScore;
private double lowScore;
public PlayerStats() {
username = "";
playCount = 0;
wins = 0;
winsWithScore = 0;
winnableGames = 0;
winsTimesPlayers = 0;
winsByPlayerCount.clear();
totalScore = 0.0;
winningScore = 0.0;
totalScoreCount = 0;
highScore = Integer.MIN_VALUE;
lowScore = Integer.MAX_VALUE;
}
public void add(PlayModel play, PlayerModel player) {
username = player.username;
playCount += play.quantity;
addByPlayerCount(playsByPlayerCount, play.playerCount, play.quantity);
if (play.isWinnable()) {
winnableGames += play.quantity;
addByPlayerCount(winnablePlaysByPlayerCount, play.playerCount, play.quantity);
if (player.win) {
wins += play.quantity;
winsTimesPlayers += play.quantity * play.playerCount;
if (StringUtils.isNumeric(player.score)) winsWithScore += play.quantity;
addByPlayerCount(winsByPlayerCount, play.playerCount, play.quantity);
}
}
if (StringUtils.isNumeric(player.score)) {
final double score = StringUtils.parseDouble(player.score);
totalScore += score * play.quantity;
totalScoreCount += play.quantity;
if (score < lowScore) lowScore = score;
if (score > highScore) highScore = score;
if (play.isWinnable() && player.win) {
winningScore += score * play.quantity;
}
}
}
private void addByPlayerCount(SparseIntArray playerCountMap, int playerCount, int quantity) {
playerCountMap.put(playerCount, playerCountMap.get(playerCount) + quantity);
}
public String getUsername() {
return username;
}
public int getWinCountByPlayerCount(int playerCount) {
return winsByPlayerCount.get(playerCount);
}
public int getWinnablePlayCountByPlayerCount(int playerCount) {
return winnablePlaysByPlayerCount.get(playerCount);
}
public int getPlayCountByPlayerCount(int playerCount) {
return playsByPlayerCount.get(playerCount);
}
public int getWinSkill() {
return (int) (((double) winsTimesPlayers / (double) winnableGames) * 100);
}
public double getAverageScore() {
if (totalScoreCount == 0) return Integer.MIN_VALUE;
return totalScore / totalScoreCount;
}
public double getAverageWinScore() {
if (totalScoreCount == 0) return Integer.MIN_VALUE;
if (winsWithScore == 0) return Integer.MIN_VALUE;
return winningScore / winsWithScore;
}
public double getHighScore() {
return highScore;
}
public double getLowScore() {
return lowScore;
}
}
private class Stats {
private final double lambda = Math.log(0.1) / -10;
private final String currentYear = String.valueOf(Calendar.getInstance().get(Calendar.YEAR));
private final Map<Integer, PlayModel> plays = new LinkedHashMap<>();
private final Map<String, PlayerStats> playerStats = new HashMap<>();
private final double personalRating;
private String firstPlayDate;
private String lastPlayDate;
private String nickelDate;
private String dimeDate;
private String quarterDate;
private String halfDollarDate;
private String dollarDate;
private int playCount;
private int playCountIncomplete;
private int playCountWithLength;
private int playCountThisYear;
private int playerCountSumWithLength;
private Map<Integer, Integer> playCountPerPlayerCount;
private int realMinutesPlayed;
private int estimatedMinutesPlayed;
private int numberOfWinnableGames;
private double scoreSum;
private int scoreCount;
private double highScore;
private double lowScore;
private int winningScoreCount;
private double winningScoreSum;
private Map<String, Integer> playCountByLocation;
private final Set<String> monthsPlayed = new HashSet<>();
public Stats(Cursor cursor, double personalRating) {
init();
this.personalRating = personalRating;
do {
PlayModel model = new PlayModel(cursor);
plays.put(model.playId, model);
} while (cursor.moveToNext());
}
private void init() {
plays.clear();
// dates
firstPlayDate = null;
lastPlayDate = null;
nickelDate = null;
dimeDate = null;
quarterDate = null;
halfDollarDate = null;
dollarDate = null;
monthsPlayed.clear();
playCount = 0;
playCountIncomplete = 0;
playCountWithLength = 0;
playCountThisYear = 0;
playerCountSumWithLength = 0;
playCountPerPlayerCount = new ArrayMap<>();
playCountByLocation = new HashMap<>();
numberOfWinnableGames = 0;
realMinutesPlayed = 0;
estimatedMinutesPlayed = 0;
scoreSum = 0;
scoreCount = 0;
highScore = Integer.MIN_VALUE;
lowScore = Integer.MAX_VALUE;
winningScoreCount = 0;
winningScoreSum = 0;
}
public void calculate() {
boolean includeIncomplete = PreferencesUtils.logPlayStatsIncomplete(getActivity());
for (PlayModel play : plays.values()) {
if (!includeIncomplete && play.incomplete) {
playCountIncomplete += play.quantity;
continue;
}
if (firstPlayDate == null) {
firstPlayDate = play.date;
}
lastPlayDate = play.date;
if (playCount < 5 && (playCount + play.quantity) >= 5) {
nickelDate = play.date;
}
if (playCount < 10 && (playCount + play.quantity) >= 10) {
dimeDate = play.date;
}
if (playCount < 25 && (playCount + play.quantity) >= 25) {
quarterDate = play.date;
}
if (playCount < 50 && (playCount + play.quantity) >= 50) {
halfDollarDate = play.date;
}
if (playCount < 100 && (playCount + play.quantity) >= 100) {
dollarDate = play.date;
}
playCount += play.quantity;
if (play.getYear().equals(currentYear)) {
playCountThisYear += play.quantity;
}
if (play.length == 0) {
estimatedMinutesPlayed += playingTime * play.quantity;
} else {
realMinutesPlayed += play.length;
playCountWithLength += play.quantity;
playerCountSumWithLength += play.playerCount * play.quantity;
}
if (play.playerCount > 0) {
int previousQuantity = 0;
if (playCountPerPlayerCount.containsKey(play.playerCount)) {
previousQuantity = playCountPerPlayerCount.get(play.playerCount);
}
playCountPerPlayerCount.put(play.playerCount, previousQuantity + play.quantity);
}
if (play.isWinnable()) {
numberOfWinnableGames += play.quantity;
}
if (!TextUtils.isEmpty(play.location)) {
int previousPlays = 0;
if (playCountByLocation.containsKey(play.location)) {
previousPlays = playCountByLocation.get(play.location);
}
playCountByLocation.put(play.location, previousPlays + play.quantity);
}
for (PlayerModel player : play.getPlayers()) {
if (!TextUtils.isEmpty(player.getUniqueName())) {
PlayerStats playerStats = this.playerStats.get(player.getUniqueName());
if (playerStats == null) {
playerStats = new PlayerStats();
}
playerStats.add(play, player);
this.playerStats.put(player.getUniqueName(), playerStats);
}
if (StringUtils.isNumeric(player.score)) {
double score = StringUtils.parseDouble(player.score);
scoreCount += play.quantity;
scoreSum += score * play.quantity;
if (player.win) {
winningScoreCount += play.quantity;
winningScoreSum += score * play.quantity;
}
if (score > highScore) highScore = score;
if (score < lowScore) lowScore = score;
}
}
monthsPlayed.add(play.getYearAndMonth());
}
}
public void addPlayerData(Cursor cursor) {
do {
PlayerModel playerModel = new PlayerModel(cursor);
if (plays.containsKey(playerModel.playId)) {
plays.get(playerModel.playId).addPlayer(playerModel);
} else {
Timber.w("Play %s not found in the play map!", playerModel.playId);
}
} while (cursor.moveToNext());
}
public int getPlayCount() {
return playCount;
}
public int getPlayCountIncomplete() {
return playCountIncomplete;
}
public String getFirstPlayDate() {
return firstPlayDate;
}
private String getNickelDate() {
return nickelDate;
}
private String getDimeDate() {
return dimeDate;
}
private String getQuarterDate() {
return quarterDate;
}
private String getHalfDollarDate() {
return halfDollarDate;
}
private String getDollarDate() {
return dollarDate;
}
public String getLastPlayDate() {
if (playCount > 0) {
return lastPlayDate;
}
return null;
}
public double getHoursPlayed() {
return (realMinutesPlayed + estimatedMinutesPlayed) / 60;
}
/* plays per month, only counting the active period) */
public double getPlayRate() {
long flash = calculateFlash();
if (flash > 0) {
double rate = ((double) (playCount * 365) / flash) / 12;
return Math.min(rate, playCount);
}
return 0;
}
public int getAveragePlayTime() {
if (playCountWithLength > 0) {
return realMinutesPlayed / playCountWithLength;
}
return 0;
}
public int getAveragePlayTimePerPlayer() {
if (playerCountSumWithLength > 0) {
return realMinutesPlayed / playerCountSumWithLength;
}
return 0;
}
public int getMonthsPlayed() {
return monthsPlayed.size();
}
public int getMinPlayerCount() {
int min = Integer.MAX_VALUE;
for (Integer playerCount : playCountPerPlayerCount.keySet()) {
if (playerCount < min) {
min = playerCount;
}
}
return min;
}
public int getMaxPlayerCount() {
int max = 0;
for (Integer playerCount : playCountPerPlayerCount.keySet()) {
if (playerCount > max) {
max = playerCount;
}
}
return max;
}
public int getWinCount(int playerCount) {
PlayerStats ps = getPersonalStats();
if (ps != null) {
return ps.getWinCountByPlayerCount(playerCount);
}
return 0;
}
public int getWinnablePlayCount(int playerCount) {
PlayerStats ps = getPersonalStats();
if (ps != null) {
return ps.getWinnablePlayCountByPlayerCount(playerCount);
}
return 0;
}
public int getPlayCount(int playerCount) {
PlayerStats ps = getPersonalStats();
if (ps != null) {
return ps.getPlayCountByPlayerCount(playerCount);
}
return 0;
}
private PlayerStats getPersonalStats() {
String username = AccountUtils.getUsername(getActivity());
for (Entry<String, PlayerStats> ps : stats.getPlayerStats()) {
if (username != null && username.equalsIgnoreCase(ps.getValue().getUsername())) {
return ps.getValue();
}
}
return null;
}
public boolean hasScores() {
return scoreCount > 0;
}
public double getAverageScore() {
return scoreSum / scoreCount;
}
public double getHighScore() {
return highScore;
}
public String getHighScorers() {
if (highScore == Integer.MIN_VALUE) return "";
List<String> players = new ArrayList<>();
for (Entry<String, PlayerStats> ps : playerStats.entrySet()) {
if (ps.getValue().highScore == highScore) {
players.add(ps.getKey());
}
}
return StringUtils.formatList(players);
}
public double getLowScore() {
return lowScore;
}
public String getLowScorers() {
if (lowScore == Integer.MAX_VALUE) return "";
List<String> players = new ArrayList<>();
for (Entry<String, PlayerStats> ps : playerStats.entrySet()) {
if (ps.getValue().lowScore == lowScore) {
players.add(ps.getKey());
}
}
return StringUtils.formatList(players);
}
public double getAverageWinningScore() {
return winningScoreSum / winningScoreCount;
}
public List<Entry<String, PlayerStats>> getPlayerStats() {
Set<Entry<String, PlayerStats>> set = playerStats.entrySet();
List<Entry<String, PlayerStats>> list = new ArrayList(set);
Collections.sort(list, new Comparator<Entry<String, PlayerStats>>() {
@Override
public int compare(Entry<String, PlayerStats> lhs, Entry<String, PlayerStats> rhs) {
if (lhs.getValue().playCount > rhs.getValue().playCount) {
return -1;
} else if (lhs.getValue().playCount < rhs.getValue().playCount) {
return 1;
} else {
return lhs.getKey().compareTo(rhs.getKey());
}
}
});
return list;
}
public List<Entry<String, Integer>> getPlaysPerLocation() {
Set<Entry<String, Integer>> set = playCountByLocation.entrySet();
List<Entry<String, Integer>> list = new ArrayList(set);
Collections.sort(list, new Comparator<Entry<String, Integer>>() {
@Override
public int compare(Entry<String, Integer> lhs, Entry<String, Integer> rhs) {
if (lhs.getValue() > rhs.getValue()) {
return -1;
} else if (lhs.getValue() < rhs.getValue()) {
return 1;
} else {
return lhs.getKey().compareTo(rhs.getKey());
}
}
});
return list;
}
public double calculateUtilization() {
return 1 - Math.exp(-lambda * playCount);
}
public int calculateFhm() {
return (int) ((personalRating * 5) + playCount + (4 * getMonthsPlayed()) + getHoursPlayed());
}
public int calculateHhm() {
return (int) ((personalRating - 5) * getHoursPlayed());
}
public double calculateRuhm() {
double raw = (((double) calculateFlash()) / calculateLag()) * getMonthsPlayed() * personalRating;
if (raw == 0) {
return 0;
}
return Math.log(raw);
}
public int getHIndexOffset() {
int hIndex = PreferencesUtils.getHIndex(getActivity());
if (playCount >= hIndex) {
return -1;
} else {
return hIndex - playCount + 1;
}
}
// public int getMonthsPerPlay() {
// long days = calculateSpan();
// int months = (int) (days / 365.25 * 12);
// return months / playCount;
// }
public double calculateGrayHotness(int intervalPlayCount) {
// http://matthew.gray.org/2005/10/games_16.html
double S = 1 + (intervalPlayCount / playCount);
// TODO: need to get HHM for the interval _only_
return S * S * Math.sqrt(intervalPlayCount) * calculateHhm();
}
public int calculateWhitemoreScore() {
// http://www.boardgamegeek.com/geeklist/37832/my-favorite-designers
int score = (int) (personalRating * 2 - 13);
if (score < 0) {
return 0;
}
return score;
}
public double calculateZefquaaviusScore() {
// http://boardgamegeek.com/user/zefquaavius
double neutralRating = 5.5;
double abs = (personalRating - neutralRating);
double squared = abs * abs;
if (personalRating < neutralRating) {
squared *= -1;
}
return squared / 2.025;
}
public double calculateZefquaaviusHotness(int intervalPlayCount) {
return calculateGrayHotness(intervalPlayCount) * calculateZefquaaviusScore();
}
private long calculateFlash() {
return daysBetweenDates(firstPlayDate, lastPlayDate);
}
private long calculateLag() {
return daysBetweenDates(lastPlayDate, null);
}
private long calculateSpan() {
return daysBetweenDates(firstPlayDate, null);
}
private long daysBetweenDates(String first, String second) {
try {
long f = System.currentTimeMillis();
long s = System.currentTimeMillis();
if (!TextUtils.isEmpty(first)) {
f = FORMAT.parse(first).getTime();
}
if (!TextUtils.isEmpty(second)) {
s = FORMAT.parse(second).getTime();
}
long days = TimeUnit.DAYS.convert(s - f, TimeUnit.MILLISECONDS);
if (days < 1) {
return 1;
}
return days;
} catch (ParseException e) {
return 1;
}
}
}
private class PlayModel {
final int playId;
final String date;
final int length;
final int quantity;
final boolean incomplete;
final int playerCount;
final boolean noWinStats;
final String location;
final long deleteTimestamp;
final long updateTimestamp;
final List<PlayerModel> players = new ArrayList<>();
PlayModel(Cursor cursor) {
playId = cursor.getInt(PlayQuery.PLAY_ID);
date = cursor.getString(PlayQuery.DATE);
length = cursor.getInt(PlayQuery.LENGTH);
quantity = cursor.getInt(PlayQuery.QUANTITY);
incomplete = CursorUtils.getBoolean(cursor, PlayQuery.INCOMPLETE);
playerCount = cursor.getInt(PlayQuery.PLAYER_COUNT);
noWinStats = CursorUtils.getBoolean(cursor, PlayQuery.NO_WIN_STATS);
location = cursor.getString(PlayQuery.LOCATION);
deleteTimestamp = cursor.getLong(PlayQuery.DELETE_TIMESTAMP);
updateTimestamp = cursor.getLong(PlayQuery.UPDATE_TIMESTAMP);
players.clear();
}
public List<PlayerModel> getPlayers() {
return players;
}
public String getYear() {
return date.substring(0, 4);
}
public String getYearAndMonth() {
return date.substring(0, 7);
}
public void addPlayer(PlayerModel player) {
players.add(player);
}
public boolean isWinnable() {
if (noWinStats) {
return false;
}
if (players == null || players.isEmpty()) {
return false;
}
if (updateTimestamp > 0) {
return true;
}
if (playId > 0 && deleteTimestamp == 0) {
return true;
}
return false;
}
}
private class PlayerModel {
final int playId;
final String username;
final String name;
final boolean win;
final String score;
PlayerModel(Cursor cursor) {
playId = cursor.getInt(PlayerQuery.PLAY_ID);
username = cursor.getString(PlayerQuery.USER_NAME);
name = cursor.getString(PlayerQuery.NAME);
win = CursorUtils.getBoolean(cursor, PlayerQuery.WIN);
score = cursor.getString(PlayerQuery.SCORE);
}
public String getUniqueName() {
if (TextUtils.isEmpty(username)) {
return name;
}
return name + " (" + username + ")";
}
}
private interface PlayQuery {
int _TOKEN = 0x01;
String[] PROJECTION = { Plays._ID, Plays.PLAY_ID, Plays.DATE, Plays.ITEM_NAME, Plays.OBJECT_ID,
Plays.LOCATION, Plays.QUANTITY, Plays.LENGTH, Plays.PLAYER_COUNT, Games.THUMBNAIL_URL,
Plays.INCOMPLETE, Plays.NO_WIN_STATS, Plays.DELETE_TIMESTAMP, Plays.UPDATE_TIMESTAMP };
int PLAY_ID = 1;
int DATE = 2;
int LOCATION = 5;
int QUANTITY = 6;
int LENGTH = 7;
int PLAYER_COUNT = 8;
int INCOMPLETE = 10;
int NO_WIN_STATS = 11;
int DELETE_TIMESTAMP = 12;
int UPDATE_TIMESTAMP = 13;
}
private interface PlayerQuery {
int _TOKEN = 0x03;
String[] PROJECTION = { PlayPlayers._ID, PlayPlayers.PLAY_ID, PlayPlayers.USER_NAME, PlayPlayers.WIN,
PlayPlayers.SCORE, PlayPlayers.NAME };
int PLAY_ID = 1;
int USER_NAME = 2;
int WIN = 3;
int SCORE = 4;
int NAME = 5;
}
private interface GameQuery {
int _TOKEN = 0x02;
String[] PROJECTION = { Games._ID, Collection.RATING, Games.PLAYING_TIME };
int RATING = 1;
int PLAYING_TIME = 2;
}
}