diff --git a/src/commonMain/kotlin/centre/sciprog/maps/compose/MapTileProvider.kt b/src/commonMain/kotlin/centre/sciprog/maps/compose/MapTileProvider.kt index c9e6af7..dfba9db 100644 --- a/src/commonMain/kotlin/centre/sciprog/maps/compose/MapTileProvider.kt +++ b/src/commonMain/kotlin/centre/sciprog/maps/compose/MapTileProvider.kt @@ -16,10 +16,14 @@ data class MapTile( interface MapTileProvider { suspend fun loadTile(id: TileId): MapTile - fun toIndex(d: Double): Int = floor(d / DEFAULT_TILE_SIZE).toInt() - fun toCoordinate(i: Int): Double = (i * DEFAULT_TILE_SIZE).toDouble() - companion object{ + val tileSize: Int get() = DEFAULT_TILE_SIZE + + fun toIndex(d: Double): Int = floor(d / tileSize).toInt() + + fun toCoordinate(i: Int): Double = (i * tileSize).toDouble() + + companion object { const val DEFAULT_TILE_SIZE = 256 } } \ No newline at end of file diff --git a/src/jvmMain/kotlin/centre/sciprog/maps/compose/MapViewJvm.kt b/src/jvmMain/kotlin/centre/sciprog/maps/compose/MapViewJvm.kt index 2a20f10..81b8bc1 100644 --- a/src/jvmMain/kotlin/centre/sciprog/maps/compose/MapViewJvm.kt +++ b/src/jvmMain/kotlin/centre/sciprog/maps/compose/MapViewJvm.kt @@ -4,11 +4,9 @@ import androidx.compose.foundation.Canvas import androidx.compose.foundation.gestures.detectDragGestures import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.runtime.* -import androidx.compose.runtime.snapshots.SnapshotStateMap import androidx.compose.ui.ExperimentalComposeUiApi import androidx.compose.ui.Modifier import androidx.compose.ui.geometry.Offset -import androidx.compose.ui.geometry.Size import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.drawscope.DrawScope import androidx.compose.ui.graphics.drawscope.clipRect @@ -20,6 +18,7 @@ import androidx.compose.ui.input.pointer.PointerEventType import androidx.compose.ui.input.pointer.PointerInputChange import androidx.compose.ui.input.pointer.onPointerEvent import androidx.compose.ui.input.pointer.pointerInput +import androidx.compose.ui.unit.* import centre.sciprog.maps.* import mu.KotlinLogging import org.jetbrains.skia.Font @@ -46,6 +45,7 @@ actual fun MapView( onClick: (GeodeticMapCoordinates) -> Unit, modifier: Modifier, ) { + var viewPoint by remember { mutableStateOf(initialViewPoint) } val zoom: Int by derivedStateOf { viewPoint.zoom.roundToInt() } @@ -53,18 +53,39 @@ actual fun MapView( val mapTiles = remember { mutableStateListOf() } //var mapRectangle by remember { mutableStateOf(initialRectangle) } - var canvasSize by remember { mutableStateOf(Size(512f, 512f)) } + var canvasSize by remember { mutableStateOf(DpSize(512.dp, 512.dp)) } val centerCoordinates by derivedStateOf { WebMercatorProjection.toMercator(viewPoint.focus, zoom) } + fun DpOffset.toMercator(): WebMercatorCoordinates = WebMercatorCoordinates( + zoom, + (x - canvasSize.width / 2).value + centerCoordinates.x, + (y - canvasSize.height / 2).value + centerCoordinates.y, + ) + + fun DpOffset.toGeodetic() = WebMercatorProjection.toGeodetic(toMercator()) + + @OptIn(ExperimentalComposeUiApi::class) + val canvasModifier = modifier.onPointerEvent(PointerEventType.Press) { + val (xPos, yPos) = it.changes.first().position + onClick(DpOffset(xPos.toDp(), yPos.toDp()).toGeodetic()) + }.onPointerEvent(PointerEventType.Scroll) { + viewPoint = viewPoint.zoom(-it.changes.first().scrollDelta.y.toDouble()) + }.pointerInput(Unit) { + detectDragGestures { _: PointerInputChange, dragAmount: Offset -> + viewPoint = viewPoint.move(-dragAmount.x, +dragAmount.y) + } + }.fillMaxSize() + + // Load tiles asynchronously LaunchedEffect(viewPoint, canvasSize) { - val left = centerCoordinates.x - canvasSize.width / 2 - val right = centerCoordinates.x + canvasSize.width / 2 + val left = centerCoordinates.x - canvasSize.width.value / 2 + val right = centerCoordinates.x + canvasSize.width.value / 2 val horizontalIndices = mapTileProvider.toIndex(left)..mapTileProvider.toIndex(right) - val top = (centerCoordinates.y + canvasSize.height / 2) - val bottom = (centerCoordinates.y - canvasSize.height / 2) + val top = (centerCoordinates.y + canvasSize.height.value / 2) + val bottom = (centerCoordinates.y - canvasSize.height.value / 2) val verticalIndices = mapTileProvider.toIndex(bottom)..mapTileProvider.toIndex(top) mapTiles.clear() @@ -87,80 +108,65 @@ actual fun MapView( } - fun Offset.toMercator(): WebMercatorCoordinates = WebMercatorCoordinates( - zoom, - x + centerCoordinates.x - canvasSize.width / 2, - y + centerCoordinates.y - canvasSize.height / 2, - ) - - fun Offset.toGeodetic() = WebMercatorProjection.toGeodetic(toMercator()) - - fun WebMercatorCoordinates.toOffset(): Offset = Offset( - (canvasSize.width / 2 - centerCoordinates.x + x).toFloat(), - (canvasSize.height / 2 - centerCoordinates.y + y).toFloat() - ) - - fun GeodeticMapCoordinates.toOffset(): Offset = WebMercatorProjection.toMercator(this, zoom).toOffset() - - @OptIn(ExperimentalComposeUiApi::class) - val canvasModifier = modifier.onPointerEvent(PointerEventType.Press) { - onClick(it.changes.first().position.toGeodetic()) - }.onPointerEvent(PointerEventType.Scroll) { - viewPoint = viewPoint.zoom(-it.changes.first().scrollDelta.y.toDouble()) - }.pointerInput(Unit) { - detectDragGestures { _: PointerInputChange, dragAmount: Offset -> - viewPoint = viewPoint.move(-dragAmount.x, +dragAmount.y) - } - }.fillMaxSize() - - fun DrawScope.drawFeature(zoom: Int, feature: MapFeature) { - when (feature) { - is MapFeatureSelector -> drawFeature(zoom, feature.selector(zoom)) - is MapCircleFeature -> drawCircle( - feature.color, - feature.size, - center = feature.center.toOffset() - ) - is MapLineFeature -> drawLine(feature.color, feature.a.toOffset(), feature.b.toOffset()) - is MapBitmapImageFeature -> drawImage(feature.image, feature.position.toOffset()) - is MapVectorImageFeature -> { - val offset = feature.position.toOffset() - translate(offset.x - feature.size.width / 2, offset.y - feature.size.height / 2) { - with(feature.painter) { - draw(feature.size) - } - } - } - is MapTextFeature -> drawIntoCanvas { canvas -> - val offset = feature.position.toOffset() - canvas.nativeCanvas.drawString( - feature.text, - offset.x + 5, - offset.y - 5, - Font().apply { size = 16f }, - feature.color.toPaint() - ) - } - - } - } - Canvas(canvasModifier) { - if (canvasSize != size) { - canvasSize = size - logger.debug { "Redraw canvas. Size: $size" } + + fun WebMercatorCoordinates.toOffset(): Offset = Offset( + (canvasSize.width / 2 - centerCoordinates.x.dp + x.dp).toPx(), + (canvasSize.height / 2 - centerCoordinates.y.dp + y.dp).toPx() + ) + + fun GeodeticMapCoordinates.toOffset(): Offset = WebMercatorProjection.toMercator(this, zoom).toOffset() + + + fun DrawScope.drawFeature(zoom: Int, feature: MapFeature) { + when (feature) { + is MapFeatureSelector -> drawFeature(zoom, feature.selector(zoom)) + is MapCircleFeature -> drawCircle( + feature.color, + feature.size, + center = feature.center.toOffset() + ) + is MapLineFeature -> drawLine(feature.color, feature.a.toOffset(), feature.b.toOffset()) + is MapBitmapImageFeature -> drawImage(feature.image, feature.position.toOffset()) + is MapVectorImageFeature -> { + val offset = feature.position.toOffset() + translate(offset.x - feature.size.width / 2, offset.y - feature.size.height / 2) { + with(feature.painter) { + draw(feature.size) + } + } + } + is MapTextFeature -> drawIntoCanvas { canvas -> + val offset = feature.position.toOffset() + canvas.nativeCanvas.drawString( + feature.text, + offset.x + 5, + offset.y - 5, + Font().apply { size = 16f }, + feature.color.toPaint() + ) + } + + } + } + + if (canvasSize != size.toDpSize()) { + canvasSize = size.toDpSize() + logger.debug { "Recalculate canvas. Size: $size" } } clipRect { + val tileSize = IntSize(mapTileProvider.tileSize.dp.roundToPx(), mapTileProvider.tileSize.dp.roundToPx()) mapTiles.forEach { (id, image) -> //converting back from tile index to screen offset - val offset = Offset( - (canvasSize.width / 2 - centerCoordinates.x + mapTileProvider.toCoordinate(id.i)).toFloat(), - (canvasSize.height / 2 - centerCoordinates.y + mapTileProvider.toCoordinate(id.j)).toFloat() + val offset = IntOffset( + (canvasSize.width / 2 - centerCoordinates.x.dp + mapTileProvider.toCoordinate(id.i).dp).roundToPx(), + (canvasSize.height / 2 - centerCoordinates.y.dp + mapTileProvider.toCoordinate(id.j).dp).roundToPx() ) drawImage( image = image, - topLeft = offset + dstOffset = offset, + dstSize = tileSize ) } features.values.filter { zoom in it.zoomRange }.forEach { feature ->