151 lines
6.0 KiB
Kotlin
Raw Normal View History

2022-07-23 10:58:16 +03:00
package center.sciprog.maps.compose
2022-07-11 09:36:43 +03:00
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.runtime.Composable
2023-09-10 13:12:45 +03:00
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.mutableStateMapOf
import androidx.compose.runtime.remember
2022-07-11 09:36:43 +03:00
import androidx.compose.ui.Modifier
2023-09-10 13:12:45 +03:00
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.PathEffect
import androidx.compose.ui.graphics.drawscope.Stroke
import androidx.compose.ui.graphics.drawscope.clipRect
import androidx.compose.ui.graphics.toComposeImageBitmap
import androidx.compose.ui.unit.IntOffset
import androidx.compose.ui.unit.IntSize
import androidx.compose.ui.unit.dp
2022-12-23 22:16:16 +03:00
import center.sciprog.maps.coordinates.Gmc
2022-12-28 20:03:08 +03:00
import center.sciprog.maps.features.*
2023-09-10 13:12:45 +03:00
import io.github.oshai.kotlinlogging.KotlinLogging
import kotlinx.coroutines.CancellationException
import kotlinx.coroutines.launch
import kotlinx.coroutines.supervisorScope
import org.jetbrains.skia.Image
import kotlin.math.ceil
import kotlin.math.pow
2022-07-11 09:36:43 +03:00
2022-07-13 11:36:02 +03:00
2023-09-10 13:12:45 +03:00
private fun IntRange.intersect(other: IntRange) = kotlin.math.max(first, other.first)..kotlin.math.min(last, other.last)
private val logger = KotlinLogging.logger("MapView")
/**
* A component that renders map and provides basic map manipulation capabilities
*/
2022-07-11 09:36:43 +03:00
@Composable
2023-09-10 13:12:45 +03:00
public fun MapView(
mapState: MapCanvasState,
mapTileProvider: MapTileProvider,
2023-01-06 22:16:46 +03:00
features: FeatureGroup<Gmc>,
2023-09-10 13:12:45 +03:00
modifier: Modifier,
) {
val mapTiles = remember(mapTileProvider) {
mutableStateMapOf<TileId, Image>()
}
with(mapState) {
// Load tiles asynchronously
LaunchedEffect(viewPoint, canvasSize) {
with(mapTileProvider) {
val indexRange = 0 until 2.0.pow(intZoom).toInt()
val left = centerCoordinates.x - canvasSize.width.value / 2 / tileScale
val right = centerCoordinates.x + canvasSize.width.value / 2 / tileScale
val horizontalIndices: IntRange = (toIndex(left)..toIndex(right)).intersect(indexRange)
val top = (centerCoordinates.y + canvasSize.height.value / 2 / tileScale)
val bottom = (centerCoordinates.y - canvasSize.height.value / 2 / tileScale)
val verticalIndices: IntRange = (toIndex(bottom)..toIndex(top)).intersect(indexRange)
for (j in verticalIndices) {
for (i in horizontalIndices) {
val id = TileId(intZoom, i, j)
//ensure that failed tiles do not fail the application
supervisorScope {
//start all
val deferred = loadTileAsync(id)
//wait asynchronously for it to finish
launch {
try {
val tile = deferred.await()
mapTiles[tile.id] = tile.image
} catch (ex: Exception) {
//displaying the error is maps responsibility
if(ex !is CancellationException) {
logger.error(ex) { "Failed to load tile with id=$id" }
}
}
}
}
mapTiles.keys.filter {
it.zoom != intZoom || it.j !in verticalIndices || it.i !in horizontalIndices
}.forEach {
mapTiles.remove(it)
}
}
}
}
}
}
FeatureCanvas(mapState, features, modifier = modifier.canvasControls(mapState, features)) {
val tileScale = mapState.tileScale
clipRect {
val tileSize = IntSize(
ceil((mapTileProvider.tileSize.dp * tileScale).toPx()).toInt(),
ceil((mapTileProvider.tileSize.dp * tileScale).toPx()).toInt()
)
mapTiles.forEach { (id, image) ->
//converting back from tile index to screen offset
val offset = IntOffset(
(mapState.canvasSize.width / 2 + (mapTileProvider.toCoordinate(id.i).dp - mapState.centerCoordinates.x.dp) * tileScale).roundToPx(),
(mapState.canvasSize.height / 2 + (mapTileProvider.toCoordinate(id.j).dp - mapState.centerCoordinates.y.dp) * tileScale).roundToPx()
)
drawImage(
image = image.toComposeImageBitmap(),
dstOffset = offset,
dstSize = tileSize
)
}
}
}
}
/**
2023-09-10 13:12:45 +03:00
* Create a [MapView] with given [features] group.
*/
@Composable
public fun MapView(
mapTileProvider: MapTileProvider,
2023-09-10 13:12:45 +03:00
config: ViewConfig<Gmc>,
2023-01-06 22:16:46 +03:00
features: FeatureGroup<Gmc>,
initialViewPoint: ViewPoint<Gmc>? = null,
initialRectangle: Rectangle<Gmc>? = null,
2023-09-10 13:12:45 +03:00
modifier: Modifier,
) {
2023-09-10 13:12:45 +03:00
val mapState = MapCanvasState.remember(mapTileProvider, config, initialViewPoint, initialRectangle)
MapView(mapState, mapTileProvider, features, modifier)
}
/**
* Draw a map using convenient parameters. If neither [initialViewPoint], noe [initialRectangle] is defined,
2023-09-10 13:12:45 +03:00
* use map features to infer the view region.
* @param initialViewPoint The view point of the map using center and zoom. Is used if provided
* @param initialRectangle The rectangle to be used for view point computation. Used if [initialViewPoint] is not defined.
* @param buildFeatures - a builder for features
*/
@Composable
2022-07-23 13:49:47 +03:00
public fun MapView(
2022-07-14 20:19:57 +03:00
mapTileProvider: MapTileProvider,
2023-09-10 13:12:45 +03:00
config: ViewConfig<Gmc> = ViewConfig(),
2023-01-06 22:16:46 +03:00
initialViewPoint: ViewPoint<Gmc>? = null,
initialRectangle: Rectangle<Gmc>? = null,
2022-07-14 20:19:57 +03:00
modifier: Modifier = Modifier.fillMaxSize(),
2023-01-02 14:08:20 +03:00
buildFeatures: FeatureGroup<Gmc>.() -> Unit = {},
) {
2023-01-02 14:08:20 +03:00
val featureState = FeatureGroup.remember(WebMercatorSpace, buildFeatures)
2023-09-10 13:12:45 +03:00
MapView(mapTileProvider, config, featureState, initialViewPoint, initialRectangle, modifier)
}