Fix for imposible tile index crush.
This commit is contained in:
parent
005ef17f8e
commit
6868c1a5ca
@ -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()
|
||||||
|
@ -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
|
||||||
|
@ -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) {
|
||||||
|
@ -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()
|
||||||
}
|
}
|
||||||
}
|
}
|
Loading…
Reference in New Issue
Block a user