diff --git a/app/src/main/java/com/mirenkov/ktheightmap/KhmParser.kt b/app/src/main/java/com/mirenkov/ktheightmap/KhmParser.kt index eae616e..6081679 100644 --- a/app/src/main/java/com/mirenkov/ktheightmap/KhmParser.kt +++ b/app/src/main/java/com/mirenkov/ktheightmap/KhmParser.kt @@ -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>> { - 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) diff --git a/app/src/main/java/com/mirenkov/ktheightmap/MainActivity.kt b/app/src/main/java/com/mirenkov/ktheightmap/MainActivity.kt index ce22994..3db792a 100644 --- a/app/src/main/java/com/mirenkov/ktheightmap/MainActivity.kt +++ b/app/src/main/java/com/mirenkov/ktheightmap/MainActivity.kt @@ -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) ) } diff --git a/app/src/main/java/com/mirenkov/ktheightmap/MapCanvas.kt b/app/src/main/java/com/mirenkov/ktheightmap/MapCanvas.kt index e7ae431..22d8f9c 100644 --- a/app/src/main/java/com/mirenkov/ktheightmap/MapCanvas.kt +++ b/app/src/main/java/com/mirenkov/ktheightmap/MapCanvas.kt @@ -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) ) } } \ No newline at end of file diff --git a/app/src/main/java/com/mirenkov/ktheightmap/TileDao.kt b/app/src/main/java/com/mirenkov/ktheightmap/TileDao.kt index 60f38f2..cd7abfb 100644 --- a/app/src/main/java/com/mirenkov/ktheightmap/TileDao.kt +++ b/app/src/main/java/com/mirenkov/ktheightmap/TileDao.kt @@ -14,4 +14,7 @@ interface TileDao { @Query("delete from tiles") fun clearTiles() + + @Query("select max(level) from tiles") + fun maxLevel(): Int? } \ No newline at end of file diff --git a/app/src/main/java/com/mirenkov/ktheightmap/TileRepository.kt b/app/src/main/java/com/mirenkov/ktheightmap/TileRepository.kt index 5b07937..028d931 100644 --- a/app/src/main/java/com/mirenkov/ktheightmap/TileRepository.kt +++ b/app/src/main/java/com/mirenkov/ktheightmap/TileRepository.kt @@ -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 + } } \ No newline at end of file diff --git a/app/src/main/java/com/mirenkov/ktheightmap/TileViewModel.kt b/app/src/main/java/com/mirenkov/ktheightmap/TileViewModel.kt index e915479..b1dcaf5 100644 --- a/app/src/main/java/com/mirenkov/ktheightmap/TileViewModel.kt +++ b/app/src/main/java/com/mirenkov/ktheightmap/TileViewModel.kt @@ -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() } } \ No newline at end of file