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
.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

3
.idea/vcs.xml generated
View file

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

View file

@ -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<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
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<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 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()
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()
}
// 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))
}
}
}
// 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)
)
}
}

View file

@ -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)