MapCanvas refactoring

This commit is contained in:
Alexey 2025-10-31 13:59:25 +03:00
commit 46c831b473
4 changed files with 229 additions and 203 deletions

13
.gitignore vendored
View file

@ -1,15 +1,10 @@
*.iml *.iml
.gradle .gradle
/local.properties local.properties
/.idea/caches .idea
/.idea/libraries
/.idea/modules.xml
/.idea/workspace.xml
/.idea/navEditor.xml
/.idea/assetWizardSettings.xml
.DS_Store .DS_Store
/build build
/captures captures
.externalNativeBuild .externalNativeBuild
.cxx .cxx
local.properties local.properties

3
.idea/vcs.xml generated
View file

@ -1,5 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?> <?xml version="1.0" encoding="UTF-8"?>
<project version="4"> <project version="4">
<component name="VcsDirectoryMappings">
<mapping directory="" vcs="Git" />
</component>
<component name="VcsProjectSettings"> <component name="VcsProjectSettings">
<option name="detectVcsMappingsAutomatically" value="false" /> <option name="detectVcsMappingsAutomatically" value="false" />
</component> </component>

View file

@ -1,10 +1,10 @@
package com.mirenkov.ktheightmap package com.mirenkov.ktheightmap
import android.content.Context
import androidx.compose.foundation.Canvas import androidx.compose.foundation.Canvas
import androidx.compose.foundation.gestures.detectDragGestures import androidx.compose.foundation.gestures.detectDragGestures
import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxSize
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.mutableStateOf
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
import androidx.compose.runtime.saveable.rememberSaveable 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.Color
import androidx.compose.ui.graphics.Path import androidx.compose.ui.graphics.Path
import androidx.compose.ui.graphics.asImageBitmap import androidx.compose.ui.graphics.asImageBitmap
import androidx.compose.ui.graphics.drawscope.DrawScope
import androidx.compose.ui.graphics.drawscope.Stroke import androidx.compose.ui.graphics.drawscope.Stroke
import androidx.compose.ui.input.pointer.pointerInput import androidx.compose.ui.input.pointer.pointerInput
import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.text.AnnotatedString import androidx.compose.ui.text.AnnotatedString
import androidx.compose.ui.text.ParagraphStyle import androidx.compose.ui.text.ParagraphStyle
import androidx.compose.ui.text.SpanStyle import androidx.compose.ui.text.SpanStyle
import androidx.compose.ui.text.TextMeasurer
import androidx.compose.ui.text.buildAnnotatedString import androidx.compose.ui.text.buildAnnotatedString
import androidx.compose.ui.text.drawText import androidx.compose.ui.text.drawText
import androidx.compose.ui.text.rememberTextMeasurer import androidx.compose.ui.text.rememberTextMeasurer
@ -31,19 +33,6 @@ import kotlin.math.floor
import kotlin.math.pow import kotlin.math.pow
import kotlin.math.sqrt 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) @OptIn(ExperimentalUnsignedTypes::class)
@Composable @Composable
fun MapCanvas( fun MapCanvas(
@ -62,22 +51,13 @@ fun MapCanvas(
var pointRequested by rememberSaveable { viewModel.pointRequested } var pointRequested by rememberSaveable { viewModel.pointRequested }
var pointLat by rememberSaveable { viewModel.rememberedPointLat } var pointLat by rememberSaveable { viewModel.rememberedPointLat }
var pointLon by rememberSaveable { viewModel.rememberedPointLon } var pointLon by rememberSaveable { viewModel.rememberedPointLon }
var invokeHeightCalc by remember { mutableStateOf(false) } var pointHeight by rememberSaveable { viewModel.rememberedPointHeight }
val startTargetMeters = 64 * 1000 * 1000
Canvas( Canvas(
modifier = modifier.fillMaxSize() modifier = modifier.fillMaxSize()
.pointerInput(Unit) { .pointerInput(Unit) {
detectDragGestures ( detectDragGestures { _, distance ->
onDragEnd = {
invokeHeightCalc = true
},
onDragCancel = {
invokeHeightCalc = true
}
) { _, distance ->
offsetX -= distance.x offsetX -= distance.x
offsetY -= distance.y 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 centerTileX = (1 + (offsetX + halvedX) / TILE_SIZE).toDouble()
val centerTileY = (1 + (offsetY + halvedY) / TILE_SIZE).toDouble() val centerTileY = (1 + (offsetY + halvedY) / TILE_SIZE).toDouble()
val lon = SphereMercator.mercateX(centerTileX, level).toFloat() val lon = SphereMercator.mercateX(centerTileX, level).toFloat()
val lat = SphereMercator.mercateY(centerTileY, level).toFloat() val lat = SphereMercator.mercateY(centerTileY, level).toFloat()
val height = KhmParser.getHeight(lon, lat, ctx)
if (pointRequested) { if (pointRequested) {
if (pointLat == 0F) { if (pointLat == 0F) {
pointLat = lat pointLat = lat
pointLon = lon pointLon = lon
pointHeight = height.toInt()
} else { } else {
pointLat = 0F pointLat = 0F
pointLon = 0F pointLon = 0F
pointHeight = 0
} }
pointRequested = false pointRequested = false
} }
@ -137,25 +116,17 @@ fun MapCanvas(
val offset = Offset(strippedOffsetX, strippedOffsetY) 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 // Background
drawRect( drawRect(
color = backColor, color = backColor,
size = size 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 // Tiles
for (cellX in 0 .. gridWidth + 2) { for (cellX in 0 .. gridWidth + 2) {
val tileX = tileOffsetX + cellX val tileX = tileOffsetX + cellX
@ -170,7 +141,7 @@ fun MapCanvas(
// Tile // Tile
bitmap?.let { bitmap?.let {
val imageBitmap = bitmap.asImageBitmap() val imageBitmap = it.asImageBitmap()
drawImage( drawImage(
image = imageBitmap, image = imageBitmap,
topLeft = totalOffset topLeft = totalOffset
@ -179,10 +150,35 @@ fun MapCanvas(
// Debug grid // Debug grid
if (debug) { if (debug) {
drawTileDebugInfo(gridColor, totalOffset, textMeasurer, tile)
}
}
}
// Placed point and line to center
if (pointLat != 0F) {
drawPointInfo(lat, lon, pointLat, pointLon, pointHeight, offsetX, offsetY, halvedX, halvedY, level, gridColor, ctx, textMeasurer)
}
val crossRadius = 24F
drawCursor(halvedX, halvedY, crossRadius, KhmParser.getHeight(lon, lat, ctx), textMeasurer)
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( drawRect(
color = gridColor, color,
size = Size(TILE_SIZE, TILE_SIZE), topLeft,
topLeft = totalOffset, size,
style = Stroke(width = 4F) style = Stroke(width = 4F)
) )
@ -190,45 +186,55 @@ fun MapCanvas(
textMeasurer = textMeasurer, textMeasurer = textMeasurer,
text = buildAnnotatedString { text = buildAnnotatedString {
withStyle(ParagraphStyle(textAlign = TextAlign.Center)) { withStyle(ParagraphStyle(textAlign = TextAlign.Center)) {
withStyle(SpanStyle(color = gridColor)) { withStyle(SpanStyle(color)) {
val mercX = tile.mercateX() val mercX = tile.mercateX()
val mercY = tile.mercateY() val mercY = tile.mercateY()
append("%.6f, %.6f,\n%d, %d".format(mercX, mercY, tileX, tileY)) append("%.6f, %.6f,\n%d, %d".format(mercX, mercY, tile.x, tile.y))
} }
} }
}, },
topLeft = totalOffset, topLeft,
size = Size(TILE_SIZE, TILE_SIZE) size = size
) )
} }
}
}
// Placed point and line to center @OptIn(ExperimentalUnsignedTypes::class)
if (pointLat != 0F) { 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 pointOffsetX = SphereMercator.mercateLon(pointLon.toDouble(), level, -TILE_SIZE.toDouble()).toFloat() - offsetX
val pointOffsetY = SphereMercator.mercateLat(pointLat.toDouble(), level, -TILE_SIZE.toDouble()).toFloat() - offsetY val pointOffsetY = SphereMercator.mercateLat(pointLat.toDouble(), level, -TILE_SIZE.toDouble()).toFloat() - offsetY
drawRect( drawRect(
color = gridColor, color,
size = Size(8F, 8F), size = Size(8F, 8F),
topLeft = Offset(pointOffsetX - 4, pointOffsetY - 4) topLeft = Offset(pointOffsetX - 4, pointOffsetY - 4)
) )
drawLine( drawLine(
color = gridColor, color,
start = Offset(pointOffsetX, pointOffsetY), start = Offset(pointOffsetX, pointOffsetY),
end = Offset(halvedX, halvedY) end = Offset(drawOffsetX, drawOffsetY)
) )
val startHeight = KhmParser.getHeight(pointLon, pointLat, ctx)
if (pointOffsetX >= 0 && pointOffsetY >= 0 && pointOffsetX < size.width && pointOffsetY < size.height) if (pointOffsetX >= 0 && pointOffsetY >= 0 && pointOffsetX < size.width && pointOffsetY < size.height)
drawText( drawText(
textMeasurer = textMeasurer, textMeasurer,
text = buildAnnotatedString { withStyle(SpanStyle(color = Color.White)) { text = buildAnnotatedString { withStyle(SpanStyle(color = Color.White)) {
append("${startHeight}m") append("${pointHeight}m")
} }, } },
topLeft = Offset(pointOffsetX, pointOffsetY - 32) topLeft = Offset(pointOffsetX, pointOffsetY - 32)
) )
if (invokeHeightCalc) {
val latPV = KhmParser.getLatPerValue() val latPV = KhmParser.getLatPerValue()
val lonPV = KhmParser.getLonPerValue() val lonPV = KhmParser.getLonPerValue()
val valueDistance = sqrt(latPV.pow(2) + lonPV.pow(2)) val valueDistance = sqrt(latPV.pow(2) + lonPV.pow(2))
@ -251,32 +257,32 @@ fun MapCanvas(
val stepOffsetY = SphereMercator.mercateLat(stepLat.toDouble(), level, -TILE_SIZE.toDouble()).toFloat() - offsetY 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) if (stepOffsetX >= 0 && stepOffsetY >= 0 && stepOffsetX < size.width && stepOffsetY < size.height && heights[step] > 0u)
drawRect( drawRect(
color = if (heights[step] > startHeight) Color.Red else Color.Green, color = if (heights[step] > pointHeight.toUShort()) Color.Red else Color.Green,
size = Size(8F, 8F), size = Size(8F, 8F),
topLeft = Offset(stepOffsetX - 4, stepOffsetY - 4) topLeft = Offset(stepOffsetX - 4, stepOffsetY - 4)
) )
} }
} val lineLength = sqrt((pointOffsetX - drawOffsetX).pow(2) + (pointOffsetY - drawOffsetY).pow(2))
val lineLength = sqrt((pointOffsetX - halvedX).pow(2) + (pointOffsetY - halvedY).pow(2))
val lineMeters = (lineLength * SphereMercator.scaledMetersPerPixel(level + 1)).toInt() val lineMeters = (lineLength * SphereMercator.scaledMetersPerPixel(level + 1)).toInt()
drawText( drawText(
textMeasurer = textMeasurer, textMeasurer,
text = distanceString(lineMeters), text = buildDistanceString(lineMeters),
size = Size(160F, 48F), size = Size(160F, 48F),
topLeft = Offset( topLeft = Offset(
halvedX - 80F, drawOffsetX - 80F,
halvedY - 80F drawOffsetY - 80F
) )
) )
} }
fun DrawScope.drawCursor(xOffset: Float, yOffset: Float, radius: Float, height: UShort, textMeasurer: TextMeasurer) {
// Cursor path // Cursor path
val cursorPath = Path() val cursorPath = Path()
with(cursorPath) { with(cursorPath) {
moveTo(halvedX - crossRadius, halvedY) moveTo(xOffset - radius, yOffset)
lineTo(halvedX + crossRadius, halvedY) lineTo(xOffset + radius, yOffset)
moveTo(halvedX, halvedY - crossRadius) moveTo(xOffset, yOffset - radius)
lineTo(halvedX, halvedY + crossRadius) lineTo(xOffset, yOffset + radius)
close() close()
} }
@ -287,47 +293,42 @@ fun MapCanvas(
style = Stroke(width = 6F) style = Stroke(width = 6F)
) )
// Height under cursor // Height under cursor
KhmParser.getHeight(lon, lat, ctx).let { if (height > 0u)
if (it < 1u) return@let
drawText( drawText(
textMeasurer = textMeasurer, textMeasurer = textMeasurer,
text = buildAnnotatedString { text = buildAnnotatedString {
withStyle(SpanStyle(color = Color.White)) { withStyle(SpanStyle(color = Color.White)) {
append("${it}m") append("${height}m")
} } },
}, topLeft = Offset(xOffset, yOffset + radius)
topLeft = Offset(halvedX, halvedY + crossRadius)
) )
} }
fun DrawScope.drawInfoBox(color: Color, size: Size, topLeft: Offset, text: AnnotatedString, textMeasurer: TextMeasurer) {
// Info box // Info box
drawRect( drawRect(
color = backColor, color,
size = latLonSize, topLeft,
topLeft = latLonOffset size
) )
// Info box content // Info box content
drawText( drawText(
textMeasurer = textMeasurer, textMeasurer = textMeasurer,
text = buildAnnotatedString { text,
withStyle(ParagraphStyle(textAlign = TextAlign.Center)) { topLeft,
withStyle(SpanStyle(color = gridColor)) { size = size
append("%.6f\n%.6f".format(lat, lon))
if (debug) {
append("\n%.0f\n%.0f".format(offsetX, offsetY))
}
}
}
},
size = latLonSize,
topLeft = latLonOffset
) )
}
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 // Distance measurer path
val measurerPath = Path() val measurerPath = Path()
with(measurerPath) { with(measurerPath) {
moveTo((halvedX - targetPixels).toFloat(), 8F) moveTo((xOffset - targetPixels).toFloat(), 8F)
relativeLineTo(0F, measurerHeight) relativeLineTo(0F, measurerHeight)
relativeMoveTo(targetPixels.toFloat() * 2F, 0F) relativeMoveTo(targetPixels.toFloat() * 2F, 0F)
relativeLineTo(0F, -measurerHeight) relativeLineTo(0F, -measurerHeight)
@ -344,9 +345,35 @@ fun MapCanvas(
// Distance measurer text // Distance measurer text
drawText( drawText(
textMeasurer = textMeasurer, textMeasurer = textMeasurer,
text = distanceString(targetMeters), text = buildDistanceString(targetMeters),
topLeft = Offset(targetPixels.toFloat() * 2F, measurerHeight), topLeft = Offset(targetPixels.toFloat() * 2F, measurerHeight),
size = Size(targetPixels.toFloat() * 2F, 48F) 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))
}
}
}
} }
} }

View file

@ -26,6 +26,7 @@ class TileViewModel(application: Application): ViewModel() {
var rememberedPointLon = mutableFloatStateOf(0F) var rememberedPointLon = mutableFloatStateOf(0F)
var rememberedPointLat = mutableFloatStateOf(0F) var rememberedPointLat = mutableFloatStateOf(0F)
var rememberedPointHeight = mutableIntStateOf(0)
init { init {
val tileDb = TileDB.getInstance(application) val tileDb = TileDB.getInstance(application)