Compare commits
1 Commits
main
..
6ed1a36886
| Author | SHA1 | Date | |
|---|---|---|---|
| 6ed1a36886 |
@@ -23,7 +23,7 @@ jobs:
|
||||
contents: write
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v7
|
||||
- uses: actions/checkout@v6
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
|
||||
@@ -73,7 +73,7 @@ Thanks to all contributors, the developers of our dependencies, and our users.
|
||||
## 📝 License
|
||||
|
||||
```
|
||||
Copyright (C) 2026 Helcel & MYDOLI
|
||||
Copyright (C) 2024 Helcel & MYDOLI
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
|
||||
+9
-15
@@ -1,8 +1,8 @@
|
||||
plugins {
|
||||
id 'com.android.application'
|
||||
id 'org.jetbrains.kotlin.plugin.serialization' version '2.4.0'
|
||||
id 'org.jetbrains.kotlin.plugin.compose' version '2.4.0'
|
||||
id 'com.mikepenz.aboutlibraries.plugin' version '15.0.3'
|
||||
id 'org.jetbrains.kotlin.plugin.serialization' version '2.3.21'
|
||||
id 'org.jetbrains.kotlin.plugin.compose' version '2.3.21'
|
||||
id 'com.mikepenz.aboutlibraries.plugin' version '14.2.0'
|
||||
}
|
||||
|
||||
android {
|
||||
@@ -15,7 +15,7 @@ android {
|
||||
applicationId 'net.helcel.beans'
|
||||
minSdk = 28
|
||||
targetSdk = 37
|
||||
versionName "1.5"
|
||||
versionName "1.3"
|
||||
versionCode project.hasProperty('VERSION_CODE') ? project.property('VERSION_CODE').toInteger() : 1
|
||||
}
|
||||
signingConfigs {
|
||||
@@ -87,7 +87,7 @@ aboutLibraries {
|
||||
|
||||
dependencies {
|
||||
implementation 'androidx.compose.material3:material3:1.4.0'
|
||||
implementation "androidx.compose.material:material:1.11.4"
|
||||
implementation "androidx.compose.material:material:1.11.2"
|
||||
implementation 'androidx.compose.material:material-icons-extended:1.7.8'
|
||||
implementation 'androidx.navigation:navigation-compose:2.9.8'
|
||||
coreLibraryDesugaring 'com.android.tools:desugar_jdk_libs_nio:2.1.5'
|
||||
@@ -106,16 +106,10 @@ dependencies {
|
||||
implementation 'com.github.chrisbanes:PhotoView:2.3.0'
|
||||
|
||||
implementation 'com.mikepenz:aboutlibraries:14.2.1'
|
||||
implementation 'com.mikepenz:aboutlibraries-compose-m3:15.0.3'
|
||||
implementation 'com.mikepenz:aboutlibraries-core:15.0.3'
|
||||
implementation 'com.mikepenz:aboutlibraries-compose-m3:14.2.1'
|
||||
implementation 'com.mikepenz:aboutlibraries-core:14.2.0'
|
||||
|
||||
|
||||
implementation platform('androidx.compose:compose-bom:2026.06.01')
|
||||
debugImplementation 'androidx.compose.ui:ui-tooling:1.11.4'
|
||||
}
|
||||
|
||||
tasks.configureEach { task ->
|
||||
if (task.name.startsWith("merge") && task.name.endsWith("Assets")) {
|
||||
task.outputs.upToDateWhen { false }
|
||||
}
|
||||
implementation platform('androidx.compose:compose-bom:2026.05.01')
|
||||
debugImplementation 'androidx.compose.ui:ui-tooling:1.11.2'
|
||||
}
|
||||
+4407
-1
File diff suppressed because one or more lines are too long
|
Before Width: | Height: | Size: 3.9 MiB After Width: | Height: | Size: 6.1 MiB |
+3102
-3104
File diff suppressed because it is too large
Load Diff
+4407
-1
File diff suppressed because one or more lines are too long
|
Before Width: | Height: | Size: 3.7 MiB After Width: | Height: | Size: 6.0 MiB |
File diff suppressed because one or more lines are too long
|
Before Width: | Height: | Size: 4.0 MiB After Width: | Height: | Size: 6.1 MiB |
@@ -14,6 +14,7 @@ import androidx.compose.material.Icon
|
||||
import androidx.compose.material.IconButton
|
||||
import androidx.compose.material.MaterialTheme
|
||||
import androidx.compose.material.Scaffold
|
||||
import androidx.compose.material.Surface
|
||||
import androidx.compose.material.Text
|
||||
import androidx.compose.material.TopAppBar
|
||||
import androidx.compose.material.icons.Icons
|
||||
@@ -21,11 +22,10 @@ import androidx.compose.material.icons.filled.Edit
|
||||
import androidx.compose.material.icons.filled.Percent
|
||||
import androidx.compose.material.icons.filled.Settings
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.viewinterop.AndroidView
|
||||
import androidx.navigation.NavHostController
|
||||
import androidx.navigation.compose.NavHost
|
||||
@@ -43,39 +43,32 @@ import net.helcel.beans.svg.SVGWrapper
|
||||
|
||||
class MainScreen : ComponentActivity() {
|
||||
|
||||
private var psvg by mutableStateOf<SVGWrapper?>(null)
|
||||
private var css by mutableStateOf<CSSWrapper?>(null)
|
||||
private lateinit var psvg: SVGWrapper
|
||||
private lateinit var css: CSSWrapper
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
actionBar?.hide()
|
||||
Settings.start(this)
|
||||
Data.loadData(this, Int.MIN_VALUE)
|
||||
GeoLocImporter.importStates(this)
|
||||
|
||||
refreshProjection()
|
||||
Data.loadData(this, Int.MIN_VALUE)
|
||||
|
||||
setContent {
|
||||
SysTheme {
|
||||
Box(modifier = Modifier.fillMaxSize().background(MaterialTheme.colors.primary).statusBarsPadding(),) {
|
||||
AppNavHost()
|
||||
Box(modifier = Modifier.fillMaxSize().background(MaterialTheme.colors.background).statusBarsPadding(),) {
|
||||
AppNavHost(psvg, css)
|
||||
}
|
||||
}
|
||||
}
|
||||
refreshProjection()
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun AppNavHost() {
|
||||
fun AppNavHost(psvg: SVGWrapper, css: CSSWrapper) {
|
||||
val navController = rememberNavController()
|
||||
NavHost(navController, startDestination = "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("main") { MainScreenC(psvg,css, navController) }
|
||||
composable("settings") { SettingsMainScreen { navController.navigate("main")} }
|
||||
composable("edit") { EditScreen { navController.navigate("main") } }
|
||||
composable("stats") { StatsScreen { navController.navigate("main") } }
|
||||
}
|
||||
@@ -112,25 +105,18 @@ class MainScreen : ComponentActivity() {
|
||||
@Composable
|
||||
fun MapScreen(psvg: SVGWrapper, css: CSSWrapper) {
|
||||
Box {
|
||||
val cssContent = css.get()
|
||||
val drawable = remember(psvg, css, cssContent) {
|
||||
val opt: RenderOptions = RenderOptions.create()
|
||||
opt.css(cssContent)
|
||||
opt.css(css.get())
|
||||
val drawable = remember(psvg, css) {
|
||||
PictureDrawable(psvg.get()?.renderToPicture(opt))
|
||||
}
|
||||
AndroidView(
|
||||
factory = { ctx ->
|
||||
AndroidView(factory = { ctx ->
|
||||
PhotoView(ctx).apply {
|
||||
setLayerType(ImageView.LAYER_TYPE_SOFTWARE, null)
|
||||
maximumScale = 64f
|
||||
setImageDrawable(drawable)
|
||||
scaleType = ImageView.ScaleType.FIT_CENTER
|
||||
}
|
||||
},
|
||||
update = { view ->
|
||||
view.setImageDrawable(drawable)
|
||||
},
|
||||
modifier = Modifier.fillMaxSize()
|
||||
)
|
||||
}, modifier = Modifier.fillMaxSize())
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -48,22 +48,23 @@ import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.window.Dialog
|
||||
import androidx.preference.PreferenceManager
|
||||
import net.helcel.beans.R
|
||||
import net.helcel.beans.countries.GeoLocImporter
|
||||
import net.helcel.beans.helper.Settings
|
||||
import androidx.core.content.edit
|
||||
import androidx.navigation.NavHostController
|
||||
import androidx.navigation.compose.NavHost
|
||||
import androidx.navigation.compose.composable
|
||||
import androidx.navigation.compose.rememberNavController
|
||||
import androidx.preference.PreferenceManager
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
import net.helcel.beans.R
|
||||
import net.helcel.beans.activity.sub.AboutScreen
|
||||
import net.helcel.beans.activity.sub.EditPlaceColorDialog
|
||||
import net.helcel.beans.activity.sub.EditPlaceDialog
|
||||
import net.helcel.beans.activity.sub.LicenseScreen
|
||||
import net.helcel.beans.countries.GeoLocImporter
|
||||
import net.helcel.beans.helper.Data
|
||||
import net.helcel.beans.helper.Settings
|
||||
|
||||
@Composable
|
||||
fun SysTheme(
|
||||
@@ -155,39 +156,150 @@ fun SettingsScreen(navController: NavHostController = settingsNav()) {
|
||||
val defaultTheme = stringResource(R.string.system)
|
||||
val keyProjection = stringResource(R.string.key_projection)
|
||||
val keyGroup = stringResource(R.string.key_group)
|
||||
val keyRegional = stringResource(R.string.key_regional)
|
||||
val keyCascadeStats = stringResource(R.string.key_cascade_stats)
|
||||
val offString = stringResource(R.string.off)
|
||||
val onString = stringResource(R.string.on)
|
||||
|
||||
var showEdit by remember { mutableStateOf(false) }
|
||||
|
||||
var theme by remember { mutableStateOf(prefs.getString(keyTheme, defaultTheme)!!) }
|
||||
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 cascadeStats by remember { mutableStateOf(prefs.getString(keyCascadeStats, offString)!!) }
|
||||
var groups by remember { mutableStateOf(prefs.getString(keyGroup,offString)!!) }
|
||||
|
||||
var showGroupDialog by remember { mutableStateOf(false) }
|
||||
var showRegionalDialog by remember { mutableStateOf(false) }
|
||||
var showLoad by remember { mutableStateOf(false) }
|
||||
val scope = rememberCoroutineScope()
|
||||
if(showEdit)
|
||||
EditPlaceDialog(true) {
|
||||
showEdit = false
|
||||
val g = Data.selected_group
|
||||
if (it && g != null)
|
||||
Data.visits.reassignAllVisitedToGroup(g.key)
|
||||
}
|
||||
|
||||
if (showGroupDialog) {
|
||||
LazyColumn(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.padding(16.dp)
|
||||
.background(MaterialTheme.colors.background)
|
||||
) {
|
||||
item {
|
||||
Text(
|
||||
"Theme", style = MaterialTheme.typography.h6,
|
||||
color = MaterialTheme.colors.onBackground,
|
||||
)
|
||||
MultiPreference(arrayOf(stringResource(R.string.system),stringResource(R.string.light),stringResource(R.string.dark)), theme) { newTheme ->
|
||||
theme = newTheme
|
||||
prefs.edit { putString(keyTheme, newTheme) }
|
||||
}
|
||||
HorizontalDivider()
|
||||
}
|
||||
item {
|
||||
Text(
|
||||
"Map Projection",
|
||||
style = MaterialTheme.typography.h6,
|
||||
color = MaterialTheme.colors.onBackground,
|
||||
modifier = Modifier.padding(top = 16.dp)
|
||||
)
|
||||
MultiPreference(arrayOf(stringResource(R.string.mercator), stringResource(R.string.azimuthalequidistant)), projection) { newProj ->
|
||||
projection = newProj
|
||||
prefs.edit { putString(keyProjection, newProj) }
|
||||
Settings.refreshProjection()
|
||||
}
|
||||
HorizontalDivider()
|
||||
}
|
||||
item {
|
||||
Text(
|
||||
"Groups",
|
||||
style = MaterialTheme.typography.h6,
|
||||
color = MaterialTheme.colors.onBackground,
|
||||
modifier = Modifier.padding(top = 16.dp)
|
||||
)
|
||||
var showDialog by remember{mutableStateOf(false)}
|
||||
if(showDialog){
|
||||
EditPlaceColorDialog(
|
||||
deleteMode = true,
|
||||
onDismiss = {
|
||||
val g = Data.selected_group
|
||||
if (g != null) {
|
||||
if (g != null)
|
||||
Data.visits.reassignAllVisitedToGroup(g.key)
|
||||
Data.groups.keepOnly(g.key)
|
||||
Data.saveData()
|
||||
groups = offString
|
||||
prefs.edit { putString(keyGroup, offString) }
|
||||
}
|
||||
showGroupDialog = false
|
||||
showDialog = false
|
||||
})
|
||||
}
|
||||
MultiPreference(
|
||||
arrayOf(stringResource(R.string.on), stringResource(R.string.off)),
|
||||
groups
|
||||
) { key ->
|
||||
if (key == offString) {
|
||||
showDialog=true
|
||||
}
|
||||
groups = key
|
||||
prefs.edit { putString(keyGroup, key) }
|
||||
}
|
||||
HorizontalDivider()
|
||||
}
|
||||
item {
|
||||
Text(
|
||||
text = "Regional",
|
||||
style = MaterialTheme.typography.h6,
|
||||
color = MaterialTheme.colors.onBackground,
|
||||
modifier = Modifier
|
||||
.padding(top = 16.dp)
|
||||
.clickable(onClick = {}),
|
||||
)
|
||||
RegionalScreen()
|
||||
HorizontalDivider()
|
||||
}
|
||||
item{
|
||||
val launcher = rememberLauncherForActivityResult(
|
||||
contract = ActivityResultContracts.OpenDocument(),
|
||||
onResult = { uri -> Data.doImport(context, uri) }
|
||||
)
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
) {
|
||||
Button(onClick = {
|
||||
launcher.launch(arrayOf("*/*"))
|
||||
}, modifier = Modifier
|
||||
.fillMaxWidth(fraction = 0.4f)
|
||||
.padding(vertical = 8.dp)) {
|
||||
Text("Import")
|
||||
}
|
||||
Spacer(
|
||||
modifier = Modifier.fillMaxWidth(0.4f)
|
||||
)
|
||||
Button(onClick = {
|
||||
Data.doExport(context)
|
||||
}, modifier = Modifier
|
||||
.fillMaxWidth(fraction = 1f)
|
||||
.padding(vertical = 8.dp)) {
|
||||
Text("Export")
|
||||
}
|
||||
}
|
||||
HorizontalDivider()
|
||||
}
|
||||
item {
|
||||
PreferenceButton("Licenses") {
|
||||
if (navController.currentDestination?.route != "licenses")
|
||||
navController.navigate("licenses")
|
||||
}
|
||||
PreferenceButton("About") {
|
||||
if (navController.currentDestination?.route != "about")
|
||||
navController.navigate("about")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (showRegionalDialog) {
|
||||
@Composable
|
||||
fun RegionalScreen() {
|
||||
val context = LocalContext.current
|
||||
|
||||
val keyRegional = stringResource(R.string.key_regional)
|
||||
val offString = stringResource(R.string.off)
|
||||
val onString = stringResource(R.string.on)
|
||||
val prefs = PreferenceManager.getDefaultSharedPreferences(context)
|
||||
var selected by remember { mutableStateOf(prefs.getString(keyRegional, offString)!!)}
|
||||
var regional by remember{ mutableStateOf(prefs.getString(keyRegional, offString)!!)}
|
||||
var showDialog by remember{mutableStateOf(false)}
|
||||
var showLoad by remember{mutableStateOf(false)}
|
||||
|
||||
if(showDialog)
|
||||
Dialog(
|
||||
content = {
|
||||
Column(
|
||||
@@ -196,27 +308,26 @@ fun SettingsScreen(navController: NavHostController = settingsNav()) {
|
||||
MaterialTheme.colors.background,
|
||||
RoundedCornerShape(corner = CornerSize(16.dp))
|
||||
)
|
||||
.padding(16.dp),
|
||||
) {
|
||||
Text(
|
||||
style = MaterialTheme.typography.caption,
|
||||
text = stringResource(R.string.delete_regions)
|
||||
)
|
||||
.padding(16.dp),){
|
||||
Text(style=MaterialTheme.typography.caption, text= stringResource(R.string.delete_regions))
|
||||
Button(onClick = {
|
||||
GeoLocImporter.clearStates()
|
||||
regional = offString
|
||||
prefs.edit { putString(keyRegional, offString) }
|
||||
showRegionalDialog = false
|
||||
}) {
|
||||
regional= selected
|
||||
prefs.edit {
|
||||
putString(
|
||||
keyRegional,
|
||||
regional
|
||||
)
|
||||
}
|
||||
showDialog=false
|
||||
}){
|
||||
Text(stringResource(R.string.ok))
|
||||
}
|
||||
}
|
||||
},
|
||||
onDismissRequest = { showRegionalDialog = false }
|
||||
onDismissRequest = { showDialog=false }
|
||||
)
|
||||
}
|
||||
|
||||
if (showLoad) {
|
||||
if(showLoad){
|
||||
Dialog(
|
||||
content = {
|
||||
CircularProgressIndicator(
|
||||
@@ -228,155 +339,41 @@ fun SettingsScreen(navController: NavHostController = settingsNav()) {
|
||||
onDismissRequest = {}
|
||||
)
|
||||
}
|
||||
|
||||
LazyColumn(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.padding(16.dp)
|
||||
.background(MaterialTheme.colors.background)
|
||||
) {
|
||||
item {
|
||||
Text(
|
||||
stringResource(R.string.key_theme),
|
||||
style = MaterialTheme.typography.h6,
|
||||
color = MaterialTheme.colors.onBackground,
|
||||
)
|
||||
MultiPreference(
|
||||
arrayOf(
|
||||
stringResource(R.string.system),
|
||||
stringResource(R.string.light),
|
||||
stringResource(R.string.dark)
|
||||
), theme
|
||||
) { newTheme ->
|
||||
theme = newTheme
|
||||
prefs.edit { putString(keyTheme, newTheme) }
|
||||
val scope = rememberCoroutineScope()
|
||||
MultiPreference(arrayOf(stringResource(R.string.on),stringResource(R.string.off)),regional) { key ->
|
||||
when (key) {
|
||||
offString -> { showDialog=true
|
||||
selected=key
|
||||
}
|
||||
HorizontalDivider()
|
||||
}
|
||||
item {
|
||||
Text(
|
||||
stringResource(R.string.key_projection),
|
||||
style = MaterialTheme.typography.h6,
|
||||
color = MaterialTheme.colors.onBackground,
|
||||
modifier = Modifier.padding(top = 16.dp)
|
||||
)
|
||||
MultiPreference(
|
||||
arrayOf(stringResource(R.string.mercator), stringResource(R.string.azimuthalequidistant)),
|
||||
projection
|
||||
) { newProj ->
|
||||
projection = newProj
|
||||
prefs.edit { putString(keyProjection, newProj) }
|
||||
Settings.refreshProjection()
|
||||
}
|
||||
HorizontalDivider()
|
||||
}
|
||||
item {
|
||||
SettingSwitch(
|
||||
label = stringResource(R.string.key_group),
|
||||
subtitle = stringResource(R.string.key_group_desc),
|
||||
isChecked = groups == onString,
|
||||
onCheckedChange = { isChecked ->
|
||||
if (!isChecked) {
|
||||
Data.selected_group = null
|
||||
showGroupDialog = true
|
||||
} else {
|
||||
groups = onString
|
||||
prefs.edit { putString(keyGroup, onString) }
|
||||
}
|
||||
}
|
||||
)
|
||||
HorizontalDivider()
|
||||
}
|
||||
item {
|
||||
SettingSwitch(
|
||||
label = stringResource(R.string.key_regional),
|
||||
subtitle = stringResource(R.string.key_regional_desc),
|
||||
isChecked = regional == onString,
|
||||
onCheckedChange = { isChecked ->
|
||||
if (isChecked) {
|
||||
regional = onString
|
||||
prefs.edit { putString(keyRegional, onString) }
|
||||
showLoad = true
|
||||
onString -> {
|
||||
regional = key
|
||||
prefs.edit { putString(keyRegional, key) }
|
||||
showLoad=true
|
||||
scope.launch {
|
||||
withContext(Dispatchers.IO) {
|
||||
GeoLocImporter.importStates(context, true)
|
||||
}
|
||||
showLoad = false
|
||||
}
|
||||
} else {
|
||||
showRegionalDialog = true
|
||||
}
|
||||
}
|
||||
)
|
||||
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 {
|
||||
val launcher = rememberLauncherForActivityResult(
|
||||
contract = ActivityResultContracts.OpenDocument(),
|
||||
onResult = { uri -> Data.doImport(context, uri) }
|
||||
)
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth().padding(vertical = 8.dp)
|
||||
) {
|
||||
Button(
|
||||
onClick = {
|
||||
launcher.launch(arrayOf("*/*"))
|
||||
}, modifier = Modifier
|
||||
.weight(1f)
|
||||
) {
|
||||
Text(stringResource(R.string.action_import))
|
||||
}
|
||||
Spacer(modifier = Modifier.size(16.dp))
|
||||
Button(
|
||||
onClick = {
|
||||
Data.doExport(context)
|
||||
}, modifier = Modifier
|
||||
.weight(1f)
|
||||
) {
|
||||
Text(stringResource(R.string.action_export))
|
||||
}
|
||||
}
|
||||
HorizontalDivider()
|
||||
}
|
||||
item {
|
||||
PreferenceButton(stringResource(R.string.licenses)) {
|
||||
if (navController.currentDestination?.route != "licenses")
|
||||
navController.navigate("licenses")
|
||||
}
|
||||
PreferenceButton(stringResource(R.string.about)) {
|
||||
if (navController.currentDestination?.route != "about")
|
||||
navController.navigate("about")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@Composable
|
||||
fun MultiPreference(list: Array<String>, selected: String, onSelected: (String) -> Unit) {
|
||||
Column(Modifier.padding(vertical = 2.dp)) {
|
||||
Column(Modifier.padding(2.dp)) {
|
||||
list.forEach { value ->
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.height(42.dp)
|
||||
.height(36.dp)
|
||||
.clickable { onSelected(value) }) {
|
||||
RadioButton(selected = selected == value, onClick = { onSelected(value) })
|
||||
Text(
|
||||
value, modifier = Modifier.padding(start = 12.dp),
|
||||
style = MaterialTheme.typography.body1,
|
||||
value, modifier = Modifier.padding(start = 8.dp),
|
||||
color = MaterialTheme.colors.onBackground,
|
||||
)
|
||||
}
|
||||
@@ -392,38 +389,3 @@ fun PreferenceButton(text: String, onClick: () -> Unit) {
|
||||
Text(text)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun SettingSwitch(
|
||||
label: String,
|
||||
subtitle: String? = null,
|
||||
isChecked: Boolean,
|
||||
onCheckedChange: (Boolean) -> Unit
|
||||
) {
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.clickable { onCheckedChange(!isChecked) }
|
||||
.padding(vertical = 12.dp),
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
Column(modifier = Modifier.weight(1f)) {
|
||||
Text(
|
||||
text=label,
|
||||
style = MaterialTheme.typography.h6,
|
||||
color = MaterialTheme.colors.onBackground,
|
||||
)
|
||||
if (subtitle != null) {
|
||||
Text(
|
||||
text = subtitle,
|
||||
style = MaterialTheme.typography.body2,
|
||||
color = MaterialTheme.colors.onSurface.copy(alpha = 0.6f)
|
||||
)
|
||||
}
|
||||
}
|
||||
androidx.compose.material.Switch(
|
||||
checked = isChecked,
|
||||
onCheckedChange = onCheckedChange
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -20,7 +20,6 @@ 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
|
||||
@@ -33,15 +32,12 @@ 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.Settings.isRegional
|
||||
import net.helcel.beans.helper.Theme.getContrastColor
|
||||
|
||||
private val MODE_LIST = listOf(LocType.WORLD, LocType.COUNTRY, LocType.STATE)
|
||||
@@ -50,7 +46,7 @@ private val MODE_LIST = listOf(LocType.WORLD, LocType.COUNTRY, LocType.STATE)
|
||||
fun StatsScreen(
|
||||
onExit: ()-> Unit
|
||||
) {
|
||||
val modes = if (Settings.isRegional(LocalContext.current)) MODE_LIST else MODE_LIST.take(2)
|
||||
val modes = if (isRegional(LocalContext.current)) MODE_LIST else MODE_LIST.take(2)
|
||||
var selectedTab by remember { mutableIntStateOf(0) }
|
||||
var countMode by remember { mutableStateOf(true) }
|
||||
|
||||
@@ -61,7 +57,7 @@ fun StatsScreen(
|
||||
TopAppBar(
|
||||
title = {
|
||||
Row(verticalAlignment = Alignment.CenterVertically){
|
||||
Text(text=stringResource(R.string.action_stat), modifier = Modifier.weight(1f))
|
||||
Text(text=stringResource(R.string.action_edit), modifier = Modifier.weight(1f))
|
||||
Button(onClick = { countMode = !countMode }) {
|
||||
Text(if (countMode) "Count" else "Area")
|
||||
}
|
||||
@@ -119,83 +115,26 @@ fun StatsList(activeMode: LocType, countMode: Boolean) {
|
||||
|
||||
@Composable
|
||||
fun StatsRow(group: Groups.Group, activeMode: LocType, countMode: Boolean) {
|
||||
val context = LocalContext.current
|
||||
val isCascadeStats = remember { Settings.isCascadeStats(context) }
|
||||
val visits by Data.visits.visitsFlow.collectAsState()
|
||||
|
||||
val continents = remember { World.WWW.children.toList() }
|
||||
val countries = remember {
|
||||
World.WWW.children.flatMap {
|
||||
if (it is Country) listOf(it) else it.children
|
||||
}
|
||||
val visited = remember(group, activeMode) {
|
||||
Data.visits.getVisitedByValue(group.key)
|
||||
}
|
||||
|
||||
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()
|
||||
val count = when (activeMode) {
|
||||
LocType.WORLD -> World.WWW.children.filter { it.code in visited }.size
|
||||
LocType.COUNTRY -> World.WWW.children.flatMap { it.children.filter { c -> c.code in visited } }.size
|
||||
LocType.STATE -> World.WWW.children.flatMap { a->a.children.flatMap { b->b.children.filter { c->c.code in visited } } }.size
|
||||
else -> 0
|
||||
}
|
||||
|
||||
fun getExplicitColor(l: GeoLoc): Int? {
|
||||
val c = visits.getOrDefault(l.code, 0)
|
||||
return if (c != NO_GROUP && c != AUTO_GROUP) c else null
|
||||
val area = when (activeMode) {
|
||||
LocType.WORLD -> World.WWW.children.filter { it.code in visited }.sumOf { it.area }
|
||||
LocType.COUNTRY -> World.WWW.children.flatMap { it.children.filter { c -> c.code in visited } }.sumOf { it.area }
|
||||
LocType.STATE -> World.WWW.children.flatMap { a->a.children.flatMap { b->b.children.filter { c->c.code in visited } } }.sumOf { it.area }
|
||||
else -> 0
|
||||
}
|
||||
|
||||
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 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 displayValue = if (countMode) count.toString() else stringResource(R.string.number_with_unit, area, "km²")
|
||||
|
||||
val backgroundColor = group.color.color
|
||||
val textColor = getContrastColor(backgroundColor)
|
||||
|
||||
@@ -54,51 +54,27 @@ fun EditPlaceScreenPreview(){
|
||||
EditPlaceScreen(Group.EEE)
|
||||
}
|
||||
|
||||
fun syncVisited(loc: GeoLoc?=World.WWW): Boolean {
|
||||
var changed = false
|
||||
fun syncVisited(loc: GeoLoc?=World.WWW){
|
||||
loc?.children?.forEach { tt ->
|
||||
tt.children.forEach {itc->
|
||||
if(Data.visits.getVisited(itc) in listOf(AUTO_GROUP,NO_GROUP)) {
|
||||
val newState = if(itc.children.any { c -> Data.visits.getVisited(c) != NO_GROUP })
|
||||
AUTO_GROUP
|
||||
if(itc.children.any { c -> Data.visits.getVisited(c) != NO_GROUP })
|
||||
Data.visits.setVisited(itc, AUTO_GROUP)
|
||||
else
|
||||
NO_GROUP
|
||||
|
||||
if (Data.visits.getVisited(itc) != newState) {
|
||||
Data.visits.setVisited(itc, newState)
|
||||
changed = true
|
||||
}
|
||||
Data.visits.setVisited(itc, NO_GROUP)
|
||||
}
|
||||
}
|
||||
if(Data.visits.getVisited(tt) in listOf(AUTO_GROUP,NO_GROUP)) {
|
||||
val newState = if(tt.children.any { itc -> Data.visits.getVisited(itc) != NO_GROUP })
|
||||
AUTO_GROUP
|
||||
if(tt.children.any { itc -> Data.visits.getVisited(itc) != NO_GROUP })
|
||||
Data.visits.setVisited(tt, AUTO_GROUP)
|
||||
else
|
||||
NO_GROUP
|
||||
|
||||
if (Data.visits.getVisited(tt) != newState) {
|
||||
Data.visits.setVisited(tt, newState)
|
||||
changed = true
|
||||
Data.visits.setVisited(tt, NO_GROUP)
|
||||
}
|
||||
}
|
||||
}
|
||||
// Sync World from Continents
|
||||
if (loc != null && Data.visits.getVisited(loc) in listOf(AUTO_GROUP, NO_GROUP)) {
|
||||
val newState = if(loc.children.any { Data.visits.getVisited(it) != NO_GROUP })
|
||||
AUTO_GROUP
|
||||
else
|
||||
NO_GROUP
|
||||
if (Data.visits.getVisited(loc) != newState) {
|
||||
Data.visits.setVisited(loc, newState)
|
||||
changed = true
|
||||
}
|
||||
}
|
||||
return changed
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun EditPlaceScreen(loc: GeoLoc, onExit:()->Unit={}) {
|
||||
val visits by Data.visits.visitsFlow.collectAsState()
|
||||
var showEdit by remember { mutableStateOf(false) }
|
||||
val tabs : SnapshotStateList<GeoLoc> = remember { mutableStateListOf(loc) }
|
||||
val ctx = LocalContext.current
|
||||
@@ -108,12 +84,7 @@ fun EditPlaceScreen(loc: GeoLoc, onExit:()->Unit={}) {
|
||||
selectedTab = tabs.lastIndex
|
||||
}
|
||||
SideEffect {
|
||||
// visits is used to trigger sync whenever data changes
|
||||
if (visits.isNotEmpty() || true) {
|
||||
if (syncVisited()) {
|
||||
Data.saveData()
|
||||
}
|
||||
}
|
||||
syncVisited()
|
||||
}
|
||||
BackHandler {
|
||||
if (tabs.size > 1) tabs.removeAt(tabs.lastIndex)
|
||||
@@ -124,7 +95,6 @@ fun EditPlaceScreen(loc: GeoLoc, onExit:()->Unit={}) {
|
||||
showEdit = false
|
||||
if (it) {
|
||||
Data.visits.setVisited(Data.selected_geoloc, NO_GROUP)
|
||||
syncVisited()
|
||||
Data.saveData()
|
||||
|
||||
if (Data.selected_geoloc!=null && Data.selected_geoloc!!.children.any { itc-> Data.visits.getVisited(itc) != NO_GROUP }) {
|
||||
@@ -133,7 +103,6 @@ fun EditPlaceScreen(loc: GeoLoc, onExit:()->Unit={}) {
|
||||
}
|
||||
if (Data.selected_group != null && Data.selected_geoloc != null) {
|
||||
Data.visits.setVisited(Data.selected_geoloc, Data.selected_group!!.key)
|
||||
syncVisited()
|
||||
Data.saveData()
|
||||
}
|
||||
Data.selected_geoloc = null
|
||||
@@ -175,14 +144,12 @@ fun EditPlaceScreen(loc: GeoLoc, onExit:()->Unit={}) {
|
||||
Data.visits.getVisited(itc)!= NO_GROUP } == true) AUTO_GROUP
|
||||
else NO_GROUP
|
||||
)
|
||||
Data.saveData()
|
||||
Data.selected_group = null
|
||||
} else {
|
||||
Data.selected_group = null
|
||||
showEdit=true
|
||||
}
|
||||
syncVisited()
|
||||
Data.saveData()
|
||||
|
||||
})
|
||||
|
||||
}
|
||||
|
||||
@@ -43,14 +43,14 @@ object Data {
|
||||
if (groups.size() == 0) {
|
||||
groups.setGroup(DEFAULT_GROUP, "Visited",
|
||||
ContextCompat.getColor(ctx, R.color.blue).toDrawable())
|
||||
}
|
||||
saveData()
|
||||
}
|
||||
}
|
||||
|
||||
fun saveData() {
|
||||
if(groups.id != visits.id) return
|
||||
val id = groups.id
|
||||
sharedPreferences.edit(commit=true) {
|
||||
sharedPreferences.edit {
|
||||
putString("groups_$id", groupsSerial.writeTo(groups))
|
||||
putString("visits_$id", visitsSerial.writeTo(visits))
|
||||
}
|
||||
|
||||
@@ -32,25 +32,12 @@ class Groups(val id: Int, @SerialName("grps") private val groups: HashMap<Int, G
|
||||
val groupsFlow: StateFlow<List<Group>> = _groupsFlow.asStateFlow()
|
||||
|
||||
fun setGroup(key: Int, name: String, col: ColorDrawable) {
|
||||
val old = groups[key]
|
||||
if (old != null && old.name == name && old.color.color == col.color) return
|
||||
groups[key] = Group(key, name, col)
|
||||
updateFlow()
|
||||
_groupsFlow.value = groups.values.toList()
|
||||
}
|
||||
|
||||
fun deleteGroup(key: Int) {
|
||||
groups.remove(key)
|
||||
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,13 +29,6 @@ object Settings {
|
||||
)
|
||||
}
|
||||
|
||||
fun isCascadeStats(ctx: Context): Boolean {
|
||||
return getBooleanValue(
|
||||
ctx,
|
||||
sp.getString(ctx.getString(R.string.key_cascade_stats), ctx.getString(R.string.off))
|
||||
)
|
||||
}
|
||||
|
||||
fun getStatPref(ctx: Context): String? {
|
||||
return sp.getString(
|
||||
ctx.getString(R.string.key_stats),
|
||||
|
||||
@@ -19,25 +19,24 @@ class Visits(val id: Int, private val locs: HashMap<String, Int>) {
|
||||
val visitsFlow: StateFlow<Map<String,Int>> = _visitsFlow
|
||||
|
||||
fun setVisited(key: GeoLoc?, b: Int) {
|
||||
if (key == null || locs[key.code] == b)
|
||||
if (key == null)
|
||||
return
|
||||
locs[key.code] = b
|
||||
updateFlow()
|
||||
_visitsFlow.value = _visitsFlow.value.toMutableMap().apply {
|
||||
this[key.code] = b
|
||||
}
|
||||
|
||||
private fun updateFlow() {
|
||||
_visitsFlow.value = locs.toMap()
|
||||
locs[key.code] = b
|
||||
}
|
||||
|
||||
fun deleteVisited(key: Int) {
|
||||
val keysToDelete = locs
|
||||
.filter { it.value == key }
|
||||
.map { it.key }
|
||||
if (keysToDelete.isEmpty()) return
|
||||
_visitsFlow.value = _visitsFlow.value.toMutableMap().apply {
|
||||
keysToDelete.forEach { this.remove(it)}
|
||||
}
|
||||
keysToDelete.forEach {
|
||||
locs.remove(it)
|
||||
}
|
||||
updateFlow()
|
||||
}
|
||||
|
||||
fun getVisited(key: GeoLoc): Int {
|
||||
@@ -61,19 +60,13 @@ class Visits(val id: Int, private val locs: HashMap<String, Int>) {
|
||||
}
|
||||
|
||||
fun reassignAllVisitedToGroup(group: Int) {
|
||||
var changed = false
|
||||
val keys = locs.filter { (_, grp) ->
|
||||
grp !in listOf(NO_GROUP, AUTO_GROUP)
|
||||
}.keys
|
||||
keys.forEach {
|
||||
if (locs[it] != group) {
|
||||
locs[it] = group
|
||||
changed = true
|
||||
}
|
||||
}
|
||||
if (changed) {
|
||||
updateFlow()
|
||||
}
|
||||
_visitsFlow.value = locs
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalSerializationApi::class)
|
||||
|
||||
@@ -17,6 +17,7 @@
|
||||
android:pathData="M36,36m-28,0a28,28 0,1 1,56 0a28,28 0,1 1,-56 0"
|
||||
android:strokeLineJoin="round"
|
||||
android:strokeWidth="2"
|
||||
android:fillColor="@color/blue"
|
||||
android:strokeColor="#000000"
|
||||
android:strokeLineCap="round"/>
|
||||
|
||||
|
||||
@@ -2,5 +2,5 @@
|
||||
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<background android:drawable="@drawable/ic_launcher_background"/>
|
||||
<foreground android:drawable="@drawable/ic_launcher_foreground"/>
|
||||
<monochrome android:drawable="@drawable/ic_launcher_monochrome"/>
|
||||
<monochrome android:drawable="@drawable/ic_launcher_foreground"/>
|
||||
</adaptive-icon>
|
||||
@@ -1,15 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<style name="Theme.Beans" parent="Theme.MaterialComponents.DayNight.NoActionBar">
|
||||
<!-- Status bar color in dark mode -->
|
||||
<item name="android:statusBarColor">@color/blue</item>
|
||||
<!-- Window background in dark mode -->
|
||||
<item name="android:windowBackground">@color/darkgray</item>
|
||||
<item name="android:windowLightStatusBar">false</item>
|
||||
<item name="android:windowDrawsSystemBarBackgrounds">true</item>
|
||||
|
||||
<item name="colorPrimary">@color/blue</item>
|
||||
<item name="colorPrimaryDark">@color/blue</item>
|
||||
<item name="colorAccent">@color/blue</item>
|
||||
</style>
|
||||
</resources>
|
||||
@@ -1,7 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<style name="Theme.Beans" parent="android:Theme.Material.Light.NoActionBar">
|
||||
<item name="android:statusBarColor">@color/blue</item>
|
||||
<item name="android:windowBackground">@color/blue</item>
|
||||
</style>
|
||||
</resources>
|
||||
@@ -15,21 +15,8 @@
|
||||
<string name="percentages">Prefer percentages (%)</string>
|
||||
<string name="licenses">Licenses</string>
|
||||
<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_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="key_regional">Regional</string>
|
||||
<string name="about">About</string>
|
||||
<string name="pref_category_appearance">Appearance</string>
|
||||
<string name="pref_category_features">Features</string>
|
||||
<string name="pref_category_data">Data</string>
|
||||
<string name="action_import">Import</string>
|
||||
<string name="action_export">Export</string>
|
||||
<string name="import_desc">Load data from a previously exported file.</string>
|
||||
<string name="export_desc">Save your current data to a file for backup or transfer.</string>
|
||||
<string name="key_projection_desc">Select how the map is projected onto a 2D surface.</string>
|
||||
<string name="beans_is_foss">Beans is free and open source software, licensed under the GNU General Public License (version 3 or later)</string>
|
||||
<string name="beans_repo_uri">https://github.com/helcel-net/beans</string>
|
||||
<string name="beans_repo">Project repository: %1$s\n Feel free to report issues or contribute.</string>
|
||||
|
||||
@@ -1,16 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<style name="Theme.Beans" parent="Theme.MaterialComponents.DayNight.NoActionBar">
|
||||
<!-- Status bar color -->
|
||||
<item name="android:statusBarColor">@color/blue</item>
|
||||
<!-- Window background - shows through statusBarsPadding() area -->
|
||||
<item name="android:windowBackground">@color/blue</item>
|
||||
<item name="android:windowLightStatusBar">false</item>
|
||||
<item name="android:windowDrawsSystemBarBackgrounds">true</item>
|
||||
|
||||
<!-- Optional: Primary colors for the theme -->
|
||||
<item name="colorPrimary">@color/blue</item>
|
||||
<item name="colorPrimaryDark">@color/blue</item>
|
||||
<item name="colorAccent">@color/blue</item>
|
||||
</style>
|
||||
</resources>
|
||||
+1
-1
@@ -2,5 +2,5 @@
|
||||
plugins {
|
||||
id 'com.android.application' version '9.2.1' apply false
|
||||
id 'com.android.library' version '9.2.1' apply false
|
||||
id 'org.jetbrains.kotlin.android' version '2.4.0' apply false
|
||||
id 'org.jetbrains.kotlin.android' version '2.3.21' apply false
|
||||
}
|
||||
+8
-52
@@ -1,10 +1,8 @@
|
||||
#!/bin/node
|
||||
|
||||
import {readFileSync,writeFileSync,existsSync} from 'fs'
|
||||
import {readFileSync, existsSync} from 'fs'
|
||||
import area from '@turf/area'
|
||||
import * as turf from '@turf/turf'
|
||||
import * as path from 'path';
|
||||
import { JSDOM } from 'jsdom';
|
||||
|
||||
|
||||
const countries =
|
||||
@@ -26,7 +24,7 @@ const countries =
|
||||
|
||||
|
||||
const groups = {
|
||||
"EEE":["ALA","ALB","AND","AUT","BLR","BEL","BIH","BGR","HRV","CYP","CZE","DNK","EST","FIN","FRA","DEU","GIB","GGY","GRC","HUN","ISL","IRL","IMN","JEY","ITA","KAZ","XKO","LVA","LIE","LTU","LUX","MLT","MDA","MCO","MNE","NLD","MKD","NOR","SJM","POL","PRT","ROU","RUS","SMR","SRB","SVK","SVN","ESP","SWE","CHE","UKR","GBR","VAT","XAD"],
|
||||
"EEE":["ALB","AND","AUT","BLR","BEL","BIH","BGR","HRV","CYP","CZE","DNK","EST","FIN","FRA","DEU","GRC","HUN","ISL","IRL","ITA","KAZ","XKO","LVA","LIE","LTU","LUX","MLT","MDA","MCO","MNE","NLD","MKD","NOR","POL","PRT","ROU","RUS","SMR","SRB","SVK","SVN","ESP","SWE","CHE","UKR","GBR","VAT","XAD"],
|
||||
"ABB":["AFG","ARM","AZE","BHR","BGD","BTN","BRN","KHM","CHN","GEO","HKG","IND","IDN","IRN","IRQ","ISR","JPN","JOR","KWT","KGZ","LAO","LBN","MAC","MYS","MDV","MNG","MMR","NPL","PRK","OMN","PAK","PSE","PHL","QAT","SAU","SGP","KOR","LKA","SYR","TWN","TJK","THA","TLS","TUR","TKM","ARE","UZB","VNM","YEM","ZNC"],
|
||||
"FFF":["DZA","AGO","BDI","BEN","BWA","BFA","BDI","CPV","CMR","CAF","TCD","COM","COG","COD","CIV","DJI","EGY","GNQ","ERI","SWZ","ETH","GAB","GMB","GHA","GIN","GNB","KEN","LSO","LBR","LBY","MDG","MWI","MLI","MRT","MUS","MYT","MAR","MOZ","NAM","NER","NGA","COD","REU","RWA","STP","SEN","SYC","SLE","SOM","ZAF","SSD","SHN","SDN","TZA","TGO","TUN","UGA","COD","ZMB","ZWE","ESH"],
|
||||
"NNN":["ABW","AIA","ATG","BHS","BRB","BLZ","BMU","VGB","CAN","CYM","CRI","CUB","CUW","DMA","DOM","SLV","GRL","GRD","GLP","GTM","HTI","HND","JAM","MTQ","MEX","MSR","ANT","CUW","NIC","PAN","PRI","KNA","LCA","MAF","SPM","VCT","TTO","TCA","USA","XCL"],
|
||||
@@ -34,6 +32,7 @@ const groups = {
|
||||
"UUU":["ASM","AUS","COK","FJI","PYF","GUM","KIR","MHL","FSM","NRU","NCL","NZL","NIU","NFK","MNP","PLW","PNG","PCN","SLB","TKL","TON","TUV","VUT","WLF"],
|
||||
"XXX":[
|
||||
"ATA", // Antarctica: not in any other region
|
||||
"ALA",// Åland Islands: an autonomous region of Finland, but not a member of the EU or UN
|
||||
"BES",// Bonaire, Sint Eustatius and Saba: special municipalities of the Netherlands in the Caribbean
|
||||
"BVT",// Bouvet Island: an uninhabited territory of Norway in the South Atlantic
|
||||
"IOT",// British Indian Ocean Territory: a British overseas territory in the Indian Ocean
|
||||
@@ -41,11 +40,16 @@ const groups = {
|
||||
"CCK",// Cocos (Keeling) Islands: an Australian external territory in the Indian Ocean
|
||||
"FRO",// Faroe Islands: an autonomous region of Denmark
|
||||
"ATF",// French Southern and Antarctic Lands: a territory of France located in the southern Indian Ocean
|
||||
"GIB",// Gibraltar: a British overseas territory located at the southern tip of the Iberian Peninsula
|
||||
"GGY",// Guernsey: a British Crown dependency in the English Channel
|
||||
"HMD",// Heard Island and McDonald Islands: an uninhabited Australian external territory in the southern Indian Ocean
|
||||
"IMN",// Isle of Man: a British Crown dependency located in the Irish Sea
|
||||
"JEY",// Jersey: a British Crown dependency located in the English Channel
|
||||
"BLM",// Saint Barthélemy: an overseas collectivity of France in the Caribbean
|
||||
"WSM",// Samoa: an independent island nation in the South Pacific
|
||||
"SXM",// Sint Maarten: a constituent country of the Kingdom of the Netherlands in the Caribbean
|
||||
"SGS",// South Georgia and the South Sandwich Islands: a British overseas territory in the southern Atlantic Ocean
|
||||
"SJM",// Svalbard and Jan Mayen: an archipelago administered by Norway
|
||||
"UMI",// United States Minor Outlying Islands: a collection of nine insular areas of the United States
|
||||
"VIR",// United States Virgin Islands: an unincorporated territory of the United States in the Caribbean
|
||||
]
|
||||
@@ -162,52 +166,4 @@ async function run(){
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
function fixSvg(svgPath) {
|
||||
const countryToRegion = {};
|
||||
for (const [region, countries] of Object.entries(groups)) {
|
||||
countries.forEach(country => countryToRegion[country] = region);
|
||||
}
|
||||
const absoluteInputPath = path.resolve(svgPath);
|
||||
if (!existsSync(absoluteInputPath)) {
|
||||
throw new Error(`Input file not found at: ${absoluteInputPath}`);
|
||||
}
|
||||
const svgContent = readFileSync(absoluteInputPath, 'utf8');
|
||||
const dom = new JSDOM(svgContent, { contentType: 'image/svg+xml' });
|
||||
const document = dom.window.document;
|
||||
const svgRoot = document.querySelector('svg');
|
||||
if (!svgRoot) {
|
||||
throw new Error("Invalid or empty SVG structure encountered.");
|
||||
}
|
||||
if (svgRoot.getAttribute('data-processed') === 'true') {
|
||||
console.log(`Skipping: File at "${svgPath}" has already been processed.`);
|
||||
return;
|
||||
}
|
||||
|
||||
const elementGroups = Array.from(svgRoot.querySelectorAll('g'));
|
||||
elementGroups.forEach(group => {
|
||||
const currentId = group.getAttribute('id') || '';
|
||||
const baseIsoCode = currentId.replace(/\d+$/, '');
|
||||
const regionKey = countryToRegion[baseIsoCode] || 'XXXX';
|
||||
let regionGroup = svgRoot.querySelector(`g[id="${regionKey}"]`);
|
||||
if (!regionGroup) {
|
||||
regionGroup = document.createElementNS('http://w3.org', 'g');
|
||||
regionGroup.setAttribute('id', regionKey);
|
||||
svgRoot.appendChild(regionGroup);
|
||||
}
|
||||
regionGroup.appendChild(group);
|
||||
});
|
||||
svgRoot.setAttribute('data-processed', 'true');
|
||||
const absoluteOutputPath = path.resolve(svgPath);
|
||||
let cleanXmlString = svgRoot.outerHTML;
|
||||
cleanXmlString = cleanXmlString.replace(/[\r\n]+/g, '');
|
||||
cleanXmlString = cleanXmlString.replace(/xmlns="http:\/\/w3\.org"\s?/g, '');
|
||||
cleanXmlString = cleanXmlString.replace(/xmlns="http:\/\/www\.w3\.org\/2000\/svg"\s?/g, '');
|
||||
cleanXmlString = cleanXmlString.replace('<svg', '<svg xmlns="http://www.w3.org/2000/svg"');
|
||||
writeFileSync(absoluteOutputPath, cleanXmlString, 'utf8');
|
||||
}
|
||||
|
||||
run()
|
||||
fixSvg("./app/src/main/assets/loxim01.svg")
|
||||
fixSvg("./app/src/main/assets/webmercator01.svg")
|
||||
fixSvg("./app/src/main/assets/aeqd01.svg")
|
||||
@@ -8,11 +8,10 @@ GADM_BASEPATH="https://geodata.ucdavis.edu/gadm"
|
||||
mapshaper="./node_modules/mapshaper/bin/mapshaper"
|
||||
ATA_URL="https://media.githubusercontent.com/media/wmgeolab/geoBoundaries/905b0baf5f4fb3b9ccf45293647dcacdb2b799d4/releaseData/gbOpen/ATA/ADM0/geoBoundaries-ATA-ADM0_simplified.geojson"
|
||||
|
||||
# Caspian Sea: "XCA"
|
||||
countries=(
|
||||
"AFG" "XAD" "ALA" "ALB" "DZA" "ASM" "AND" "AGO" "AIA" "ATG" "ARG" "ARM" "ABW" "AUS" "AUT" "AZE"
|
||||
"BHS" "BHR" "BGD" "BRB" "BLR" "BEL" "BLZ" "BEN" "BMU" "BTN" "BOL" "BES" "BIH" "BWA" "BVT" "BRA" "IOT" "VGB" "BRN" "BGR" "BFA" "BDI" "KHM"
|
||||
"CMR" "CAN" "CPV" "CYM" "CAF" "TCD" "CHL" "CHN" "CXR" "XCL" "CCK" "COL" "COM" "COK" "CRI" "CIV" "HRV" "CUB" "CUW" "CYP" "CZE" "COD"
|
||||
"CMR" "CAN" "CPV" "XCA" "CYM" "CAF" "TCD" "CHL" "CHN" "CXR" "XCL" "CCK" "COL" "COM" "COK" "CRI" "CIV" "HRV" "CUB" "CUW" "CYP" "CZE" "COD"
|
||||
"DNK" "DJI" "DMA" "DOM" "ECU" "EGY" "SLV" "GNQ" "ERI" "EST" "ETH" "FLK" "FRO" "FJI" "FIN" "FRA" "GUF" "PYF" "ATF"
|
||||
"GAB" "GMB" "GEO" "DEU" "GHA" "GIB" "GRC" "GRL" "GRD" "GLP" "GUM" "GTM" "GGY" "GIN" "GNB" "GUY" "HTI" "HMD" "HND" "HUN"
|
||||
"ISL" "IND" "IDN" "IRN" "IRQ" "IRL" "IMN" "ISR" "ITA" "JAM" "JPN" "JEY" "JOR" "KAZ" "KEN" "KIR" "XKO" "KWT" "KGZ"
|
||||
@@ -116,47 +115,6 @@ toSVG_1() {
|
||||
"$mapshaper" -i combine-files ${input_files[@]} -proj aeqd +lat_0=90 -simplify 0.005 weighted keep-shapes resolution=1200x1200 -o ./app/src/main/assets/aeqd1.svg svg-data=GID_0,COUNTRY,GID,NAME id-field=GID
|
||||
}
|
||||
|
||||
generate_svg_map() {
|
||||
local OUT_FILE="$1" # First argument: Output destination path
|
||||
local PROJ_ARGS="$2" # Second argument: Projection parameters
|
||||
shift 2 # Remove the first two arguments, leaving only the files
|
||||
local FILES_TO_RUN=("$@") # Capture all remaining arguments as the file array
|
||||
|
||||
echo "Generating: $OUT_FILE using projection [$PROJ_ARGS]"
|
||||
echo "Processing ${#FILES_TO_RUN[@]} files..."
|
||||
|
||||
local JS_PIPELINE_LOGIC="
|
||||
const conflicts = {
|
||||
'Z01': 'IND', 'Z04': 'IND', 'Z05': 'IND', 'Z07': 'IND', 'Z09': 'IND',
|
||||
'Z02': 'CHN', 'Z03': 'CHN','Z08': 'CHN',
|
||||
'Z06': 'PAK'
|
||||
};
|
||||
let rawCode = GID_0 || 'UNK';
|
||||
let cCode = conflicts[rawCode] ? conflicts[rawCode] : rawCode;
|
||||
let isGidMissing = (!GID || GID === 'undefined' || GID === 'null' || GID === '');
|
||||
if (isGidMissing) {
|
||||
COUNTRY_GROUP = cCode+'2';
|
||||
} else {
|
||||
COUNTRY_GROUP = cCode+'1';
|
||||
}
|
||||
"
|
||||
|
||||
"$mapshaper" -i "${FILES_TO_RUN[@]}" combine-files \
|
||||
-snap \
|
||||
-merge-layers force\
|
||||
-proj $PROJ_ARGS densify \
|
||||
-simplify 2% weighted keep-shapes \
|
||||
-filter-islands min-area=0 \
|
||||
-sort 'this.area' ascending \
|
||||
-each "$JS_PIPELINE_LOGIC" \
|
||||
-split COUNTRY_GROUP \
|
||||
-o "$OUT_FILE" \
|
||||
format=svg \
|
||||
id-field=GID \
|
||||
precision=0.1 \
|
||||
target=*
|
||||
|
||||
}
|
||||
|
||||
toSVG_01() {
|
||||
input_files=()
|
||||
@@ -178,9 +136,9 @@ toSVG_01() {
|
||||
fi
|
||||
done
|
||||
|
||||
generate_svg_map "./app/src/main/assets/loxim01.svg" "loxim" "${input_files[@]}"
|
||||
generate_svg_map "./app/src/main/assets/webmercator01.svg" "webmercator" "${input_files[@]}"
|
||||
generate_svg_map "./app/src/main/assets/aeqd01.svg" "aeqd +lat_0=90" "${input_files[@]}"
|
||||
"$mapshaper" -i combine-files ${input_files[@]} snap -proj loxim densify -simplify 0.001 weighted keep-shapes -o ./app/src/main/assets/loxim01.svg svg-data=GID_0,COUNTRY,GID,NAME id-field=GID
|
||||
"$mapshaper" -i combine-files ${input_files[@]} snap -proj webmercator densify -simplify 0.001 weighted keep-shapes -o ./app/src/main/assets/webmercator01.svg svg-data=GID_0,COUNTRY,GID,NAME id-field=GID
|
||||
"$mapshaper" -i combine-files ${input_files[@]} snap -proj aeqd +lat_0=90 densify -simplify 0.001 weighted keep-shapes -o ./app/src/main/assets/aeqd01.svg svg-data=GID_0,COUNTRY,GID,NAME id-field=GID
|
||||
}
|
||||
|
||||
do_1() {
|
||||
@@ -195,12 +153,10 @@ do_0() {
|
||||
do
|
||||
download_0 "$country"
|
||||
done
|
||||
wget -q -O "./temp/0/ATA.json" "$ATA_URL"
|
||||
wget -q -O "./temp/1/ATA.json" "$ATA_URL"
|
||||
}
|
||||
# do_0
|
||||
# do_1
|
||||
# toSVG_0
|
||||
# toSVG_1
|
||||
toSVG_01
|
||||
|
||||
#CUW, CCK, XCL, CXR, IOT, BVT, ABW, FLK, GIB, HMD, KIR, SXM, MDV, MCO, NIU, NFK, PCN, MAF, SGS, VAT
|
||||
|
||||
+2
-2
@@ -1,7 +1,7 @@
|
||||
distributionBase=GRADLE_USER_HOME
|
||||
distributionPath=wrapper/dists
|
||||
distributionSha256Sum=9c0f7faeeb306cb14e4279a3e084ca6b596894089a0638e68a07c945a32c9e14
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-9.6.1-bin.zip
|
||||
distributionSha256Sum=bafc141b619ad6350fd975fc903156dd5c151998cc8b058e8c1044ab5f7b031f
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-9.5.1-bin.zip
|
||||
networkTimeout=10000
|
||||
retries=0
|
||||
retryBackOffMs=500
|
||||
|
||||
@@ -20,7 +20,7 @@
|
||||
|
||||
##############################################################################
|
||||
#
|
||||
# gradlew start up script for POSIX generated by Gradle.
|
||||
# Gradle start up script for POSIX generated by Gradle.
|
||||
#
|
||||
# Important for running:
|
||||
#
|
||||
@@ -29,7 +29,7 @@
|
||||
# bash, then to run this script, type that shell name before the whole
|
||||
# command line, like:
|
||||
#
|
||||
# ksh gradlew
|
||||
# ksh Gradle
|
||||
#
|
||||
# Busybox and similar reduced shells will NOT work, because this script
|
||||
# requires all of these POSIX shell features:
|
||||
|
||||
Vendored
+2
-2
@@ -19,7 +19,7 @@
|
||||
@if "%DEBUG%"=="" @echo off
|
||||
@rem ##########################################################################
|
||||
@rem
|
||||
@rem gradlew startup script for Windows
|
||||
@rem Gradle startup script for Windows
|
||||
@rem
|
||||
@rem ##########################################################################
|
||||
|
||||
@@ -72,7 +72,7 @@ echo location of your Java installation. 1>&2
|
||||
|
||||
|
||||
|
||||
@rem Execute gradlew
|
||||
@rem Execute Gradle
|
||||
@rem endlocal doesn't take effect until after the line is parsed and variables are expanded
|
||||
@rem which allows us to clear the local environment before executing the java command
|
||||
endlocal & "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -jar "%APP_HOME%\gradle\wrapper\gradle-wrapper.jar" %* & call :exitWithErrorLevel
|
||||
|
||||
+1
-1
@@ -3,7 +3,7 @@
|
||||
"@turf/area": "^7.3.5",
|
||||
"@turf/turf": "^7.3.5",
|
||||
"jsdom": "^29.1.1",
|
||||
"mapshaper": "^0.7.22"
|
||||
"mapshaper": "^0.7.19"
|
||||
},
|
||||
"type": "module"
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user