package com.pluscubed.plustimer.ui.historysessions; import android.content.Context; import android.content.Intent; import android.graphics.Color; import android.support.annotation.NonNull; import android.util.SparseArray; import com.github.mikephil.charting.data.Entry; import com.github.mikephil.charting.data.LineData; import com.github.mikephil.charting.data.LineDataSet; import com.github.mikephil.charting.interfaces.datasets.ILineDataSet; import com.pluscubed.plustimer.R; import com.pluscubed.plustimer.base.Presenter; import com.pluscubed.plustimer.base.PresenterFactory; import com.pluscubed.plustimer.model.PuzzleType; import com.pluscubed.plustimer.model.Session; import com.pluscubed.plustimer.model.Solve; import com.pluscubed.plustimer.ui.historysolvelist.HistorySolveListActivity; import com.pluscubed.plustimer.utils.PrefUtils; import com.pluscubed.plustimer.utils.Utils; import java.util.ArrayList; import java.util.Collections; import java.util.List; import rx.Completable; import rx.Observable; import rx.Single; import rx.android.schedulers.AndroidSchedulers; import rx.schedulers.Schedulers; public class HistorySessionsPresenter extends Presenter<HistorySessionsView> { private String mPuzzleTypeId; private boolean mMillisecondsEnabled; public HistorySessionsPresenter() { } @Override public void onViewAttached(HistorySessionsView view) { super.onViewAttached(view); mPuzzleTypeId = PuzzleType.getCurrentId(view.getContextCompat()); updateSessionsAdapter(); } @SuppressWarnings("ConstantConditions") public void onSessionClicked(Session session) { if (!isViewAttached()) { return; } Intent i = new Intent(getView().getContextCompat(), HistorySolveListActivity.class); i.putExtra(HistorySolveListActivity.EXTRA_HISTORY_SESSION_ID, session.getId()); i.putExtra(HistorySolveListActivity.EXTRA_HISTORY_PUZZLETYPE_ID, mPuzzleTypeId); getView().getContextCompat().startActivity(i); } @SuppressWarnings("ConstantConditions") public void updateSessionsAdapter() { if (!isViewAttached()) { return; } getView().getHistorySessionsAdapter().setMillisecondsEnabled(mMillisecondsEnabled); PuzzleType.get(getView().getContextCompat(), mPuzzleTypeId) .flatMapObservable(puzzleType -> puzzleType.getHistorySessionsSorted(getView().getContextCompat())) .toList() .observeOn(AndroidSchedulers.mainThread()) .doOnNext(sessions -> { getView().getHistorySessionsAdapter().setSessions(sessions); getView().getHistorySessionsAdapter().notifyDataSetChanged(); getView().showList(!sessions.isEmpty()); }) .flatMap(sessions -> updateGraph(getView().getContextCompat(), sessions) .mergeWith(updateStatsText(getView().getContextCompat(), sessions)) .andThen(Observable.just(sessions)) ) .observeOn(AndroidSchedulers.mainThread()) .subscribe(historySessions -> { getView().getHistorySessionsAdapter().notifyHeaderChanged(); }); } private Completable updateGraph(Context context, List<Session> historySessions) { return Single.<LineData>create(singleSubscriber -> { mMillisecondsEnabled = PrefUtils.isDisplayMillisecondsEnabled(context); if (historySessions.isEmpty()) { singleSubscriber.onSuccess(null); return; } List<Integer> sessionSecondsTimestamps = new ArrayList<>(); for (Session session : historySessions) { long timestamp = session.getTimestamp(context).toBlocking().value(); sessionSecondsTimestamps.add((int) (timestamp / 1000)); } long firstTimestamp = sessionSecondsTimestamps.get(0); long lastTimestamp = sessionSecondsTimestamps.get(sessionSecondsTimestamps.size() - 1); double firstViewportTimestamp = firstTimestamp - (lastTimestamp - firstTimestamp) * 0.1; double lastViewportTimestamp = lastTimestamp + (lastTimestamp - firstTimestamp) * 0.1; int firstViewportSecondsTimestamp = (int) (firstViewportTimestamp); int lastViewportSecondsTimestamp = (int) (lastViewportTimestamp); SparseArray<SparseArray<Long>> bestAveragePerSessionPerNumber = getBestAveragePerSessionPerNumber(context, historySessions); List<ILineDataSet> graphData = new ArrayList<>(); for (int numberIndex = 0; numberIndex < bestAveragePerSessionPerNumber.size(); numberIndex++) { addAverageDataSet(context, sessionSecondsTimestamps, firstViewportSecondsTimestamp, bestAveragePerSessionPerNumber, graphData, numberIndex); } SparseArray<Long> bestSolvesTimes = addBestTimesDataSet(context, historySessions, sessionSecondsTimestamps, firstViewportSecondsTimestamp, graphData); boolean averageMoreThanOne = false; for (int i = 0; i < bestAveragePerSessionPerNumber.size(); i++) { if (bestAveragePerSessionPerNumber.valueAt(i).size() > 1) { averageMoreThanOne = true; } } if (averageMoreThanOne || bestSolvesTimes.size() > 1) { List<String> xVals = new ArrayList<>(); //ADD FILLER TIMESTAMPS xVals.add(Utils.dateTimeStringFromTimestamp(context, firstViewportSecondsTimestamp * 1000L)); for (int i = firstViewportSecondsTimestamp + 1; i < lastViewportSecondsTimestamp; i++) { xVals.add(""); } xVals.add(Utils.dateTimeStringFromTimestamp(context, lastViewportSecondsTimestamp * 1000L)); /*int lastAdded = 0; for (int i = 0; i < sessionSecondsTimestamps.size(); i++) { int timestamp = sessionSecondsTimestamps.get(i); int previousTimestamp; if (i == 0) { previousTimestamp = firstViewportSecondsTimestamp; } else { previousTimestamp = sessionSecondsTimestamps.get(i - 1); } for(int j=previousTimestamp;j<timestamp;j++){ xVals.add(""); } String timestampString = Utils.dateTimeSecondsStringFromTimestamp(context, timestamp * 1000L); xVals.add(timestampString); lastAdded = timestamp; } for(int i=lastAdded;i<lastViewportSecondsTimestamp; i++){ xVals.add(""); }*/ LineData data = new LineData(xVals, graphData); singleSubscriber.onSuccess(data); } else { singleSubscriber.onSuccess(null); } }).subscribeOn(Schedulers.io()) .observeOn(AndroidSchedulers.mainThread()) .doOnSuccess(lineData -> { if (isViewAttached()) { //noinspection ConstantConditions getView().getHistorySessionsAdapter().setLineData(lineData); } }).toObservable().toCompletable(); } @NonNull private SparseArray<Long> addBestTimesDataSet(Context context, List<Session> historySessions, List<Integer> sessionSecondsTimestamps, int firstViewportSecondsTimestamp, List<ILineDataSet> graphData) { //Get best times of each session excluding DNF, // and create GraphViewData array bestTimes SparseArray<Long> bestSolvesTimes = new SparseArray<>(); for (int i = 0; i < historySessions.size(); i++) { Session session = historySessions.get(i); List<Solve> solveList = session.getSolves(context).toList().toBlocking().first(); Solve bestSolveOfList = Utils.getBestSolveOfList(solveList); if (bestSolveOfList.getPenalty() != Solve.PENALTY_DNF) { //noinspection ConstantConditions bestSolvesTimes .put(i, bestSolveOfList.getTimeTwo()); } } List<Entry> dataSetEntries = new ArrayList<>(); for (int i = 0; i < bestSolvesTimes.size(); i++) { int secondsTimestamp = sessionSecondsTimestamps.get(bestSolvesTimes.keyAt(i)); Entry entry = new Entry( bestSolvesTimes.valueAt(i), secondsTimestamp - firstViewportSecondsTimestamp ); dataSetEntries.add(entry); } LineDataSet bestSolvesDataSet = new LineDataSet( dataSetEntries, context.getString(R.string.best_times) ); bestSolvesDataSet.setDrawCircles(true); bestSolvesDataSet.setCircleColor(Color.BLUE); bestSolvesDataSet.setColor(Color.BLUE); bestSolvesDataSet.setLineWidth(2f); bestSolvesDataSet.setDrawValues(false); graphData.add(bestSolvesDataSet); return bestSolvesTimes; } private void addAverageDataSet(Context context, List<Integer> sessionSecondsTimestamps, int firstViewportSecondsTimestamp, SparseArray<SparseArray<Long>> bestAveragePerSessionPerNumber, List<ILineDataSet> graphData, int numberIndex) { SparseArray<Long> bestAveragePerSession = bestAveragePerSessionPerNumber.valueAt(numberIndex); if (bestAveragePerSession.size() == 0) { return; } List<Entry> dataSetEntries = new ArrayList<>(); for (int sessionIndex = 0; sessionIndex < bestAveragePerSession.size(); sessionIndex++) { int secondsTimestamp = sessionSecondsTimestamps.get(bestAveragePerSession.keyAt(sessionIndex)); Entry entry = new Entry(bestAveragePerSession.valueAt(sessionIndex), secondsTimestamp - firstViewportSecondsTimestamp); dataSetEntries.add(entry); } int lineColor = Color.RED; switch (bestAveragePerSessionPerNumber.keyAt(numberIndex)) { case 5: lineColor = Color.RED; break; case 12: lineColor = Color.GREEN; break; case 50: lineColor = Color.MAGENTA; break; case 100: lineColor = Color.BLACK; break; case 1000: lineColor = Color.YELLOW; } LineDataSet averageDataSet = new LineDataSet( dataSetEntries, String.format(context.getString(R.string.bao), bestAveragePerSessionPerNumber.keyAt(numberIndex)) ); averageDataSet.setDrawCircles(true); averageDataSet.setCircleColor(lineColor); averageDataSet.setLineWidth(2f); averageDataSet.setColor(lineColor); averageDataSet.setDrawValues(false); graphData.add(averageDataSet); } @NonNull private SparseArray<SparseArray<Long>> getBestAveragePerSessionPerNumber(Context context, List<Session> historySessions) { //This SparseArray contains any number of SparseArray<Long>, // one for each average (5,12,etc) // e.g. 5:{0 : 23.402, 1 : 21.206, 2:...}, 12:{...}... SparseArray<SparseArray<Long>> bestAveragePerSessionPerNumber = new SparseArray<>(); for (int averageNumber : new int[]{5, 12, 50, 100, 1000}) { SparseArray<Long> timesSparseArray = new SparseArray<>(); for (int i = 0; i < historySessions.size(); i++) { Session session = historySessions.get(i); if (session.getNumberOfSolves() >= averageNumber) { List<Solve> list = session.getSolves(context).toList().toBlocking().first(); long bestAverage = session.getBestAverageOf(list, averageNumber).toBlocking().value(); if (bestAverage != Long.MAX_VALUE && bestAverage != Session.GET_AVERAGE_INVALID_NOT_ENOUGH) { timesSparseArray.put(i, bestAverage); } } } if (timesSparseArray.size() > 0) { bestAveragePerSessionPerNumber.put(averageNumber, timesSparseArray); } } return bestAveragePerSessionPerNumber; } private Completable updateStatsText(Context context, List<Session> historySessions) { return Single.<String>create(singleSubscriber -> { if (historySessions.isEmpty()) { singleSubscriber.onSuccess(null); } StringBuilder s = new StringBuilder(); //Get best solves of each history session and add to list ArrayList<Solve> bestSolvesOfSessionsArray = new ArrayList<>(); for (Session session : historySessions) { List<Solve> sessionSolveList = session.getSortedSolves(context).toList().toBlocking().first(); Solve bestSolveOfList = Utils.getBestSolveOfList(sessionSolveList); bestSolvesOfSessionsArray.add(bestSolveOfList); } //Add PB of all historySessions //noinspection ConstantConditions String bestTimeString = Utils.getBestSolveOfList(bestSolvesOfSessionsArray) .getTimeString(mMillisecondsEnabled); s.append(String.format(context.getString(R.string.pb), bestTimeString)); //Add PB of Ao5,12,50,100,1000 s.append(getBestAverageOfNumberOfSessions(context, new int[]{1000, 100, 50, 12, 5}, historySessions)); singleSubscriber.onSuccess(s.toString()); }).subscribeOn(Schedulers.io()) .observeOn(AndroidSchedulers.mainThread()) .doOnSuccess(s -> { if (isViewAttached()) //noinspection ConstantConditions getView().getHistorySessionsAdapter().setStats(s); }).toObservable().toCompletable(); } /** * Returns string with best averages of [numbers]. * * @param numbers the numbers for the averages * @param sessions list of sessions * @return String with the best averages of [numbers] */ private String getBestAverageOfNumberOfSessions(Context context, int[] numbers, List<Session> sessions) { StringBuilder builder = new StringBuilder(); for (int number : numbers) { ArrayList<Long> bestAverages = new ArrayList<>(); if (sessions.size() > 0) { for (Session session : sessions) { List<Solve> list = session.getSolves(context).toList().toBlocking().first(); long bestAverage = session.getBestAverageOf(list, number).toBlocking().value(); if (bestAverage != Session.GET_AVERAGE_INVALID_NOT_ENOUGH) { //If the average is possible for the number bestAverages.add(bestAverage); } } if (bestAverages.size() > 0) { Long bestAverage = Collections.min(bestAverages); String finalPbAo = bestAverage == Long.MAX_VALUE ? "DNF" : Utils.timeStringFromNs(bestAverage, mMillisecondsEnabled); String pbAo = String.format(context.getString(R.string.pb_ao), number, finalPbAo); builder.append("\n") .append(pbAo); } } } return builder.toString(); } @SuppressWarnings("ConstantConditions") public void onPuzzleSelected(PuzzleType puzzleType) { if (!isViewAttached()) { return; } if (!mPuzzleTypeId.equals(puzzleType.getId())) { mPuzzleTypeId = puzzleType.getId(); updateSessionsAdapter(); } } @SuppressWarnings("ConstantConditions") public void onCreateOptionsMenu() { if (!isViewAttached()) { return; } PuzzleType.getEnabledPuzzleTypes(getView().getContextCompat()) .toList() .observeOn(AndroidSchedulers.mainThread()) .subscribe(list -> { String id = PuzzleType.getCurrentId(getView().getContextCompat()); int position = 0; for (int i = 0; i < list.size(); i++) { PuzzleType type = list.get(i); if (type.getId().equals(id)) { position = i; break; } } getView().initPuzzleSpinner(list, position); }); } public static class Factory implements PresenterFactory<HistorySessionsPresenter> { public Factory() { } @Override public HistorySessionsPresenter create() { return new HistorySessionsPresenter(); } } }