Compare commits

..

6 commits

Author SHA1 Message Date
b70b98feb6 fixed no data crashes 2025-09-30 14:44:28 +03:00
3510da0428 prettifying 2025-09-30 14:27:50 +03:00
004b498ee9 distance to point measurer 2025-09-30 14:15:09 +03:00
049311ec9b distance measurer 2025-09-30 13:27:56 +03:00
182f0b29b0 automated scale range setting 2025-09-30 12:34:04 +03:00
d8139f2140 hide zeroed values 2025-09-30 12:08:46 +03:00
6 changed files with 95 additions and 20 deletions

View file

@ -63,6 +63,8 @@ class KhmParser {
}
fun getHeight(lon: Float, lat: Float, ctx: Context): UShort {
if (!ctx.getFileStreamPath(HEIGHT_FILE).exists())
return 0u.toUShort()
val dis = DataInputStream(ctx.openFileInput(HEIGHT_FILE))
dis.use {
val header = readHeader(dis)
@ -80,7 +82,7 @@ class KhmParser {
@OptIn(ExperimentalUnsignedTypes::class)
fun getHeightsMul(ctx: Context, coords: Array<Pair<Float, Float>>): Pair<UShortArray, Array<Pair<Float, Float>>> {
if (coords.isEmpty()) return Pair(ushortArrayOf(), coords)
if (coords.isEmpty() || !ctx.getFileStreamPath(HEIGHT_FILE).exists()) return Pair(ushortArrayOf(), coords)
val dis = DataInputStream(ctx.openFileInput(HEIGHT_FILE))
dis.use {
val header = readHeader(dis)

View file

@ -3,6 +3,7 @@ package com.mirenkov.ktheightmap
import android.app.Application
import android.content.Intent
import android.os.Bundle
import android.util.Log
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.activity.enableEdgeToEdge
@ -84,6 +85,7 @@ class MainActivity : ComponentActivity() {
@Composable
fun Main(vm: TileViewModel = viewModel()) {
var scale by rememberSaveable { vm.scale }
val maxScale by rememberSaveable { vm.maxLevel }
val coroutineScope = rememberCoroutineScope()
val tileContainer = TileContainer(vm, coroutineScope)
KtHeightMapTheme {
@ -111,7 +113,7 @@ fun Main(vm: TileViewModel = viewModel()) {
Slider(
value = scale,
onValueChange = { scale = it },
valueRange = 1F..14F,
valueRange = 2F..maxScale.toFloat(),
modifier = Modifier.align(Alignment.CenterStart)
)
}

View file

@ -18,6 +18,7 @@ import androidx.compose.ui.graphics.asImageBitmap
import androidx.compose.ui.graphics.drawscope.Stroke
import androidx.compose.ui.input.pointer.pointerInput
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.text.AnnotatedString
import androidx.compose.ui.text.ParagraphStyle
import androidx.compose.ui.text.SpanStyle
import androidx.compose.ui.text.buildAnnotatedString
@ -30,6 +31,19 @@ import kotlin.math.floor
import kotlin.math.pow
import kotlin.math.sqrt
fun distanceString(targetMeters: Int): AnnotatedString {
return buildAnnotatedString {
withStyle(ParagraphStyle(textAlign = TextAlign.Center)) {
withStyle(SpanStyle(color = Color.White)) {
val text = if (targetMeters >= 100000)
"${targetMeters / 1000}km"
else
"${targetMeters}m"
append(text)
}
}
}
}
@OptIn(ExperimentalUnsignedTypes::class)
@Composable
fun MapCanvas(
@ -49,6 +63,7 @@ fun MapCanvas(
var pointLat by rememberSaveable { viewModel.rememberedPointLat }
var pointLon by rememberSaveable { viewModel.rememberedPointLon }
var invokeHeightCalc by remember { mutableStateOf(false) }
val startTargetMeters = 64 * 1000 * 1000
Canvas(
modifier = modifier.fillMaxSize()
.pointerInput(Unit) {
@ -93,6 +108,10 @@ fun MapCanvas(
}
}
val targetMeters = startTargetMeters shr level
val targetPixels = (targetMeters).toFloat() / SphereMercator.scaledMetersPerPixel(level)
val measurerHeight = 24F
val centerTileX = (1 + (offsetX + halvedX) / TILE_SIZE).toDouble()
val centerTileY = (1 + (offsetY + halvedY) / TILE_SIZE).toDouble()
@ -184,6 +203,7 @@ fun MapCanvas(
}
}
}
// Placed point and line to center
if (pointLat != 0F) {
val pointOffsetX = SphereMercator.mercateLon(pointLon.toDouble(), level, -TILE_SIZE.toDouble()).toFloat() - offsetX
@ -198,12 +218,13 @@ fun MapCanvas(
start = Offset(pointOffsetX, pointOffsetY),
end = Offset(halvedX, halvedY)
)
val startHeight = KhmParser.getHeight(pointLon, pointLat, ctx)
if (pointOffsetX >= 0 && pointOffsetY >= 0 && pointOffsetX < size.width && pointOffsetY < size.height)
drawText(
textMeasurer = textMeasurer,
text = buildAnnotatedString { withStyle(SpanStyle(color = Color.White)) {
append("${startHeight}m")
append("${startHeight}m")
} },
topLeft = Offset(pointOffsetX, pointOffsetY - 32)
)
@ -228,7 +249,7 @@ fun MapCanvas(
val stepLon = coords[step].first
val stepOffsetX = SphereMercator.mercateLon(stepLon.toDouble(), level, -TILE_SIZE.toDouble()).toFloat() - offsetX
val stepOffsetY = SphereMercator.mercateLat(stepLat.toDouble(), level, -TILE_SIZE.toDouble()).toFloat() - offsetY
if (stepOffsetX >= 0 && stepOffsetY >= 0 && stepOffsetX < size.width && stepOffsetY < size.height)
if (stepOffsetX >= 0 && stepOffsetY >= 0 && stepOffsetX < size.width && stepOffsetY < size.height && heights[step] > 0u)
drawRect(
color = if (heights[step] > startHeight) Color.Red else Color.Green,
size = Size(8F, 8F),
@ -236,22 +257,49 @@ fun MapCanvas(
)
}
}
val lineLength = sqrt((pointOffsetX - halvedX).pow(2) + (pointOffsetY - halvedY).pow(2))
val lineMeters = (lineLength * SphereMercator.scaledMetersPerPixel(level + 1)).toInt()
drawText(
textMeasurer = textMeasurer,
text = distanceString(lineMeters),
size = Size(160F, 48F),
topLeft = Offset(
halvedX - 80F,
halvedY - 80F
)
)
}
// Cursor path
val path = Path()
path.moveTo(halvedX - crossRadius, halvedY)
path.lineTo(halvedX + crossRadius, halvedY)
path.moveTo(halvedX, halvedY - crossRadius)
path.lineTo(halvedX, halvedY + crossRadius)
path.close()
val cursorPath = Path()
with(cursorPath) {
moveTo(halvedX - crossRadius, halvedY)
lineTo(halvedX + crossRadius, halvedY)
moveTo(halvedX, halvedY - crossRadius)
lineTo(halvedX, halvedY + crossRadius)
close()
}
// Cursor
drawPath(
path,
cursorPath,
Color.White,
style = Stroke(width = 6F)
)
// Height under cursor
KhmParser.getHeight(lon, lat, ctx).let {
if (it < 1u) return@let
drawText(
textMeasurer = textMeasurer,
text = buildAnnotatedString {
withStyle(SpanStyle(color = Color.White)) {
append("${it}m")
}
},
topLeft = Offset(halvedX, halvedY + crossRadius)
)
}
// Info box
drawRect(
@ -259,7 +307,6 @@ fun MapCanvas(
size = latLonSize,
topLeft = latLonOffset
)
// Info box content
drawText(
textMeasurer = textMeasurer,
@ -277,15 +324,29 @@ fun MapCanvas(
topLeft = latLonOffset
)
// Height under cursor
// Distance measurer path
val measurerPath = Path()
with(measurerPath) {
moveTo((halvedX - targetPixels).toFloat(), 8F)
relativeLineTo(0F, measurerHeight)
relativeMoveTo(targetPixels.toFloat() * 2F, 0F)
relativeLineTo(0F, -measurerHeight)
relativeMoveTo(0F, measurerHeight / 2F)
relativeLineTo(-2F * targetPixels.toFloat(), 0F)
close()
}
// Distance measurer
drawPath(
measurerPath,
Color.White,
style = Stroke(width = 6F)
)
// Distance measurer text
drawText(
textMeasurer = textMeasurer,
text = buildAnnotatedString {
withStyle(SpanStyle(color = Color.White)) {
append("${KhmParser.getHeight(lon, lat, ctx)}m")
}
},
topLeft = Offset(halvedX + crossRadius, halvedY + crossRadius)
text = distanceString(targetMeters),
topLeft = Offset(targetPixels.toFloat() * 2F, measurerHeight),
size = Size(targetPixels.toFloat() * 2F, 48F)
)
}
}

View file

@ -14,4 +14,7 @@ interface TileDao {
@Query("delete from tiles")
fun clearTiles()
@Query("select max(level) from tiles")
fun maxLevel(): Int?
}

View file

@ -21,4 +21,8 @@ class TileRepository(private val tileDao: TileDao) {
fun clearTiles() {
coroutineScope.launch(Dispatchers.IO) { tileDao.clearTiles() }
}
fun getMaxLevel(): Int {
return coroutineScope.future(Dispatchers.IO){ tileDao.maxLevel() }.join() ?: 2
}
}

View file

@ -2,6 +2,7 @@ package com.mirenkov.ktheightmap
import android.app.Application
import androidx.compose.runtime.mutableFloatStateOf
import androidx.compose.runtime.mutableIntStateOf
import androidx.compose.runtime.mutableStateOf
import androidx.compose.ui.text.input.TextFieldValue
import androidx.lifecycle.ViewModel
@ -17,7 +18,8 @@ class TileViewModel(application: Application): ViewModel() {
val mapOffsetX = mutableFloatStateOf(-646.65625F)
val mapOffsetY = mutableFloatStateOf(-1157.2814F)
val scale = mutableFloatStateOf(1F)
val scale = mutableFloatStateOf(2F)
var maxLevel = mutableIntStateOf(2)
var halvedOffsetX: Float? = null
var halvedOffsetY: Float? = null
@ -29,5 +31,6 @@ class TileViewModel(application: Application): ViewModel() {
val tileDb = TileDB.getInstance(application)
val tileDao = tileDb.tileDao()
repository = TileRepository(tileDao)
maxLevel.intValue = repository.getMaxLevel()
}
}