Compare commits

..

2 commits

Author SHA1 Message Date
9ceb3787c2 Settings menu completed 2025-11-11 15:48:17 +03:00
d30721cdf6 Update load/clear buttons on data availability change 2025-11-11 13:25:50 +03:00
9 changed files with 118 additions and 36 deletions

4
TODO
View file

@ -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:

View file

@ -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
}
}

View file

@ -13,7 +13,9 @@ import com.nareshchocha.filepickerlibrary.models.FilePickerResult
class FileLoader {
companion object {
var filePath: MutableState<String?> = 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

View file

@ -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
}
}
}

View file

@ -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

View file

@ -13,13 +13,16 @@ 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
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
@ -32,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
@ -66,50 +71,78 @@ class SettingsActivity : ComponentActivity() {
fun SettingsMain(vm: TileViewModel, launcher: ActivityResultLauncher<DocumentFilePickerConfig?>) {
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()) }
var enableClearHeight by remember { mutableStateOf(KhmParser.fileExists(ctx)) }
val loadToast = Toast.makeText(ctx, "File successfully loaded.", Toast.LENGTH_LONG)
val clearTilesToast = Toast.makeText(ctx, "Tiles removed from database.", Toast.LENGTH_LONG)
val clearHeightToast = Toast.makeText(ctx, "Height data removed.", Toast.LENGTH_LONG)
val selectedFileText = if (pathIsNotNull) filePath!!.split('/').last() else "none"
val fileSelectorRow = @Composable () {
Row(Modifier.fillMaxWidth()) {
Button({ launcher.launch(null) }) { Text("Select") }
Text("Selected file: none", Modifier.padding(8.dp, 0.dp).align(Alignment.CenterVertically))
Button({ launcher.launch(null); launcherActive = true }) { Text("Select") }
Text("Selected file: $selectedFileText", Modifier.padding(8.dp, 0.dp).align(Alignment.CenterVertically))
}
}
KtHeightMapTheme {
Scaffold { paddingValues ->
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)
coroutineScope.launch(Dispatchers.Main) { loadToast.show() }
enableClearTiles = true
coroutineScope.launch(Dispatchers.Main) { loadToast.show(); showLoading = false }
}
} }) { Text("SQLite (.zip)") }
} }, enabled = pathContainsSQLite) { Text("SQLite (.zip)") }
Button({ filePath?.let {
showLoading = true
coroutineScope.launch(Dispatchers.IO) {
SasJPEGParser.processZip(it, vm.repository)
coroutineScope.launch(Dispatchers.Main) { loadToast.show() }
enableClearTiles = true
coroutineScope.launch(Dispatchers.Main) { loadToast.show(); showLoading = false }
}
} }) { Text("JPEG (.zip)") }
} }, enabled = pathIsValidZip && !pathContainsSQLite) { Text("JPEG (.zip)") }
Button({ filePath?.let {
showLoading = true
coroutineScope.launch(Dispatchers.IO) {
KhmParser.load(it, ctx)
loadToast.show()
} }) { Text("Height data (.khm)") }
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()
clearTilesToast.show()
} }) { Text("Tile data") }
enableClearTiles = false
coroutineScope.launch(Dispatchers.Main) { clearTilesToast.show(); showLoading = false }
} }, enabled = enableClearTiles) { Text("Tile data") }
Button({
KhmParser.clear(ctx)
enableClearHeight = false
clearHeightToast.show()
}) { Text("Height data") }
}, enabled = enableClearHeight) { Text("Height data") }
}
}
}

View file

@ -25,4 +25,8 @@ class TileRepository(private val tileDao: TileDao) {
fun getMaxLevel(): Int {
return coroutineScope.future(Dispatchers.IO){ tileDao.maxLevel() }.join() ?: 2
}
fun containsTiles(): Boolean {
return coroutineScope.future(Dispatchers.IO){ tileDao.maxLevel() }.join() != null
}
}

View file

@ -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

View file

@ -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
@ -21,27 +22,48 @@ class KhmParser {
const val HEIGHT_FILE: String = "height.khm"
fun load(filePath: String, ctx: Context) {
if (ctx.getFileStreamPath(HEIGHT_FILE).exists()) {
if (fileExists(ctx)) {
return
}
val inp = FileInputStream(filePath)
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 (ctx.getFileStreamPath(HEIGHT_FILE).exists()) {
if (fileExists(ctx)) {
assert(ctx.deleteFile(HEIGHT_FILE))
}
}
fun fileExists(ctx: Context): Boolean {
return (ctx.getFileStreamPath(HEIGHT_FILE).exists())
}
private fun getOffset(header: HeightInfo, x: Int, y: Int): Int {
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) {
@ -70,7 +92,7 @@ class KhmParser {
}
fun getHeight(lon: Float, lat: Float, ctx: Context): UShort {
if (!ctx.getFileStreamPath(HEIGHT_FILE).exists())
if (!fileExists(ctx))
return 0u.toUShort()
val dis = DataInputStream(ctx.openFileInput(HEIGHT_FILE))
dis.use {