From 46c831b4738ac8c30f436a62f6e391b09175d949 Mon Sep 17 00:00:00 2001 From: 2ndbeam <2ndbeam@disroot.org> Date: Fri, 31 Oct 2025 13:59:25 +0300 Subject: [PATCH] MapCanvas refactoring --- .gitignore | 13 +- .idea/vcs.xml | 3 + .../com/mirenkov/ktheightmap/MapCanvas.kt | 415 ++++++++++-------- .../com/mirenkov/ktheightmap/TileViewModel.kt | 1 + 4 files changed, 229 insertions(+), 203 deletions(-) diff --git a/.gitignore b/.gitignore index aa724b7..cf7f569 100644 --- a/.gitignore +++ b/.gitignore @@ -1,15 +1,10 @@ *.iml .gradle -/local.properties -/.idea/caches -/.idea/libraries -/.idea/modules.xml -/.idea/workspace.xml -/.idea/navEditor.xml -/.idea/assetWizardSettings.xml +local.properties +.idea .DS_Store -/build -/captures +build +captures .externalNativeBuild .cxx local.properties diff --git a/.idea/vcs.xml b/.idea/vcs.xml index 06a6207..ff6da57 100644 --- a/.idea/vcs.xml +++ b/.idea/vcs.xml @@ -1,5 +1,8 @@ + + + diff --git a/app/src/main/java/com/mirenkov/ktheightmap/MapCanvas.kt b/app/src/main/java/com/mirenkov/ktheightmap/MapCanvas.kt index 22d8f9c..d7f28b3 100644 --- a/app/src/main/java/com/mirenkov/ktheightmap/MapCanvas.kt +++ b/app/src/main/java/com/mirenkov/ktheightmap/MapCanvas.kt @@ -1,10 +1,10 @@ package com.mirenkov.ktheightmap +import android.content.Context import androidx.compose.foundation.Canvas import androidx.compose.foundation.gestures.detectDragGestures import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.runtime.Composable -import androidx.compose.runtime.getValue +import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.saveable.rememberSaveable @@ -15,12 +15,14 @@ import androidx.compose.ui.geometry.Size import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Path import androidx.compose.ui.graphics.asImageBitmap +import androidx.compose.ui.graphics.drawscope.DrawScope 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.TextMeasurer import androidx.compose.ui.text.buildAnnotatedString import androidx.compose.ui.text.drawText import androidx.compose.ui.text.rememberTextMeasurer @@ -31,19 +33,6 @@ 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( @@ -62,22 +51,13 @@ fun MapCanvas( var pointRequested by rememberSaveable { viewModel.pointRequested } var pointLat by rememberSaveable { viewModel.rememberedPointLat } var pointLon by rememberSaveable { viewModel.rememberedPointLon } - var invokeHeightCalc by remember { mutableStateOf(false) } - val startTargetMeters = 64 * 1000 * 1000 + var pointHeight by rememberSaveable { viewModel.rememberedPointHeight } Canvas( modifier = modifier.fillMaxSize() .pointerInput(Unit) { - detectDragGestures ( - onDragEnd = { - invokeHeightCalc = true - }, - onDragCancel = { - invokeHeightCalc = true - } - ) { _, distance -> + detectDragGestures { _, distance -> offsetX -= distance.x offsetY -= distance.y - invokeHeightCalc = false } } ) { @@ -108,23 +88,22 @@ 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() val lon = SphereMercator.mercateX(centerTileX, level).toFloat() val lat = SphereMercator.mercateY(centerTileY, level).toFloat() + val height = KhmParser.getHeight(lon, lat, ctx) if (pointRequested) { if (pointLat == 0F) { pointLat = lat pointLon = lon + pointHeight = height.toInt() } else { pointLat = 0F pointLon = 0F + pointHeight = 0 } pointRequested = false } @@ -137,25 +116,17 @@ fun MapCanvas( val offset = Offset(strippedOffsetX, strippedOffsetY) - val grid = size / TILE_SIZE - - val gridWidth = grid.width.toInt() - val gridHeight = grid.height.toInt() - - val tiles = tileContainer.getTiles(tileOffsetX, tileOffsetY, tileOffsetX + gridWidth + 2, tileOffsetY + gridHeight + 2, level) - - val crossRadius = 24F - - val additionalSize = if (debug) 96F else 0F - val latLonSize = Size(216F, 96F + additionalSize) - val latLonOffset = Offset(16F, 16F) - // Background drawRect( color = backColor, size = size ) + val grid = size / TILE_SIZE + val gridWidth = grid.width.toInt() + val gridHeight = grid.height.toInt() + + val tiles = tileContainer.getTiles(tileOffsetX, tileOffsetY, tileOffsetX + gridWidth + 2, tileOffsetY + gridHeight + 2, level) // Tiles for (cellX in 0 .. gridWidth + 2) { val tileX = tileOffsetX + cellX @@ -170,7 +141,7 @@ fun MapCanvas( // Tile bitmap?.let { - val imageBitmap = bitmap.asImageBitmap() + val imageBitmap = it.asImageBitmap() drawImage( image = imageBitmap, topLeft = totalOffset @@ -179,174 +150,230 @@ fun MapCanvas( // Debug grid if (debug) { - drawRect( - color = gridColor, - size = Size(TILE_SIZE, TILE_SIZE), - topLeft = totalOffset, - style = Stroke(width = 4F) - ) - - drawText( - textMeasurer = textMeasurer, - text = buildAnnotatedString { - withStyle(ParagraphStyle(textAlign = TextAlign.Center)) { - withStyle(SpanStyle(color = gridColor)) { - val mercX = tile.mercateX() - val mercY = tile.mercateY() - append("%.6f, %.6f,\n%d, %d".format(mercX, mercY, tileX, tileY)) - } - } - }, - topLeft = totalOffset, - size = Size(TILE_SIZE, TILE_SIZE) - ) + drawTileDebugInfo(gridColor, totalOffset, textMeasurer, tile) } } } // Placed point and line to center if (pointLat != 0F) { - val pointOffsetX = SphereMercator.mercateLon(pointLon.toDouble(), level, -TILE_SIZE.toDouble()).toFloat() - offsetX - val pointOffsetY = SphereMercator.mercateLat(pointLat.toDouble(), level, -TILE_SIZE.toDouble()).toFloat() - offsetY - drawRect( - color = gridColor, - size = Size(8F, 8F), - topLeft = Offset(pointOffsetX - 4, pointOffsetY - 4) - ) - drawLine( - color = gridColor, - start = Offset(pointOffsetX, pointOffsetY), - end = Offset(halvedX, halvedY) - ) + drawPointInfo(lat, lon, pointLat, pointLon, pointHeight, offsetX, offsetY, halvedX, halvedY, level, gridColor, ctx, textMeasurer) + } - 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") - } }, - topLeft = Offset(pointOffsetX, pointOffsetY - 32) - ) - if (invokeHeightCalc) { - val latPV = KhmParser.getLatPerValue() - val lonPV = KhmParser.getLonPerValue() - val valueDistance = sqrt(latPV.pow(2) + lonPV.pow(2)) - val latDiff = lat - pointLat - val lonDiff = lon - pointLon - val distance = sqrt((latDiff).pow(2) + (lonDiff).pow(2)) - val valuesCount = floor(distance / valueDistance).toInt() - val array: Array> = Array(valuesCount) { step -> - val interCoef = 1F - step.toFloat() / valuesCount - Pair(lon - lonDiff * interCoef, lat - latDiff * interCoef) - } - val heightPair = KhmParser.getHeightsMul(ctx, array) - val heights = heightPair.first - val coords = heightPair.second + val crossRadius = 24F + drawCursor(halvedX, halvedY, crossRadius, KhmParser.getHeight(lon, lat, ctx), textMeasurer) - for (step in 0 until coords.size) { - val stepLat = coords[step].second - 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 && heights[step] > 0u) - drawRect( - color = if (heights[step] > startHeight) Color.Red else Color.Green, - size = Size(8F, 8F), - topLeft = Offset(stepOffsetX - 4, stepOffsetY - 4) - ) + val additionalSize = if (debug) 96F else 0F + val infoBoxSize = Size(216F, 96F + additionalSize) + val infoBoxOffset = Offset(16F, 16F) + val infoBoxText = buildInfoBoxString(gridColor, lat, lon, offsetX, offsetY, debug) + drawInfoBox(backColor, infoBoxSize, infoBoxOffset, infoBoxText, textMeasurer) + + drawDistanceMeasurer(level, halvedX, textMeasurer) + } +} + +fun DrawScope.drawTileDebugInfo(color: Color, topLeft: Offset, textMeasurer: TextMeasurer, tile: Tile) { + val size = Size(TILE_SIZE, TILE_SIZE) + drawRect( + color, + topLeft, + size, + style = Stroke(width = 4F) + ) + + drawText( + textMeasurer = textMeasurer, + text = buildAnnotatedString { + withStyle(ParagraphStyle(textAlign = TextAlign.Center)) { + withStyle(SpanStyle(color)) { + val mercX = tile.mercateX() + val mercY = tile.mercateY() + append("%.6f, %.6f,\n%d, %d".format(mercX, mercY, tile.x, tile.y)) } } - 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 - ) - ) - } + }, + topLeft, + size = size + ) +} - // Cursor path - val cursorPath = Path() - with(cursorPath) { - moveTo(halvedX - crossRadius, halvedY) - lineTo(halvedX + crossRadius, halvedY) - moveTo(halvedX, halvedY - crossRadius) - lineTo(halvedX, halvedY + crossRadius) - close() - } +@OptIn(ExperimentalUnsignedTypes::class) +fun DrawScope.drawPointInfo( + lat: Float, + lon: Float, + pointLat: Float, + pointLon: Float, + pointHeight: Int, + offsetX: Float, + offsetY: Float, + drawOffsetX: Float, + drawOffsetY: Float, + level: Int, + color: Color, + ctx: Context, + textMeasurer: TextMeasurer +) { + val pointOffsetX = SphereMercator.mercateLon(pointLon.toDouble(), level, -TILE_SIZE.toDouble()).toFloat() - offsetX + val pointOffsetY = SphereMercator.mercateLat(pointLat.toDouble(), level, -TILE_SIZE.toDouble()).toFloat() - offsetY + drawRect( + color, + size = Size(8F, 8F), + topLeft = Offset(pointOffsetX - 4, pointOffsetY - 4) + ) + drawLine( + color, + start = Offset(pointOffsetX, pointOffsetY), + end = Offset(drawOffsetX, drawOffsetY) + ) - // Cursor - drawPath( - cursorPath, - Color.White, - style = Stroke(width = 6F) + if (pointOffsetX >= 0 && pointOffsetY >= 0 && pointOffsetX < size.width && pointOffsetY < size.height) + drawText( + textMeasurer, + text = buildAnnotatedString { withStyle(SpanStyle(color = Color.White)) { + append("↑${pointHeight}m") + } }, + topLeft = Offset(pointOffsetX, pointOffsetY - 32) ) - // 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) + val latPV = KhmParser.getLatPerValue() + val lonPV = KhmParser.getLonPerValue() + val valueDistance = sqrt(latPV.pow(2) + lonPV.pow(2)) + val latDiff = lat - pointLat + val lonDiff = lon - pointLon + val distance = sqrt((latDiff).pow(2) + (lonDiff).pow(2)) + val valuesCount = floor(distance / valueDistance).toInt() + val array: Array> = Array(valuesCount) { step -> + val interCoef = 1F - step.toFloat() / valuesCount + Pair(lon - lonDiff * interCoef, lat - latDiff * interCoef) + } + val heightPair = KhmParser.getHeightsMul(ctx, array) + val heights = heightPair.first + val coords = heightPair.second + + for (step in 0 until coords.size) { + val stepLat = coords[step].second + 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 && heights[step] > 0u) + drawRect( + color = if (heights[step] > pointHeight.toUShort()) Color.Red else Color.Green, + size = Size(8F, 8F), + topLeft = Offset(stepOffsetX - 4, stepOffsetY - 4) ) - } - - - // Info box - drawRect( - color = backColor, - size = latLonSize, - topLeft = latLonOffset + } + val lineLength = sqrt((pointOffsetX - drawOffsetX).pow(2) + (pointOffsetY - drawOffsetY).pow(2)) + val lineMeters = (lineLength * SphereMercator.scaledMetersPerPixel(level + 1)).toInt() + drawText( + textMeasurer, + text = buildDistanceString(lineMeters), + size = Size(160F, 48F), + topLeft = Offset( + drawOffsetX - 80F, + drawOffsetY - 80F ) - // Info box content + ) +} + +fun DrawScope.drawCursor(xOffset: Float, yOffset: Float, radius: Float, height: UShort, textMeasurer: TextMeasurer) { + // Cursor path + val cursorPath = Path() + with(cursorPath) { + moveTo(xOffset - radius, yOffset) + lineTo(xOffset + radius, yOffset) + moveTo(xOffset, yOffset - radius) + lineTo(xOffset, yOffset + radius) + close() + } + + // Cursor + drawPath( + cursorPath, + Color.White, + style = Stroke(width = 6F) + ) + // Height under cursor + if (height > 0u) drawText( textMeasurer = textMeasurer, text = buildAnnotatedString { - withStyle(ParagraphStyle(textAlign = TextAlign.Center)) { - withStyle(SpanStyle(color = gridColor)) { - append("%.6f\n%.6f".format(lat, lon)) - if (debug) { - append("\n%.0f\n%.0f".format(offsetX, offsetY)) - } - } - } - }, - size = latLonSize, - topLeft = latLonOffset + withStyle(SpanStyle(color = Color.White)) { + append("↑${height}m") + } }, + topLeft = Offset(xOffset, yOffset + radius) ) +} - // 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 = distanceString(targetMeters), - topLeft = Offset(targetPixels.toFloat() * 2F, measurerHeight), - size = Size(targetPixels.toFloat() * 2F, 48F) - ) +fun DrawScope.drawInfoBox(color: Color, size: Size, topLeft: Offset, text: AnnotatedString, textMeasurer: TextMeasurer) { + // Info box + drawRect( + color, + topLeft, + size + ) + // Info box content + drawText( + textMeasurer = textMeasurer, + text, + topLeft, + size = size + ) +} + +fun DrawScope.drawDistanceMeasurer(level: Int, xOffset: Float, textMeasurer: TextMeasurer) { + val startTargetMeters = 64 * 1000 * 1000 + val targetMeters = startTargetMeters shr level + val targetPixels = (targetMeters).toFloat() / SphereMercator.scaledMetersPerPixel(level) + val measurerHeight = 24F + // Distance measurer path + val measurerPath = Path() + with(measurerPath) { + moveTo((xOffset - targetPixels).toFloat(), 8F) + relativeLineTo(0F, measurerHeight) + relativeMoveTo(targetPixels.toFloat() * 2F, 0F) + relativeLineTo(0F, -measurerHeight) + relativeMoveTo(0F, measurerHeight / 2F) + relativeLineTo(-2F * targetPixels.toFloat(), 0F) + close() } -} \ No newline at end of file + // Distance measurer + drawPath( + measurerPath, + Color.White, + style = Stroke(width = 6F) + ) + // Distance measurer text + drawText( + textMeasurer = textMeasurer, + text = buildDistanceString(targetMeters), + topLeft = Offset(targetPixels.toFloat() * 2F, measurerHeight), + size = Size(targetPixels.toFloat() * 2F, 48F) + ) +} + +fun buildDistanceString(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) + } + } + } +} + +fun buildInfoBoxString(color: Color, lat: Float, lon: Float, offsetX: Float, offsetY: Float, debug: Boolean): AnnotatedString { + return buildAnnotatedString { + withStyle(ParagraphStyle(textAlign = TextAlign.Center)) { + withStyle(SpanStyle(color = color)) { + append("%.6f\n%.6f".format(lat, lon)) + if (debug) { + append("\n%.0f\n%.0f".format(offsetX, offsetY)) + } + } + } + } +} diff --git a/app/src/main/java/com/mirenkov/ktheightmap/TileViewModel.kt b/app/src/main/java/com/mirenkov/ktheightmap/TileViewModel.kt index b1dcaf5..878a31f 100644 --- a/app/src/main/java/com/mirenkov/ktheightmap/TileViewModel.kt +++ b/app/src/main/java/com/mirenkov/ktheightmap/TileViewModel.kt @@ -26,6 +26,7 @@ class TileViewModel(application: Application): ViewModel() { var rememberedPointLon = mutableFloatStateOf(0F) var rememberedPointLat = mutableFloatStateOf(0F) + var rememberedPointHeight = mutableIntStateOf(0) init { val tileDb = TileDB.getInstance(application)