From 9ceb3787c2d47944a6b2cbc8f62cac4354412eea Mon Sep 17 00:00:00 2001 From: 2ndbeam <2ndbeam@disroot.org> Date: Tue, 11 Nov 2025 15:48:17 +0300 Subject: [PATCH] Settings menu completed --- TODO | 4 +- .../java/com/mirenkov/ktheightmap/Config.kt | 8 ---- .../com/mirenkov/ktheightmap/FileLoader.kt | 2 + .../java/com/mirenkov/ktheightmap/Global.kt | 32 +++++++++++++ .../com/mirenkov/ktheightmap/MapCanvas.kt | 3 -- .../mirenkov/ktheightmap/SettingsActivity.kt | 45 +++++++++++++------ .../com/mirenkov/ktheightmap/TileViewModel.kt | 4 +- .../mirenkov/ktheightmap/parser/KhmParser.kt | 24 ++++++++-- 8 files changed, 90 insertions(+), 32 deletions(-) delete mode 100644 app/src/main/java/com/mirenkov/ktheightmap/Config.kt create mode 100644 app/src/main/java/com/mirenkov/ktheightmap/Global.kt diff --git a/TODO b/TODO index 70eb2e3..f67a3ca 100644 --- a/TODO +++ b/TODO @@ -3,11 +3,11 @@ functionality: refactor: (done) extract file opener from settings activity (done) extract zip parser from settings activity - make use of Config.kt (maybe move consts here?) + (done) make use of Config.kt fix: app rotation with placed point ui: - proper file loading menu + (done) proper file loading menu translation usage test: khm: diff --git a/app/src/main/java/com/mirenkov/ktheightmap/Config.kt b/app/src/main/java/com/mirenkov/ktheightmap/Config.kt deleted file mode 100644 index 4d04d8b..0000000 --- a/app/src/main/java/com/mirenkov/ktheightmap/Config.kt +++ /dev/null @@ -1,8 +0,0 @@ -package com.mirenkov.ktheightmap - -class Config { - companion object { - const val MAP_START_OFFSET_X = -646.65625F - const val MAP_START_OFFSET_Y = -1157.2814F - } -} \ No newline at end of file diff --git a/app/src/main/java/com/mirenkov/ktheightmap/FileLoader.kt b/app/src/main/java/com/mirenkov/ktheightmap/FileLoader.kt index 73a4326..4566413 100644 --- a/app/src/main/java/com/mirenkov/ktheightmap/FileLoader.kt +++ b/app/src/main/java/com/mirenkov/ktheightmap/FileLoader.kt @@ -13,7 +13,9 @@ import com.nareshchocha.filepickerlibrary.models.FilePickerResult class FileLoader { companion object { var filePath: MutableState = mutableStateOf(null) + val launcherActive = mutableStateOf(false) fun filePickerResult(result: FilePickerResult): String? { + launcherActive.value = false if (result.errorMessage != null) { Log.e(TAG, result.errorMessage!!) return null diff --git a/app/src/main/java/com/mirenkov/ktheightmap/Global.kt b/app/src/main/java/com/mirenkov/ktheightmap/Global.kt new file mode 100644 index 0000000..42d8821 --- /dev/null +++ b/app/src/main/java/com/mirenkov/ktheightmap/Global.kt @@ -0,0 +1,32 @@ +package com.mirenkov.ktheightmap + +import android.util.Log +import java.io.FileInputStream +import java.util.zip.ZipEntry +import java.util.zip.ZipException +import java.util.zip.ZipInputStream + +class Global { + companion object { + const val MAP_START_OFFSET_X = -646.65625F + const val MAP_START_OFFSET_Y = -1157.2814F + + fun fileIsValidZip(file: String): Boolean { + ZipInputStream(FileInputStream(file)).use { + return it.nextEntry != null + } + } + + fun zipContainsSQLite(file: String): Boolean { + ZipInputStream(FileInputStream(file)).use { + var entry: ZipEntry? = it.nextEntry + while (entry != null) { + if (entry.name.endsWith(".sqlitedb")) + return true + entry = it.nextEntry + } + } + return false + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/mirenkov/ktheightmap/MapCanvas.kt b/app/src/main/java/com/mirenkov/ktheightmap/MapCanvas.kt index d691183..5134a03 100644 --- a/app/src/main/java/com/mirenkov/ktheightmap/MapCanvas.kt +++ b/app/src/main/java/com/mirenkov/ktheightmap/MapCanvas.kt @@ -29,12 +29,9 @@ import androidx.compose.ui.text.drawText import androidx.compose.ui.text.rememberTextMeasurer import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.text.withStyle -import com.mirenkov.ktheightmap.Config.Companion.MAP_START_OFFSET_X -import com.mirenkov.ktheightmap.Config.Companion.MAP_START_OFFSET_Y import com.mirenkov.ktheightmap.parser.KhmParser import kotlin.math.absoluteValue import kotlin.math.ceil -import kotlin.math.floor import kotlin.math.min import kotlin.math.pow import kotlin.math.sqrt diff --git a/app/src/main/java/com/mirenkov/ktheightmap/SettingsActivity.kt b/app/src/main/java/com/mirenkov/ktheightmap/SettingsActivity.kt index b1a7f57..873c328 100644 --- a/app/src/main/java/com/mirenkov/ktheightmap/SettingsActivity.kt +++ b/app/src/main/java/com/mirenkov/ktheightmap/SettingsActivity.kt @@ -13,6 +13,7 @@ import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding import androidx.compose.material3.Button +import androidx.compose.material3.CircularProgressIndicator import androidx.compose.material3.HorizontalDivider import androidx.compose.material3.Scaffold import androidx.compose.material3.Text @@ -34,6 +35,8 @@ import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import androidx.lifecycle.viewmodel.compose.LocalViewModelStoreOwner import androidx.lifecycle.viewmodel.compose.viewModel +import com.mirenkov.ktheightmap.Global.Companion.fileIsValidZip +import com.mirenkov.ktheightmap.Global.Companion.zipContainsSQLite import com.mirenkov.ktheightmap.parser.KhmParser import com.mirenkov.ktheightmap.parser.SasJPEGParser import com.mirenkov.ktheightmap.parser.SasSQLiteParser @@ -68,8 +71,16 @@ class SettingsActivity : ComponentActivity() { fun SettingsMain(vm: TileViewModel, launcher: ActivityResultLauncher) { val coroutineScope = rememberCoroutineScope() + val filePath by remember { FileLoader.filePath } val pathIsNotNull = filePath != null + val pathIsValidKhm = pathIsNotNull && KhmParser.isFileValid(filePath!!) + val pathIsValidZip = pathIsNotNull && fileIsValidZip(filePath!!) + val pathContainsSQLite = pathIsValidZip && zipContainsSQLite(filePath!!) + + var launcherActive by remember { FileLoader.launcherActive } + var showLoading by remember { mutableStateOf(false) } + val ctx = LocalContext.current var enableClearTiles by remember { mutableStateOf(vm.repository.containsTiles()) } @@ -82,44 +93,50 @@ fun SettingsMain(vm: TileViewModel, launcher: ActivityResultLauncher + Scaffold(floatingActionButton = { if (showLoading || launcherActive) CircularProgressIndicator() }) { paddingValues -> Column(Modifier.padding(paddingValues)) { LabeledSection("Select file:", Modifier.padding(24.dp, 8.dp)) { fileSelectorRow() } LabeledSection("Load file as...", Modifier.padding(24.dp, 8.dp)) { Button({ filePath?.let { + showLoading = true coroutineScope.launch(Dispatchers.IO) { SasSQLiteParser.processZip(it, vm.repository, ctx) enableClearTiles = true - coroutineScope.launch(Dispatchers.Main) { loadToast.show() } + coroutineScope.launch(Dispatchers.Main) { loadToast.show(); showLoading = false } } - } }, enabled = pathIsNotNull) { Text("SQLite (.zip)") } + } }, enabled = pathContainsSQLite) { Text("SQLite (.zip)") } Button({ filePath?.let { + showLoading = true coroutineScope.launch(Dispatchers.IO) { SasJPEGParser.processZip(it, vm.repository) enableClearTiles = true - coroutineScope.launch(Dispatchers.Main) { loadToast.show() } + coroutineScope.launch(Dispatchers.Main) { loadToast.show(); showLoading = false } } - } }, enabled = pathIsNotNull) { Text("JPEG (.zip)") } + } }, enabled = pathIsValidZip && !pathContainsSQLite) { Text("JPEG (.zip)") } Button({ filePath?.let { - KhmParser.load(it, ctx) - enableClearHeight = true - loadToast.show() - } }, enabled = pathIsNotNull) { Text("Height data (.khm)") } + showLoading = true + coroutineScope.launch(Dispatchers.IO) { + KhmParser.load(it, ctx) + enableClearHeight = true + coroutineScope.launch(Dispatchers.Main) { loadToast.show(); showLoading = false } + } + } }, enabled = pathIsValidKhm) { Text("Height data (.khm)") } } LabeledSection("Clear...", Modifier.padding(24.dp, 8.dp)) { Button({ coroutineScope.launch { + showLoading = true vm.repository.clearTiles() enableClearTiles = false - clearTilesToast.show() + coroutineScope.launch(Dispatchers.Main) { clearTilesToast.show(); showLoading = false } } }, enabled = enableClearTiles) { Text("Tile data") } Button({ KhmParser.clear(ctx) diff --git a/app/src/main/java/com/mirenkov/ktheightmap/TileViewModel.kt b/app/src/main/java/com/mirenkov/ktheightmap/TileViewModel.kt index 9d35050..2bb07e7 100644 --- a/app/src/main/java/com/mirenkov/ktheightmap/TileViewModel.kt +++ b/app/src/main/java/com/mirenkov/ktheightmap/TileViewModel.kt @@ -7,8 +7,8 @@ import androidx.compose.runtime.mutableIntStateOf import androidx.compose.runtime.mutableStateOf import androidx.compose.ui.text.input.TextFieldValue import androidx.lifecycle.ViewModel -import com.mirenkov.ktheightmap.Config.Companion.MAP_START_OFFSET_X -import com.mirenkov.ktheightmap.Config.Companion.MAP_START_OFFSET_Y +import com.mirenkov.ktheightmap.Global.Companion.MAP_START_OFFSET_X +import com.mirenkov.ktheightmap.Global.Companion.MAP_START_OFFSET_Y class TileViewModel(application: Application): ViewModel() { val repository: TileRepository diff --git a/app/src/main/java/com/mirenkov/ktheightmap/parser/KhmParser.kt b/app/src/main/java/com/mirenkov/ktheightmap/parser/KhmParser.kt index bcbf32a..e22cf5b 100644 --- a/app/src/main/java/com/mirenkov/ktheightmap/parser/KhmParser.kt +++ b/app/src/main/java/com/mirenkov/ktheightmap/parser/KhmParser.kt @@ -3,6 +3,7 @@ package com.mirenkov.ktheightmap.parser import android.content.Context import java.io.DataInputStream import java.io.FileInputStream +import kotlin.math.abs import kotlin.math.max import kotlin.math.min @@ -26,12 +27,20 @@ class KhmParser { } val inp = FileInputStream(filePath) - ctx.openFileOutput(HEIGHT_FILE, Context.MODE_PRIVATE).use { - it.write(inp.readBytes()) - } + + if (checkHeader(inp)) + ctx.openFileOutput(HEIGHT_FILE, Context.MODE_PRIVATE).use { + it.write(inp.readBytes()) + } inp.close() } + fun isFileValid(filePath: String): Boolean { + FileInputStream(filePath).use { + return checkHeader(it) + } + } + fun clear(ctx: Context) { if (fileExists(ctx)) { assert(ctx.deleteFile(HEIGHT_FILE)) @@ -46,6 +55,15 @@ class KhmParser { return (x * header.width + y) * 2 } + // NOTE: this function only checks if values are in accepted ranges + private fun checkHeader(fis: FileInputStream): Boolean { + header = null + readHeader(DataInputStream(fis)) + return abs(header!!.latPerValue) < abs(header!!.minLat) && + abs(header!!.lonPerValue) < abs(header!!.minLon) && + header!!.width > 0 && header!!.height > 0 + } + private fun readHeader(dis: DataInputStream): HeightInfo { if (header == null) { header = with(dis) {