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
|
2022-09-14 15:09:02 +03:00
|
|
|
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
|
|
|
|
)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
2022-07-11 18:32:36 +03:00
|
|
|
|
2022-09-14 15:09:02 +03:00
|
|
|
/**
|
2023-09-10 13:12:45 +03:00
|
|
|
* Create a [MapView] with given [features] group.
|
2022-09-14 15:09:02 +03:00
|
|
|
*/
|
|
|
|
@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,
|
2022-12-25 14:33:31 +03:00
|
|
|
initialRectangle: Rectangle<Gmc>? = null,
|
2023-09-10 13:12:45 +03:00
|
|
|
modifier: Modifier,
|
2022-09-14 15:09:02 +03:00
|
|
|
) {
|
2023-09-10 13:12:45 +03:00
|
|
|
val mapState = MapCanvasState.remember(mapTileProvider, config, initialViewPoint, initialRectangle)
|
|
|
|
MapView(mapState, mapTileProvider, features, modifier)
|
2022-09-14 15:09:02 +03:00
|
|
|
}
|
2022-09-13 20:30:49 +03:00
|
|
|
|
2022-09-13 13:44:38 +03:00
|
|
|
/**
|
|
|
|
* 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.
|
2022-09-13 13:44:38 +03:00
|
|
|
* @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
|
|
|
|
*/
|
2022-07-11 18:32:36 +03:00
|
|
|
@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,
|
2022-12-25 14:33:31 +03:00
|
|
|
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 = {},
|
2022-09-14 15:09:02 +03:00
|
|
|
) {
|
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)
|
2022-09-13 18:27:57 +03:00
|
|
|
}
|