Fix for imposible tile index crush.

This commit is contained in:
Alexander Nozik 2022-07-11 11:30:24 +03:00
parent 005ef17f8e
commit 6868c1a5ca
No known key found for this signature in database
GPG Key ID: F7FCF2DD25C71357
4 changed files with 59 additions and 26 deletions

View File

@ -14,8 +14,6 @@ data class MapTile(
val image: ImageBitmap, val image: ImageBitmap,
) )
interface MapTileProvider { interface MapTileProvider {
suspend fun loadTile(id: TileId): MapTile suspend fun loadTile(id: TileId): MapTile
fun toIndex(d: Double): Int = floor(d / DEFAULT_TILE_SIZE).toInt() fun toIndex(d: Double): Int = floor(d / DEFAULT_TILE_SIZE).toInt()

View File

@ -13,32 +13,58 @@ import centre.sciprog.maps.MapViewPoint
import centre.sciprog.maps.compose.* import centre.sciprog.maps.compose.*
import io.ktor.client.HttpClient import io.ktor.client.HttpClient
import io.ktor.client.engine.cio.CIO import io.ktor.client.engine.cio.CIO
import kotlinx.coroutines.delay
import java.nio.file.Path import java.nio.file.Path
import kotlin.random.Random
/**
* initial set of features
*/
@Composable
private fun initialFeatures() = buildList {
val pointOne = 55.568548 to 37.568604
val pointTwo = 55.929444 to 37.518434
add(MapVectorImageFeature(pointOne.toCoordinates(), Icons.Filled.Home))
// add(MapCircleFeature(pointOne))
add(MapCircleFeature(pointTwo))
add(MapLineFeature(pointOne, pointTwo))
add(MapTextFeature(pointOne.toCoordinates(), "Home"))
}
@Composable @Composable
@Preview @Preview
fun App() { fun App() {
MaterialTheme { MaterialTheme {
val viewPoint = MapViewPoint( //create a view point
GeodeticMapCoordinates.ofDegrees(55.7558, 37.6173), val viewPoint = remember {
6.0 MapViewPoint(
) GeodeticMapCoordinates.ofDegrees(55.7558, 37.6173),
val pointOne = 55.568548 to 37.568604 6.0
val pointTwo = 55.929444 to 37.518434 )
val features = buildList<MapFeature> {
// add(MapCircleFeature(pointOne))
add(MapCircleFeature(pointTwo))
add(MapLineFeature(pointOne, pointTwo))
add(MapTextFeature(pointOne.toCoordinates(), "Home"))
add(MapVectorImageFeature(pointOne.toCoordinates(), Icons.Filled.Home))
} }
// observable list of features
val features = mutableStateListOf<MapFeature>().apply {
addAll(initialFeatures())
}
// // test dynamic rendering
// LaunchedEffect(features) {
// repeat(10000) {
// delay(10)
// val randomPoint = Random.nextDouble(55.568548, 55.929444) to Random.nextDouble(37.518434, 37.568604)
// features.add(MapCircleFeature(randomPoint))
// }
// }
val scope = rememberCoroutineScope() val scope = rememberCoroutineScope()
val mapTileProvider = remember { OpenStreetMapTileProvider(scope, HttpClient(CIO), Path.of("mapCache")) } val mapTileProvider = remember { OpenStreetMapTileProvider(scope, HttpClient(CIO), Path.of("mapCache")) }
var coordinates by remember { mutableStateOf<GeodeticMapCoordinates?>(null) } var coordinates by remember { mutableStateOf<GeodeticMapCoordinates?>(null) }
Column { Column {
//display click coordinates
Text(coordinates?.toString() ?: "") Text(coordinates?.toString() ?: "")
MapView(viewPoint, mapTileProvider, features = features) { MapView(viewPoint, mapTileProvider, features = features) {
coordinates = it coordinates = it

View File

@ -36,7 +36,6 @@ private val logger = KotlinLogging.logger("MapView")
/** /**
* A component that renders map and provides basic map manipulation capabilities * A component that renders map and provides basic map manipulation capabilities
*/ */
@OptIn(ExperimentalComposeUiApi::class)
@Composable @Composable
actual fun MapView( actual fun MapView(
initialViewPoint: MapViewPoint, initialViewPoint: MapViewPoint,
@ -58,8 +57,6 @@ actual fun MapView(
// Load tiles asynchronously // Load tiles asynchronously
LaunchedEffect(viewPoint, canvasSize) { LaunchedEffect(viewPoint, canvasSize) {
//remember zoom state to avoid races
val z = zoom
val left = centerCoordinates.x - canvasSize.width / 2 val left = centerCoordinates.x - canvasSize.width / 2
val right = centerCoordinates.x + canvasSize.width / 2 val right = centerCoordinates.x + canvasSize.width / 2
val horizontalIndices = mapTileProvider.toIndex(left)..mapTileProvider.toIndex(right) val horizontalIndices = mapTileProvider.toIndex(left)..mapTileProvider.toIndex(right)
@ -70,14 +67,18 @@ actual fun MapView(
mapTiles.clear() mapTiles.clear()
val indexRange = 0 until 2.0.pow(z).toInt() val indexRange = 0 until 2.0.pow(zoom).toInt()
for (j in verticalIndices) { for (j in verticalIndices) {
for (i in horizontalIndices) { for (i in horizontalIndices) {
if (z == zoom && i in indexRange && j in indexRange) { if (i in indexRange && j in indexRange) {
val tileId = TileId(z, i, j) val tileId = TileId(zoom, i, j)
val tile = mapTileProvider.loadTile(tileId) try {
mapTiles.add(tile) val tile = mapTileProvider.loadTile(tileId)
mapTiles.add(tile)
} catch (ex: Exception) {
logger.error(ex) { "Failed to load tile $tileId" }
}
} }
} }
} }
@ -99,6 +100,7 @@ actual fun MapView(
fun GeodeticMapCoordinates.toOffset(): Offset = WebMercatorProjection.toMercator(this, zoom).toOffset() fun GeodeticMapCoordinates.toOffset(): Offset = WebMercatorProjection.toMercator(this, zoom).toOffset()
@OptIn(ExperimentalComposeUiApi::class)
val canvasModifier = modifier.onPointerEvent(PointerEventType.Press) { val canvasModifier = modifier.onPointerEvent(PointerEventType.Press) {
onClick(it.changes.first().position.toGeodetic()) onClick(it.changes.first().position.toGeodetic())
}.onPointerEvent(PointerEventType.Scroll) { }.onPointerEvent(PointerEventType.Scroll) {

View File

@ -5,15 +5,13 @@ import androidx.compose.ui.graphics.toComposeImageBitmap
import io.ktor.client.HttpClient import io.ktor.client.HttpClient
import io.ktor.client.request.get import io.ktor.client.request.get
import io.ktor.client.statement.readBytes import io.ktor.client.statement.readBytes
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.*
import kotlinx.coroutines.Deferred
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.async
import mu.KotlinLogging import mu.KotlinLogging
import org.jetbrains.skia.Image import org.jetbrains.skia.Image
import java.net.URL import java.net.URL
import java.nio.file.Path import java.nio.file.Path
import kotlin.io.path.* import kotlin.io.path.*
import kotlin.math.pow
/** /**
* A [MapTileProvider] based on Open Street Map API. With in-memory and file cache * A [MapTileProvider] based on Open Street Map API. With in-memory and file cache
@ -25,6 +23,9 @@ public class OpenStreetMapTileProvider(private val scope: CoroutineScope, privat
private fun TileId.cacheFilePath() = cacheDirectory.resolve("${zoom}/${i}/${j}.png") private fun TileId.cacheFilePath() = cacheDirectory.resolve("${zoom}/${i}/${j}.png")
/**
* Download and cache the tile image
*/
private fun downloadImageAsync(id: TileId) = scope.async(Dispatchers.IO) { private fun downloadImageAsync(id: TileId) = scope.async(Dispatchers.IO) {
id.cacheFilePath()?.let { path -> id.cacheFilePath()?.let { path ->
if (path.exists()) { if (path.exists()) {
@ -53,6 +54,11 @@ public class OpenStreetMapTileProvider(private val scope: CoroutineScope, privat
} }
override suspend fun loadTile(id: TileId): MapTile { override suspend fun loadTile(id: TileId): MapTile {
val indexRange = indexRange(id.zoom)
if(id.i !in indexRange || id.j !in indexRange){
error("Indices (${id.i}, ${id.j}) are not in index range $indexRange for zoom ${id.zoom}")
}
val image = cache.getOrPut(id) { val image = cache.getOrPut(id) {
downloadImageAsync(id) downloadImageAsync(id)
}.await() }.await()
@ -62,5 +68,6 @@ public class OpenStreetMapTileProvider(private val scope: CoroutineScope, privat
companion object{ companion object{
private val logger = KotlinLogging.logger("OpenStreetMapCache") private val logger = KotlinLogging.logger("OpenStreetMapCache")
private fun indexRange(zoom: Int): IntRange = 0 until 2.0.pow(zoom).toInt()
} }
} }