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: refactor:
(done) extract file opener from settings activity (done) extract file opener from settings activity
(done) extract zip parser 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: fix:
app rotation with placed point app rotation with placed point
ui: ui:
proper file loading menu (done) proper file loading menu
translation usage translation usage
test: test:
khm: 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 { class FileLoader {
companion object { companion object {
var filePath: MutableState<String?> = mutableStateOf(null) var filePath: MutableState<String?> = mutableStateOf(null)
val launcherActive = mutableStateOf(false)
fun filePickerResult(result: FilePickerResult): String? { fun filePickerResult(result: FilePickerResult): String? {
launcherActive.value = false
if (result.errorMessage != null) { if (result.errorMessage != null) {
Log.e(TAG, result.errorMessage!!) Log.e(TAG, result.errorMessage!!)
return null 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.rememberTextMeasurer
import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.text.withStyle 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 com.mirenkov.ktheightmap.parser.KhmParser
import kotlin.math.absoluteValue import kotlin.math.absoluteValue
import kotlin.math.ceil import kotlin.math.ceil
import kotlin.math.floor
import kotlin.math.min import kotlin.math.min
import kotlin.math.pow import kotlin.math.pow
import kotlin.math.sqrt 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.fillMaxWidth
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.material3.Button import androidx.compose.material3.Button
import androidx.compose.material3.CircularProgressIndicator
import androidx.compose.material3.HorizontalDivider import androidx.compose.material3.HorizontalDivider
import androidx.compose.material3.Scaffold import androidx.compose.material3.Scaffold
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalContext
@ -32,6 +35,8 @@ import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp import androidx.compose.ui.unit.sp
import androidx.lifecycle.viewmodel.compose.LocalViewModelStoreOwner import androidx.lifecycle.viewmodel.compose.LocalViewModelStoreOwner
import androidx.lifecycle.viewmodel.compose.viewModel 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.KhmParser
import com.mirenkov.ktheightmap.parser.SasJPEGParser import com.mirenkov.ktheightmap.parser.SasJPEGParser
import com.mirenkov.ktheightmap.parser.SasSQLiteParser import com.mirenkov.ktheightmap.parser.SasSQLiteParser
@ -66,50 +71,78 @@ class SettingsActivity : ComponentActivity() {
fun SettingsMain(vm: TileViewModel, launcher: ActivityResultLauncher<DocumentFilePickerConfig?>) { fun SettingsMain(vm: TileViewModel, launcher: ActivityResultLauncher<DocumentFilePickerConfig?>) {
val coroutineScope = rememberCoroutineScope() val coroutineScope = rememberCoroutineScope()
val filePath by remember { FileLoader.filePath } 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 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 loadToast = Toast.makeText(ctx, "File successfully loaded.", Toast.LENGTH_LONG)
val clearTilesToast = Toast.makeText(ctx, "Tiles removed from database.", 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 clearHeightToast = Toast.makeText(ctx, "Height data removed.", Toast.LENGTH_LONG)
val selectedFileText = if (pathIsNotNull) filePath!!.split('/').last() else "none"
val fileSelectorRow = @Composable () { val fileSelectorRow = @Composable () {
Row(Modifier.fillMaxWidth()) { Row(Modifier.fillMaxWidth()) {
Button({ launcher.launch(null) }) { Text("Select") } Button({ launcher.launch(null); launcherActive = true }) { Text("Select") }
Text("Selected file: none", Modifier.padding(8.dp, 0.dp).align(Alignment.CenterVertically)) Text("Selected file: $selectedFileText", Modifier.padding(8.dp, 0.dp).align(Alignment.CenterVertically))
} }
} }
KtHeightMapTheme { KtHeightMapTheme {
Scaffold { paddingValues -> Scaffold(floatingActionButton = { if (showLoading || launcherActive) CircularProgressIndicator() }) { paddingValues ->
Column(Modifier.padding(paddingValues)) { Column(Modifier.padding(paddingValues)) {
LabeledSection("Select file:", Modifier.padding(24.dp, 8.dp)) { LabeledSection("Select file:", Modifier.padding(24.dp, 8.dp)) {
fileSelectorRow() fileSelectorRow()
} }
LabeledSection("Load file as...", Modifier.padding(24.dp, 8.dp)) { LabeledSection("Load file as...", Modifier.padding(24.dp, 8.dp)) {
Button({ filePath?.let { Button({ filePath?.let {
showLoading = true
coroutineScope.launch(Dispatchers.IO) { coroutineScope.launch(Dispatchers.IO) {
SasSQLiteParser.processZip(it, vm.repository, ctx) 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 { Button({ filePath?.let {
showLoading = true
coroutineScope.launch(Dispatchers.IO) { coroutineScope.launch(Dispatchers.IO) {
SasJPEGParser.processZip(it, vm.repository) 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 { Button({ filePath?.let {
KhmParser.load(it, ctx) showLoading = true
loadToast.show() coroutineScope.launch(Dispatchers.IO) {
} }) { Text("Height data (.khm)") } 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)) { LabeledSection("Clear...", Modifier.padding(24.dp, 8.dp)) {
Button({ coroutineScope.launch { Button({ coroutineScope.launch {
showLoading = true
vm.repository.clearTiles() vm.repository.clearTiles()
clearTilesToast.show() enableClearTiles = false
} }) { Text("Tile data") } coroutineScope.launch(Dispatchers.Main) { clearTilesToast.show(); showLoading = false }
} }, enabled = enableClearTiles) { Text("Tile data") }
Button({ Button({
KhmParser.clear(ctx) KhmParser.clear(ctx)
enableClearHeight = false
clearHeightToast.show() 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 { fun getMaxLevel(): Int {
return coroutineScope.future(Dispatchers.IO){ tileDao.maxLevel() }.join() ?: 2 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.runtime.mutableStateOf
import androidx.compose.ui.text.input.TextFieldValue import androidx.compose.ui.text.input.TextFieldValue
import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModel
import com.mirenkov.ktheightmap.Config.Companion.MAP_START_OFFSET_X import com.mirenkov.ktheightmap.Global.Companion.MAP_START_OFFSET_X
import com.mirenkov.ktheightmap.Config.Companion.MAP_START_OFFSET_Y import com.mirenkov.ktheightmap.Global.Companion.MAP_START_OFFSET_Y
class TileViewModel(application: Application): ViewModel() { class TileViewModel(application: Application): ViewModel() {
val repository: TileRepository val repository: TileRepository

View file

@ -3,6 +3,7 @@ package com.mirenkov.ktheightmap.parser
import android.content.Context import android.content.Context
import java.io.DataInputStream import java.io.DataInputStream
import java.io.FileInputStream import java.io.FileInputStream
import kotlin.math.abs
import kotlin.math.max import kotlin.math.max
import kotlin.math.min import kotlin.math.min
@ -21,27 +22,48 @@ class KhmParser {
const val HEIGHT_FILE: String = "height.khm" const val HEIGHT_FILE: String = "height.khm"
fun load(filePath: String, ctx: Context) { fun load(filePath: String, ctx: Context) {
if (ctx.getFileStreamPath(HEIGHT_FILE).exists()) { if (fileExists(ctx)) {
return return
} }
val inp = FileInputStream(filePath) 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() inp.close()
} }
fun isFileValid(filePath: String): Boolean {
FileInputStream(filePath).use {
return checkHeader(it)
}
}
fun clear(ctx: Context) { fun clear(ctx: Context) {
if (ctx.getFileStreamPath(HEIGHT_FILE).exists()) { if (fileExists(ctx)) {
assert(ctx.deleteFile(HEIGHT_FILE)) 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 { private fun getOffset(header: HeightInfo, x: Int, y: Int): Int {
return (x * header.width + y) * 2 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 { private fun readHeader(dis: DataInputStream): HeightInfo {
if (header == null) { if (header == null) {
header = with(dis) { header = with(dis) {
@ -70,7 +92,7 @@ class KhmParser {
} }
fun getHeight(lon: Float, lat: Float, ctx: Context): UShort { fun getHeight(lon: Float, lat: Float, ctx: Context): UShort {
if (!ctx.getFileStreamPath(HEIGHT_FILE).exists()) if (!fileExists(ctx))
return 0u.toUShort() return 0u.toUShort()
val dis = DataInputStream(ctx.openFileInput(HEIGHT_FILE)) val dis = DataInputStream(ctx.openFileInput(HEIGHT_FILE))
dis.use { dis.use {