Refactoring to separate calculus UI uses from the backend

kmm
Laurent 5 years ago
parent 5b1bb4a631
commit 89526e63ba
  1. 84
      app/src/main/java/net/pokeranalytics/android/calcul/ComputedResultsExtensions.kt
  2. 64
      app/src/main/java/net/pokeranalytics/android/calcul/ReportExtensions.kt
  3. 7
      app/src/main/java/net/pokeranalytics/android/calculus/AggregationType.kt
  4. 90
      app/src/main/java/net/pokeranalytics/android/calculus/ComputableGroup.kt
  5. 227
      app/src/main/java/net/pokeranalytics/android/calculus/Report.kt
  6. 4
      app/src/main/java/net/pokeranalytics/android/ui/fragment/report/ProgressReportFragment.kt
  7. 7
      app/src/main/java/net/pokeranalytics/android/ui/graph/GraphExtensions.kt
  8. 1
      app/src/main/java/net/pokeranalytics/android/ui/modules/calendar/CalendarDetailsFragment.kt
  9. 2
      app/src/main/java/net/pokeranalytics/android/ui/view/RowViewType.kt
  10. 2
      app/src/main/java/net/pokeranalytics/android/ui/viewmodel/GraphViewModel.kt
  11. 3
      app/src/main/java/net/pokeranalytics/android/ui/viewmodel/ReportHolder.kt
  12. 5
      app/src/main/java/net/pokeranalytics/android/ui/viewmodel/ReportViewModel.kt

@ -0,0 +1,84 @@
package net.pokeranalytics.android.calcul
import android.content.Context
import com.github.mikephil.charting.data.*
import net.pokeranalytics.android.R
import net.pokeranalytics.android.calculus.ComputedResults
import net.pokeranalytics.android.calculus.Point
import net.pokeranalytics.android.calculus.Stat
import net.pokeranalytics.android.ui.graph.DataSetFactory
import kotlin.math.abs
// MPAndroidChart
fun ComputedResults.defaultStatEntries(stat: Stat, context: Context): DataSet<out Entry> {
return when (stat) {
Stat.NUMBER_OF_SETS, Stat.NUMBER_OF_GAMES, Stat.HOURLY_DURATION -> this.barEntries(stat, context = context)
Stat.STANDARD_DEVIATION -> this.distributionEntries(stat, context)
else -> this.singleLineEntries(stat, context)
}
}
fun ComputedResults.singleLineEntries(stat: Stat, context: Context): LineDataSet {
val entries = mutableListOf<Entry>()
this.evolutionValues[stat]?.let { points ->
points.forEachIndexed { index, p ->
entries.add(Entry(index.toFloat(), p.y.toFloat(), p.data))
}
}
return DataSetFactory.lineDataSetInstance(entries, this.group.query.getName(context), context)
}
fun ComputedResults.durationEntries(stat: Stat, context: Context): LineDataSet {
val entries = mutableListOf<Entry>()
this.evolutionValues[stat]?.let { points ->
points.forEach { p ->
entries.add(Entry(p.x.toFloat(), p.y.toFloat(), p.data))
}
}
return DataSetFactory.lineDataSetInstance(entries, stat.name, context)
}
private fun ComputedResults.barEntries(stat: Stat, context: Context): BarDataSet {
val entries = mutableListOf<BarEntry>()
this.evolutionValues[stat]?.let { points ->
points.forEach { p ->
entries.add(BarEntry(p.x.toFloat(), p.y.toFloat(), p.data))
}
}
return DataSetFactory.barDataSetInstance(entries, stat.name, context)
}
private fun ComputedResults.distributionEntries(stat: Stat, context: Context): BarDataSet {
val colors = mutableListOf<Int>()
val entries = mutableListOf<BarEntry>()
this.evolutionValues[stat]?.let { points ->
val negative = mutableListOf<Point>()
val positive = mutableListOf<Point>()
points.sortByDescending { it.y }
points.forEach {
if (it.y < 0) {
negative.add(it)
} else {
positive.add(it)
}
}
negative.forEachIndexed { index, p ->
entries.add(BarEntry(index.toFloat(), abs(p.y.toFloat()), p.data))
colors.add(context.getColor(R.color.red))
}
positive.forEachIndexed { index, p ->
val x = negative.size + index.toFloat()
entries.add(BarEntry(x, p.y.toFloat(), p.data))
colors.add(context.getColor(R.color.green))
}
}
return DataSetFactory.barDataSetInstance(entries, stat.name, context, colors)
}

@ -0,0 +1,64 @@
package net.pokeranalytics.android.calcul
import android.content.Context
import com.github.mikephil.charting.data.BarDataSet
import com.github.mikephil.charting.data.BarEntry
import com.github.mikephil.charting.data.Entry
import com.github.mikephil.charting.data.LineDataSet
import net.pokeranalytics.android.calculus.Report
import net.pokeranalytics.android.calculus.Stat
import net.pokeranalytics.android.ui.graph.DataSetFactory
import net.pokeranalytics.android.util.ColorUtils
/**
* Returns the list of entries corresponding to the provided [stat]
* One value will be returned by result
*/
fun Report.lineEntries(stat: Stat? = null, context: Context): LineDataSet {
val entries = mutableListOf<Entry>()
val statToUse = stat ?: this.options.stats.firstOrNull()
val statName = statToUse?.name ?: ""
statToUse?.let {
this.results.forEachIndexed { index, results ->
results.computedStat(it)?.progressValue?.let { progressValue ->
entries.add(Entry(index.toFloat(), progressValue.toFloat(), results))
}
}
}
return DataSetFactory.lineDataSetInstance(entries, statName, context)
}
fun Report.barEntries(stat: Stat? = null, context: Context): BarDataSet {
val entries = mutableListOf<BarEntry>()
val statToUse = stat ?: options.stats.firstOrNull()
statToUse?.let {
this.results.forEachIndexed { index, results ->
val cs = results.computedStat(it)
cs?.let { computedStat ->
val barEntry = BarEntry(index.toFloat(), computedStat.value.toFloat(), results)
entries.add(barEntry)
}
}
}
val label = statToUse?.name ?: ""
return DataSetFactory.barDataSetInstance(entries, label, context)
}
fun Report.multiLineEntries(context: Context): List<LineDataSet> {
val dataSets = mutableListOf<LineDataSet>()
options.stats.forEach { stat ->
this.results.forEachIndexed { index, result ->
val ds = result.singleLineEntries(stat, context)
ds.color = ColorUtils.almostRandomColor(index, context)
dataSets.add(ds)
}
}
return dataSets
}

@ -2,7 +2,12 @@ package net.pokeranalytics.android.calculus
import net.pokeranalytics.android.R
import net.pokeranalytics.android.model.Criteria
import net.pokeranalytics.android.ui.graph.AxisFormatting
enum class AxisFormatting {
DEFAULT,
X_DURATION,
Y_DURATION,
}
enum class AggregationType {
SESSION,

@ -0,0 +1,90 @@
package net.pokeranalytics.android.calculus
import io.realm.Realm
import io.realm.RealmResults
import net.pokeranalytics.android.model.filter.Query
import net.pokeranalytics.android.model.filter.QueryCondition
import net.pokeranalytics.android.model.realm.ComputableResult
import net.pokeranalytics.android.model.realm.Filter
import net.pokeranalytics.android.model.realm.SessionSet
/**
* A sessionGroup of computable items identified by a name
*/
class ComputableGroup(var query: Query, var stats: List<Stat>? = null) {
/**
* A subgroup used to compute stat variation
*/
var comparedGroup: ComputableGroup? = null
/**
* The computed statIds of the comparable sessionGroup
*/
var comparedComputedResults: ComputedResults? = null
/**
* A list of _conditions to get
*/
val conditions: List<QueryCondition>
get() {
return this.query.conditions
}
/**
* The list of endedSessions to compute
*/
private var _computables: RealmResults<ComputableResult>? = null
/**
* Retrieves the computables on the relative [realm] filtered with the provided [conditions]
*/
fun computables(realm: Realm, sorted: Boolean = false): RealmResults<ComputableResult> {
// if computables exists and is valid (previous realm not closed)
this._computables?.let {
if (it.isValid) {
return it
}
}
val sortedField = if (sorted) "session.startDate" else null
val computables = Filter.queryOn<ComputableResult>(realm, this.query, sortedField)
this._computables = computables
return computables
}
/**
* The list of sets to compute
*/
private var _sessionSets: RealmResults<SessionSet>? = null
/**
* Retrieves the session sets on the relative [realm] filtered with the provided [conditions]
*/
fun sessionSets(realm: Realm, sorted: Boolean = false): RealmResults<SessionSet> {
// if computables exists and is valid (previous realm not closed)
this._sessionSets?.let {
if (it.isValid) {
return it
}
}
val sortedField = if (sorted) SessionSet.Field.START_DATE.identifier else null
val sets = Filter.queryOn<SessionSet>(realm, this.query, sortedField)
this._sessionSets = sets
return sets
}
fun cleanup() {
this._computables = null
this._sessionSets = null
}
val isEmpty: Boolean
get() {
return this._computables?.isEmpty() ?: true
}
}

@ -1,25 +1,14 @@
package net.pokeranalytics.android.calculus
import android.content.Context
import com.github.mikephil.charting.data.*
import io.realm.Realm
import io.realm.RealmResults
import net.pokeranalytics.android.R
import com.github.mikephil.charting.data.Entry
import net.pokeranalytics.android.exceptions.PAIllegalStateException
import net.pokeranalytics.android.model.filter.Query
import net.pokeranalytics.android.model.filter.QueryCondition
import net.pokeranalytics.android.model.interfaces.GraphIdentifiableEntry
import net.pokeranalytics.android.model.realm.ComputableResult
import net.pokeranalytics.android.model.realm.Filter
import net.pokeranalytics.android.model.realm.SessionSet
import net.pokeranalytics.android.ui.fragment.GraphFragment
import net.pokeranalytics.android.ui.graph.DataSetFactory
import net.pokeranalytics.android.ui.graph.GraphUnderlyingEntry
import net.pokeranalytics.android.ui.view.DefaultLegendValues
import net.pokeranalytics.android.ui.view.LegendContent
import net.pokeranalytics.android.util.ColorUtils
import net.pokeranalytics.android.util.TextFormat
import kotlin.math.abs
/**
* The class returned after performing calculation in the Calculator object
@ -43,138 +32,11 @@ class Report(var options: Calculator.Options) {
this._results.add(result)
}
/**
* Returns the list of entries corresponding to the provided [stat]
* One value will be returned by result
*/
fun lineEntries(stat: Stat? = null, context: Context): LineDataSet {
val entries = mutableListOf<Entry>()
val statToUse = stat ?: options.stats.firstOrNull()
val statName = statToUse?.name ?: ""
statToUse?.let {
this._results.forEachIndexed { index, results ->
results.computedStat(it)?.progressValue?.let { progressValue ->
entries.add(Entry(index.toFloat(), progressValue.toFloat(), results))
}
}
}
return DataSetFactory.lineDataSetInstance(entries, statName, context)
}
fun barEntries(stat: Stat? = null, context: Context): BarDataSet {
val entries = mutableListOf<BarEntry>()
val statToUse = stat ?: options.stats.firstOrNull()
statToUse?.let {
this._results.forEachIndexed { index, results ->
val cs = results.computedStat(it)
cs?.let { computedStat ->
val barEntry = BarEntry(index.toFloat(), computedStat.value.toFloat(), results)
entries.add(barEntry)
}
}
}
val label = statToUse?.name ?: ""
return DataSetFactory.barDataSetInstance(entries, label, context)
}
fun multiLineEntries(context: Context): List<LineDataSet> {
val dataSets = mutableListOf<LineDataSet>()
options.stats.forEach { stat ->
this._results.forEachIndexed { index, result ->
val ds = result.singleLineEntries(stat, context)
ds.color = ColorUtils.almostRandomColor(index, context)
dataSets.add(ds)
}
}
return dataSets
}
}
/**
* A sessionGroup of computable items identified by a name
*/
class ComputableGroup(var query: Query, var stats: List<Stat>? = null) {
/**
* A subgroup used to compute stat variation
*/
var comparedGroup: ComputableGroup? = null
/**
* The computed statIds of the comparable sessionGroup
*/
var comparedComputedResults: ComputedResults? = null
/**
* A list of _conditions to get
*/
val conditions: List<QueryCondition>
get() {
return this.query.conditions
}
/**
* The list of endedSessions to compute
*/
private var _computables: RealmResults<ComputableResult>? = null
/**
* Retrieves the computables on the relative [realm] filtered with the provided [conditions]
*/
fun computables(realm: Realm, sorted: Boolean = false): RealmResults<ComputableResult> {
// if computables exists and is valid (previous realm not closed)
this._computables?.let {
if (it.isValid) {
return it
}
}
val sortedField = if (sorted) "session.startDate" else null
val computables = Filter.queryOn<ComputableResult>(realm, this.query, sortedField)
this._computables = computables
return computables
}
/**
* The list of sets to compute
*/
private var _sessionSets: RealmResults<SessionSet>? = null
/**
* Retrieves the session sets on the relative [realm] filtered with the provided [conditions]
*/
fun sessionSets(realm: Realm, sorted: Boolean = false): RealmResults<SessionSet> {
// if computables exists and is valid (previous realm not closed)
this._sessionSets?.let {
if (it.isValid) {
return it
}
}
val sortedField = if (sorted) SessionSet.Field.START_DATE.identifier else null
val sets = Filter.queryOn<SessionSet>(realm, this.query, sortedField)
this._sessionSets = sets
return sets
}
fun cleanup() {
this._computables = null
this._sessionSets = null
}
val isEmpty: Boolean
get() {
return this._computables?.isEmpty() ?: true
}
class Point(val x: Double, val y: Double, val data: Any) {
constructor(y: Double, data: Any) : this(0.0, y, data)
}
class ComputedResults(group: ComputableGroup,
@ -196,6 +58,10 @@ class ComputedResults(group: ComputableGroup,
private val allStats: Collection<ComputedStat>
get() { return this._computedStats.values }
val evolutionValues: Map<Stat, MutableList<Point>>
get() {
return this._evolutionValues
}
/**
* Adds a value to the evolution values
@ -355,79 +221,6 @@ class ComputedResults(group: ComputableGroup,
this.consolidateProgressStats()
}
// MPAndroidChart
fun defaultStatEntries(stat: Stat, context: Context): DataSet<out Entry> {
return when (stat) {
Stat.NUMBER_OF_SETS, Stat.NUMBER_OF_GAMES, Stat.HOURLY_DURATION -> this.barEntries(stat, context = context)
Stat.STANDARD_DEVIATION -> this.distributionEntries(stat, context)
else -> this.singleLineEntries(stat, context)
}
}
fun singleLineEntries(stat: Stat, context: Context): LineDataSet {
val entries = mutableListOf<Entry>()
this._evolutionValues[stat]?.let { points ->
points.forEachIndexed { index, p ->
entries.add(Entry(index.toFloat(), p.y.toFloat(), p.data))
}
}
return DataSetFactory.lineDataSetInstance(entries, this.group.query.getName(context), context)
}
fun durationEntries(stat: Stat, context: Context): LineDataSet {
val entries = mutableListOf<Entry>()
this._evolutionValues[stat]?.let { points ->
points.forEach { p ->
entries.add(Entry(p.x.toFloat(), p.y.toFloat(), p.data))
}
}
return DataSetFactory.lineDataSetInstance(entries, stat.name, context)
}
private fun barEntries(stat: Stat, context: Context): BarDataSet {
val entries = mutableListOf<BarEntry>()
this._evolutionValues[stat]?.let { points ->
points.forEach { p ->
entries.add(BarEntry(p.x.toFloat(), p.y.toFloat(), p.data))
}
}
return DataSetFactory.barDataSetInstance(entries, stat.name, context)
}
private fun distributionEntries(stat: Stat, context: Context): BarDataSet {
val colors = mutableListOf<Int>()
val entries = mutableListOf<BarEntry>()
this._evolutionValues[stat]?.let { points ->
val negative = mutableListOf<Point>()
val positive = mutableListOf<Point>()
points.sortByDescending { it.y }
points.forEach {
if (it.y < 0) {
negative.add(it)
} else {
positive.add(it)
}
}
negative.forEachIndexed { index, p ->
entries.add(BarEntry(index.toFloat(), abs(p.y.toFloat()), p.data))
colors.add(context.getColor(R.color.red))
}
positive.forEachIndexed { index, p ->
val x = negative.size + index.toFloat()
entries.add(BarEntry(x, p.y.toFloat(), p.data))
colors.add(context.getColor(R.color.green))
}
}
return DataSetFactory.barDataSetInstance(entries, stat.name, context, colors)
}
val isEmpty: Boolean
get() {
return this.group.isEmpty
@ -488,10 +281,4 @@ class ComputedResults(group: ComputableGroup,
}
}
}
class Point(val x: Double, val y: Double, val data: Any) {
constructor(y: Double, data: Any) : this(0.0, y, data)
}

@ -16,6 +16,10 @@ import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.launch
import net.pokeranalytics.android.R
import net.pokeranalytics.android.calcul.barEntries
import net.pokeranalytics.android.calcul.defaultStatEntries
import net.pokeranalytics.android.calcul.durationEntries
import net.pokeranalytics.android.calcul.lineEntries
import net.pokeranalytics.android.calculus.AggregationType
import net.pokeranalytics.android.calculus.Calculator
import net.pokeranalytics.android.calculus.Report

@ -8,15 +8,10 @@ import com.github.mikephil.charting.charts.BarChart
import com.github.mikephil.charting.charts.BarLineChartBase
import com.github.mikephil.charting.components.XAxis
import net.pokeranalytics.android.R
import net.pokeranalytics.android.calculus.AxisFormatting
import net.pokeranalytics.android.model.utils.CrashLogging
import net.pokeranalytics.android.ui.extensions.px
enum class AxisFormatting {
DEFAULT,
X_DURATION,
Y_DURATION,
}
fun BarLineChartBase<*>.setStyle(
small: Boolean,
axisFormatting: AxisFormatting = AxisFormatting.DEFAULT,

@ -18,6 +18,7 @@ import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.launch
import net.pokeranalytics.android.R
import net.pokeranalytics.android.calcul.defaultStatEntries
import net.pokeranalytics.android.calculus.Calculator
import net.pokeranalytics.android.calculus.Stat
import net.pokeranalytics.android.model.filter.Query

@ -18,6 +18,7 @@ import com.google.android.material.chip.ChipGroup
import kotlinx.android.synthetic.main.row_feed_session.view.*
import kotlinx.android.synthetic.main.row_transaction.view.*
import net.pokeranalytics.android.R
import net.pokeranalytics.android.calculus.AxisFormatting
import net.pokeranalytics.android.calculus.ComputedStat
import net.pokeranalytics.android.calculus.Stat
import net.pokeranalytics.android.calculus.bankroll.BankrollReportManager
@ -33,7 +34,6 @@ import net.pokeranalytics.android.ui.extensions.ChipGroupExtension
import net.pokeranalytics.android.ui.extensions.px
import net.pokeranalytics.android.ui.extensions.setTextFormat
import net.pokeranalytics.android.ui.modules.bankroll.BankrollRowRepresentable
import net.pokeranalytics.android.ui.graph.AxisFormatting
import net.pokeranalytics.android.ui.graph.setStyle
import net.pokeranalytics.android.ui.modules.handhistory.views.RowHandHistoryViewHolder
import net.pokeranalytics.android.ui.view.holder.RowViewHolder

@ -4,10 +4,10 @@ import android.content.Context
import androidx.lifecycle.ViewModel
import com.github.mikephil.charting.data.BarDataSet
import com.github.mikephil.charting.data.LineDataSet
import net.pokeranalytics.android.calculus.AxisFormatting
import net.pokeranalytics.android.calculus.Stat
import net.pokeranalytics.android.exceptions.PAIllegalStateException
import net.pokeranalytics.android.ui.fragment.GraphFragment
import net.pokeranalytics.android.ui.graph.AxisFormatting
open class GraphViewModel : ViewModel(), GraphDataProvider {

@ -3,10 +3,9 @@ package net.pokeranalytics.android.ui.viewmodel
import android.content.Context
import com.github.mikephil.charting.data.BarDataSet
import com.github.mikephil.charting.data.LineDataSet
import net.pokeranalytics.android.calculus.AxisFormatting
import net.pokeranalytics.android.calculus.Report
import net.pokeranalytics.android.calculus.Stat
import net.pokeranalytics.android.ui.fragment.GraphFragment
import net.pokeranalytics.android.ui.graph.AxisFormatting
interface ReportHolder {

@ -3,6 +3,9 @@ package net.pokeranalytics.android.ui.viewmodel
import android.content.Context
import com.github.mikephil.charting.data.BarDataSet
import com.github.mikephil.charting.data.LineDataSet
import net.pokeranalytics.android.calcul.barEntries
import net.pokeranalytics.android.calcul.lineEntries
import net.pokeranalytics.android.calcul.multiLineEntries
import net.pokeranalytics.android.calculus.Calculator
import net.pokeranalytics.android.calculus.Report
import net.pokeranalytics.android.ui.activity.components.ReportParameters
@ -15,7 +18,7 @@ class ReportViewModel : GraphViewModel(), ReportHolder, GraphDataProvider {
companion object {
// Unparcel fails when setting a custom Parcelable object on Entry so we use a static reference to passe objects
// Unparcel fails when setting a custom Parcelable object on Entry so we use a static reference to pass objects
private var _parameters: ReportParameters? = null
val parameters: ReportParameters?

Loading…
Cancel
Save