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,174 +150,230 @@ fun MapCanvas(
// Debug grid // Debug grid
if (debug) { if (debug) {
drawRect( drawTileDebugInfo(gridColor, totalOffset, textMeasurer, tile)
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)
)
} }
} }
} }
// Placed point and line to center // Placed point and line to center
if (pointLat != 0F) { if (pointLat != 0F) {
val pointOffsetX = SphereMercator.mercateLon(pointLon.toDouble(), level, -TILE_SIZE.toDouble()).toFloat() - offsetX drawPointInfo(lat, lon, pointLat, pointLon, pointHeight, offsetX, offsetY, halvedX, halvedY, level, gridColor, ctx, textMeasurer)
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)
)
val startHeight = KhmParser.getHeight(pointLon, pointLat, ctx) val crossRadius = 24F
if (pointOffsetX >= 0 && pointOffsetY >= 0 && pointOffsetX < size.width && pointOffsetY < size.height) drawCursor(halvedX, halvedY, crossRadius, KhmParser.getHeight(lon, lat, ctx), textMeasurer)
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<Pair<Float, Float>> = 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 additionalSize = if (debug) 96F else 0F
val stepLat = coords[step].second val infoBoxSize = Size(216F, 96F + additionalSize)
val stepLon = coords[step].first val infoBoxOffset = Offset(16F, 16F)
val stepOffsetX = SphereMercator.mercateLon(stepLon.toDouble(), level, -TILE_SIZE.toDouble()).toFloat() - offsetX val infoBoxText = buildInfoBoxString(gridColor, lat, lon, offsetX, offsetY, debug)
val stepOffsetY = SphereMercator.mercateLat(stepLat.toDouble(), level, -TILE_SIZE.toDouble()).toFloat() - offsetY drawInfoBox(backColor, infoBoxSize, infoBoxOffset, infoBoxText, textMeasurer)
if (stepOffsetX >= 0 && stepOffsetY >= 0 && stepOffsetX < size.width && stepOffsetY < size.height && heights[step] > 0u)
drawRect( drawDistanceMeasurer(level, halvedX, textMeasurer)
color = if (heights[step] > startHeight) Color.Red else Color.Green, }
size = Size(8F, 8F), }
topLeft = Offset(stepOffsetX - 4, stepOffsetY - 4)
) 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() topLeft,
drawText( size = size
textMeasurer = textMeasurer, )
text = distanceString(lineMeters), }
size = Size(160F, 48F),
topLeft = Offset(
halvedX - 80F,
halvedY - 80F
)
)
}
// Cursor path @OptIn(ExperimentalUnsignedTypes::class)
val cursorPath = Path() fun DrawScope.drawPointInfo(
with(cursorPath) { lat: Float,
moveTo(halvedX - crossRadius, halvedY) lon: Float,
lineTo(halvedX + crossRadius, halvedY) pointLat: Float,
moveTo(halvedX, halvedY - crossRadius) pointLon: Float,
lineTo(halvedX, halvedY + crossRadius) pointHeight: Int,
close() 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 if (pointOffsetX >= 0 && pointOffsetY >= 0 && pointOffsetX < size.width && pointOffsetY < size.height)
drawPath( drawText(
cursorPath, textMeasurer,
Color.White, text = buildAnnotatedString { withStyle(SpanStyle(color = Color.White)) {
style = Stroke(width = 6F) append("${pointHeight}m")
} },
topLeft = Offset(pointOffsetX, pointOffsetY - 32)
) )
// Height under cursor val latPV = KhmParser.getLatPerValue()
KhmParser.getHeight(lon, lat, ctx).let { val lonPV = KhmParser.getLonPerValue()
if (it < 1u) return@let val valueDistance = sqrt(latPV.pow(2) + lonPV.pow(2))
drawText( val latDiff = lat - pointLat
textMeasurer = textMeasurer, val lonDiff = lon - pointLon
text = buildAnnotatedString { val distance = sqrt((latDiff).pow(2) + (lonDiff).pow(2))
withStyle(SpanStyle(color = Color.White)) { val valuesCount = floor(distance / valueDistance).toInt()
append("${it}m") val array: Array<Pair<Float, Float>> = Array(valuesCount) { step ->
} val interCoef = 1F - step.toFloat() / valuesCount
}, Pair(lon - lonDiff * interCoef, lat - latDiff * interCoef)
topLeft = Offset(halvedX, halvedY + crossRadius) }
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)
) )
} }
val lineLength = sqrt((pointOffsetX - drawOffsetX).pow(2) + (pointOffsetY - drawOffsetY).pow(2))
val lineMeters = (lineLength * SphereMercator.scaledMetersPerPixel(level + 1)).toInt()
// Info box drawText(
drawRect( textMeasurer,
color = backColor, text = buildDistanceString(lineMeters),
size = latLonSize, size = Size(160F, 48F),
topLeft = latLonOffset 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( drawText(
textMeasurer = textMeasurer, textMeasurer = textMeasurer,
text = buildAnnotatedString { text = buildAnnotatedString {
withStyle(ParagraphStyle(textAlign = TextAlign.Center)) { withStyle(SpanStyle(color = Color.White)) {
withStyle(SpanStyle(color = gridColor)) { append("${height}m")
append("%.6f\n%.6f".format(lat, lon)) } },
if (debug) { topLeft = Offset(xOffset, yOffset + radius)
append("\n%.0f\n%.0f".format(offsetX, offsetY))
}
}
}
},
size = latLonSize,
topLeft = latLonOffset
) )
}
// Distance measurer path fun DrawScope.drawInfoBox(color: Color, size: Size, topLeft: Offset, text: AnnotatedString, textMeasurer: TextMeasurer) {
val measurerPath = Path() // Info box
with(measurerPath) { drawRect(
moveTo((halvedX - targetPixels).toFloat(), 8F) color,
relativeLineTo(0F, measurerHeight) topLeft,
relativeMoveTo(targetPixels.toFloat() * 2F, 0F) size
relativeLineTo(0F, -measurerHeight) )
relativeMoveTo(0F, measurerHeight / 2F) // Info box content
relativeLineTo(-2F * targetPixels.toFloat(), 0F) drawText(
close() textMeasurer = textMeasurer,
} text,
// Distance measurer topLeft,
drawPath( size = size
measurerPath, )
Color.White, }
style = Stroke(width = 6F)
) fun DrawScope.drawDistanceMeasurer(level: Int, xOffset: Float, textMeasurer: TextMeasurer) {
// Distance measurer text val startTargetMeters = 64 * 1000 * 1000
drawText( val targetMeters = startTargetMeters shr level
textMeasurer = textMeasurer, val targetPixels = (targetMeters).toFloat() / SphereMercator.scaledMetersPerPixel(level)
text = distanceString(targetMeters), val measurerHeight = 24F
topLeft = Offset(targetPixels.toFloat() * 2F, measurerHeight), // Distance measurer path
size = Size(targetPixels.toFloat() * 2F, 48F) 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()
} }
} // 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))
}
}
}
}
}

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)