Compare commits
6 commits
86a2dc3786
...
b70b98feb6
| Author | SHA1 | Date | |
|---|---|---|---|
| b70b98feb6 | |||
| 3510da0428 | |||
| 004b498ee9 | |||
| 049311ec9b | |||
| 182f0b29b0 | |||
| d8139f2140 |
6 changed files with 95 additions and 20 deletions
|
|
@ -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<Float, Float>>): Pair<UShortArray, Array<Pair<Float, Float>>> {
|
||||
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)
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -14,4 +14,7 @@ interface TileDao {
|
|||
|
||||
@Query("delete from tiles")
|
||||
fun clearTiles()
|
||||
|
||||
@Query("select max(level) from tiles")
|
||||
fun maxLevel(): Int?
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue