1 Commits

Author SHA1 Message Date
soraefir cf9b1cc6e2 Better cascading stats 2026-07-09 20:52:27 +02:00
6 changed files with 113 additions and 93 deletions
@@ -57,23 +57,25 @@ class MainScreen : ComponentActivity() {
setContent {
SysTheme {
val currentPsvg = psvg
val currentCss = css
if (currentPsvg != null && currentCss != null) {
Box(modifier = Modifier.fillMaxSize().background(MaterialTheme.colors.primary).statusBarsPadding(),) {
AppNavHost(currentPsvg, currentCss)
}
Box(modifier = Modifier.fillMaxSize().background(MaterialTheme.colors.primary).statusBarsPadding(),) {
AppNavHost()
}
}
}
}
@Composable
fun AppNavHost(psvg: SVGWrapper, css: CSSWrapper) {
fun AppNavHost() {
val navController = rememberNavController()
NavHost(navController, startDestination = "main") {
composable("main") { MainScreenC(psvg,css, navController) }
composable("settings") { SettingsMainScreen { navController.navigate("main")} }
composable("main") {
val currentPsvg = psvg
val currentCss = css
if (currentPsvg != null && currentCss != null) {
MainScreenC(currentPsvg, currentCss, navController)
}
}
composable("settings") { SettingsMainScreen { navController.navigate("main") } }
composable("edit") { EditScreen { navController.navigate("main") } }
composable("stats") { StatsScreen { navController.navigate("main") } }
}
@@ -156,7 +156,7 @@ fun SettingsScreen(navController: NavHostController = settingsNav()) {
val keyProjection = stringResource(R.string.key_projection)
val keyGroup = stringResource(R.string.key_group)
val keyRegional = stringResource(R.string.key_regional)
val keyRegionalStats = stringResource(R.string.key_regional_stats)
val keyCascadeStats = stringResource(R.string.key_cascade_stats)
val offString = stringResource(R.string.off)
val onString = stringResource(R.string.on)
@@ -164,7 +164,7 @@ fun SettingsScreen(navController: NavHostController = settingsNav()) {
var projection by remember { mutableStateOf(prefs.getString(keyProjection, "default")!!) }
var groups by remember { mutableStateOf(prefs.getString(keyGroup, offString)!!) }
var regional by remember { mutableStateOf(prefs.getString(keyRegional, offString)!!) }
var regionalStats by remember { mutableStateOf(prefs.getString(keyRegionalStats, offString)!!) }
var cascadeStats by remember { mutableStateOf(prefs.getString(keyCascadeStats, offString)!!) }
var showGroupDialog by remember { mutableStateOf(false) }
var showRegionalDialog by remember { mutableStateOf(false) }
@@ -178,7 +178,10 @@ fun SettingsScreen(navController: NavHostController = settingsNav()) {
val g = Data.selected_group
if (g != null) {
Data.visits.reassignAllVisitedToGroup(g.key)
Data.groups.keepOnly(g.key)
Data.saveData()
groups = offString
prefs.edit { putString(keyGroup, offString) }
}
showGroupDialog = false
})
@@ -274,6 +277,7 @@ fun SettingsScreen(navController: NavHostController = settingsNav()) {
isChecked = groups == onString,
onCheckedChange = { isChecked ->
if (!isChecked) {
Data.selected_group = null
showGroupDialog = true
} else {
groups = onString
@@ -304,17 +308,18 @@ fun SettingsScreen(navController: NavHostController = settingsNav()) {
}
}
)
if (regional == onString) {
SettingSwitch(
label = stringResource(R.string.pref_regional_stats),
subtitle = stringResource(R.string.pref_regional_stats_desc),
isChecked = regionalStats == onString,
onCheckedChange = { isChecked ->
regionalStats = if (isChecked) onString else offString
prefs.edit { putString(keyRegionalStats, regionalStats) }
}
)
}
HorizontalDivider()
}
item {
SettingSwitch(
label = stringResource(R.string.pref_cascade_stats),
subtitle = stringResource(R.string.pref_cascade_stats_desc),
isChecked = cascadeStats == onString,
onCheckedChange = { isChecked ->
cascadeStats = if (isChecked) onString else offString
prefs.edit { putString(keyCascadeStats, cascadeStats) }
}
)
HorizontalDivider()
}
item {
@@ -20,6 +20,7 @@ import androidx.compose.material.TopAppBar
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.filled.ArrowBack
import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableIntStateOf
import androidx.compose.runtime.mutableStateOf
@@ -32,11 +33,14 @@ import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import net.helcel.beans.R
import net.helcel.beans.countries.Country
import net.helcel.beans.countries.GeoLoc
import net.helcel.beans.countries.GeoLoc.LocType
import net.helcel.beans.countries.World
import net.helcel.beans.helper.AUTO_GROUP
import net.helcel.beans.helper.Data
import net.helcel.beans.helper.Groups
import net.helcel.beans.helper.NO_GROUP
import net.helcel.beans.helper.Settings
import net.helcel.beans.helper.Theme.getContrastColor
@@ -116,80 +120,82 @@ fun StatsList(activeMode: LocType, countMode: Boolean) {
@Composable
fun StatsRow(group: Groups.Group, activeMode: LocType, countMode: Boolean) {
val context = LocalContext.current
val isRegionalStats = remember { Settings.isRegionalStats(context) }
val isCascadeStats = remember { Settings.isCascadeStats(context) }
val visits by Data.visits.visitsFlow.collectAsState()
val countries = remember { World.WWW.children.flatMap { it.children } }
val continents = remember { World.WWW.children.toList() }
val count = remember(group, activeMode, isRegionalStats) {
when (activeMode) {
LocType.WORLD -> continents.filter { continent ->
if (group.key == AUTO_GROUP) {
val isUncat = Data.visits.getVisited(continent) == AUTO_GROUP || continent.children.any { Data.visits.getVisited(it) == AUTO_GROUP }
val isInRealGroup = Data.groups.groupsFlow.value.any { g ->
Data.visits.getVisited(continent) == g.key || continent.children.any { Data.visits.getVisited(it) == g.key }
}
isUncat && (!isRegionalStats || !isInRealGroup)
} else {
Data.visits.getVisited(continent) == group.key || (isRegionalStats && continent.children.any { Data.visits.getVisited(it) == group.key })
}
}.size
LocType.COUNTRY -> countries.filter { country ->
if (group.key == AUTO_GROUP) {
val isUncat = Data.visits.getVisited(country) == AUTO_GROUP || country.children.any { Data.visits.getVisited(it) == AUTO_GROUP }
val isInRealGroup = Data.groups.groupsFlow.value.any { g ->
Data.visits.getVisited(country) == g.key || country.children.any { Data.visits.getVisited(it) == g.key }
}
isUncat && (!isRegionalStats || !isInRealGroup)
} else {
Data.visits.getVisited(country) == group.key || (isRegionalStats && country.children.any { Data.visits.getVisited(it) == group.key })
}
}.size
LocType.STATE -> countries.flatMap { it.children }.filter { region ->
Data.visits.getVisited(region) == group.key
}.size
else -> 0
val countries = remember {
World.WWW.children.flatMap {
if (it is Country) listOf(it) else it.children
}
}
val area = remember(group, activeMode, isRegionalStats) {
when (activeMode) {
LocType.WORLD -> continents.filter { continent ->
if (group.key == AUTO_GROUP) {
val isUncat = Data.visits.getVisited(continent) == AUTO_GROUP || continent.children.any { Data.visits.getVisited(it) == AUTO_GROUP }
val isInRealGroup = Data.groups.groupsFlow.value.any { g ->
Data.visits.getVisited(continent) == g.key || continent.children.any { Data.visits.getVisited(it) == g.key }
}
isUncat && (!isRegionalStats || !isInRealGroup)
} else {
Data.visits.getVisited(continent) == group.key || (isRegionalStats && continent.children.any { Data.visits.getVisited(it) == group.key })
}
}.sumOf { it.area }
LocType.COUNTRY -> countries.filter { country ->
if (group.key == AUTO_GROUP) {
val isUncat = Data.visits.getVisited(country) == AUTO_GROUP || country.children.any { Data.visits.getVisited(it) == AUTO_GROUP }
val isInRealGroup = Data.groups.groupsFlow.value.any { g ->
Data.visits.getVisited(country) == g.key || country.children.any { Data.visits.getVisited(it) == g.key }
}
isUncat && (!isRegionalStats || !isInRealGroup)
} else {
Data.visits.getVisited(country) == group.key || (isRegionalStats && country.children.any { Data.visits.getVisited(it) == group.key })
}
}.sumOf { it.area }
LocType.STATE -> countries.flatMap { it.children }.filter { region ->
Data.visits.getVisited(region) == group.key
}.sumOf { it.area }
else -> 0
val visitedItems = remember(group, activeMode, isCascadeStats, visits) {
val list = when (activeMode) {
LocType.WORLD -> continents
LocType.COUNTRY -> countries
LocType.STATE -> countries.flatMap { it.children }
else -> emptyList()
}
fun getExplicitColor(l: GeoLoc): Int? {
val c = visits.getOrDefault(l.code, 0)
return if (c != NO_GROUP && c != AUTO_GROUP) c else null
}
fun getEffectiveGroup(l: GeoLoc): Int {
getExplicitColor(l)?.let { return it }
if (isCascadeStats) {
// Parent -> Child (closest explicit ancestor)
val country = countries.find { it.children.contains(l) }
val countryColor = country?.let { getExplicitColor(it) }
if (countryColor != null) return countryColor
val continent = continents.find {
it.children.contains(l) || (country != null && it.children.contains(country))
}
val continentColor = continent?.let { getExplicitColor(it) }
if (continentColor != null) return continentColor
val worldColor = getExplicitColor(World.WWW)
if (worldColor != null) return worldColor
// Child -> Parent (most common explicit descendant)
val descendants = mutableListOf<GeoLoc>()
fun collect(curr: GeoLoc) {
curr.children.forEach {
descendants.add(it)
collect(it)
}
}
collect(l)
val descendantColors = descendants.mapNotNull { getExplicitColor(it) }
if (descendantColors.isNotEmpty()) {
return descendantColors.groupBy { it }.maxByOrNull { it.value.size }?.key ?: AUTO_GROUP
}
} else {
if (l.type == LocType.WORLD || l.type == LocType.COUNTRY) {
val descendants = mutableListOf<GeoLoc>()
fun collect(curr: GeoLoc) {
curr.children.forEach { descendants.add(it); collect(it) }
}
collect(l)
val dc = descendants.mapNotNull { getExplicitColor(it) }
if (dc.isNotEmpty()) return dc.groupBy { it }.maxByOrNull { it.value.size }?.key ?: AUTO_GROUP
}
}
return AUTO_GROUP
}
list.filter { getEffectiveGroup(it) == group.key }
}
val displayValue = if (countMode) count.toString() else stringResource(R.string.number_with_unit, area, "km²")
val count = visitedItems.size
val area = visitedItems.sumOf { it.area.toLong() }
val displayValue = if (countMode) count.toString() else stringResource(R.string.number_with_unit, area.toInt(), "km²")
val backgroundColor = group.color.color
val textColor = getContrastColor(backgroundColor)
@@ -43,6 +43,13 @@ class Groups(val id: Int, @SerialName("grps") private val groups: HashMap<Int, G
updateFlow()
}
fun keepOnly(key: Int) {
val keep = groups[key] ?: return
groups.clear()
groups[key] = keep
updateFlow()
}
private fun updateFlow() {
_groupsFlow.value = groups.values.toList()
}
@@ -29,10 +29,10 @@ object Settings {
)
}
fun isRegionalStats(ctx: Context): Boolean {
fun isCascadeStats(ctx: Context): Boolean {
return getBooleanValue(
ctx,
sp.getString(ctx.getString(R.string.key_regional_stats), ctx.getString(R.string.off))
sp.getString(ctx.getString(R.string.key_cascade_stats), ctx.getString(R.string.off))
)
}
+3 -3
View File
@@ -17,9 +17,9 @@
<string name="key_group">Groups</string>
<string name="key_regional">Regional mode</string>
<string name="key_regional_desc">Enable tracking of states and provinces</string>
<string name="key_regional_stats">key_regional_stats</string>
<string name="pref_regional_stats">Regional statistics</string>
<string name="pref_regional_stats_desc">Count countries with visited regions</string>
<string name="key_cascade_stats">key_cascade_stats</string>
<string name="pref_cascade_stats">Cascade statistics</string>
<string name="pref_cascade_stats_desc">Include parents and children in the counts</string>
<string name="key_group_desc">Enable multiple colors</string>
<string name="about">About</string>
<string name="pref_category_appearance">Appearance</string>