diff --git a/src/commonMain/kotlin/centre/sciprog/maps/MapViewPoint.kt b/src/commonMain/kotlin/centre/sciprog/maps/MapViewPoint.kt index c99cfc0..e873048 100644 --- a/src/commonMain/kotlin/centre/sciprog/maps/MapViewPoint.kt +++ b/src/commonMain/kotlin/centre/sciprog/maps/MapViewPoint.kt @@ -2,7 +2,6 @@ package centre.sciprog.maps import kotlin.math.pow import kotlin.math.roundToInt -import kotlin.math.sign /** * Observable position on the map. Includes observation coordinate and [zoom] factor @@ -14,7 +13,7 @@ data class MapViewPoint( val scaleFactor by lazy { WebMercatorProjection.scaleFactor(zoom.roundToInt()) } } -fun MapViewPoint.move(deltaX: Float, deltaY: Float): MapViewPoint { +fun MapViewPoint.move(deltaX: Double, deltaY: Double): MapViewPoint { val newCoordinates = GeodeticMapCoordinates.ofRadians( (focus.latitude + deltaY / scaleFactor).coerceIn( -MercatorProjection.MAXIMUM_LATITUDE, @@ -38,11 +37,11 @@ fun MapViewPoint.move(delta: GeodeticMapCoordinates): MapViewPoint { fun MapViewPoint.zoom(zoomDelta: Double): MapViewPoint = copy(zoom = (zoom + zoomDelta).coerceIn(2.0, 18.0)) -fun MapViewPoint.zoom(zoomDelta: Double, at: GeodeticMapCoordinates): MapViewPoint { +fun MapViewPoint.zoom(zoomDelta: Double, invariant: GeodeticMapCoordinates): MapViewPoint { val difScale = 2.0.pow(-zoomDelta) val newCenter = GeodeticMapCoordinates.ofRadians( - focus.latitude + (at.latitude - focus.latitude) * difScale, - focus.longitude + (at.longitude - focus.longitude) * difScale + focus.latitude + (invariant.latitude - focus.latitude) * difScale, + focus.longitude + (invariant.longitude - focus.longitude) * difScale ) return MapViewPoint(newCenter, (zoom + zoomDelta).coerceIn(2.0, 18.0)) } \ No newline at end of file diff --git a/src/commonMain/kotlin/centre/sciprog/maps/compose/MapView.kt b/src/commonMain/kotlin/centre/sciprog/maps/compose/MapView.kt index ca6bb99..91c2fca 100644 --- a/src/commonMain/kotlin/centre/sciprog/maps/compose/MapView.kt +++ b/src/commonMain/kotlin/centre/sciprog/maps/compose/MapView.kt @@ -6,12 +6,19 @@ import androidx.compose.ui.Modifier import centre.sciprog.maps.GeodeticMapCoordinates import centre.sciprog.maps.MapViewPoint + +data class MapViewConfig( + val zoomSpeed: Double = 1.0 / 3.0, +) + @Composable expect fun MapView( initialViewPoint: MapViewPoint, mapTileProvider: MapTileProvider, features: Map, onClick: (GeodeticMapCoordinates) -> Unit = {}, + //TODO consider replacing by modifier + config: MapViewConfig = MapViewConfig(), modifier: Modifier = Modifier.fillMaxSize(), ) @@ -20,10 +27,11 @@ fun MapView( initialViewPoint: MapViewPoint, mapTileProvider: MapTileProvider, onClick: (GeodeticMapCoordinates) -> Unit = {}, + config: MapViewConfig = MapViewConfig(), modifier: Modifier = Modifier.fillMaxSize(), addFeatures: @Composable() (FeatureBuilder.() -> Unit) = {}, ) { val featuresBuilder = MapFeatureBuilder() featuresBuilder.addFeatures() - MapView(initialViewPoint, mapTileProvider, featuresBuilder.build(), onClick, modifier) + MapView(initialViewPoint, mapTileProvider, featuresBuilder.build(), onClick, config, modifier) } \ 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 600ed69..2ee8101 100644 --- a/src/jvmMain/kotlin/centre/sciprog/maps/compose/MapViewJvm.kt +++ b/src/jvmMain/kotlin/centre/sciprog/maps/compose/MapViewJvm.kt @@ -43,6 +43,7 @@ actual fun MapView( mapTileProvider: MapTileProvider, features: Map, onClick: (GeodeticMapCoordinates) -> Unit, + config: MapViewConfig, modifier: Modifier, ) { @@ -50,6 +51,8 @@ actual fun MapView( val zoom: Int by derivedStateOf { viewPoint.zoom.roundToInt() } + val tileScale: Double by derivedStateOf { 2.0.pow(viewPoint.zoom - zoom) } + val mapTiles = remember { mutableStateListOf() } //var mapRectangle by remember { mutableStateOf(initialRectangle) } @@ -59,10 +62,13 @@ actual fun MapView( fun DpOffset.toMercator(): WebMercatorCoordinates = WebMercatorCoordinates( zoom, - (x - canvasSize.width / 2).value + centerCoordinates.x, - (y - canvasSize.height / 2).value + centerCoordinates.y, + (x - canvasSize.width / 2).value / tileScale + centerCoordinates.x, + (y - canvasSize.height / 2).value / tileScale + centerCoordinates.y, ) + /* + * Convert screen independent offset to GMC, adjusting for fractional zoom + */ fun DpOffset.toGeodetic() = WebMercatorProjection.toGeodetic(toMercator()) @OptIn(ExperimentalComposeUiApi::class) @@ -73,23 +79,23 @@ actual fun MapView( val change = it.changes.first() val (xPos, yPos) = change.position //compute invariant point of translation - val at = DpOffset(xPos.toDp(), yPos.toDp()).toGeodetic() - viewPoint = viewPoint.zoom(-change.scrollDelta.y.toDouble(), at) + val invariant = DpOffset(xPos.toDp(), yPos.toDp()).toGeodetic() + viewPoint = viewPoint.zoom(-change.scrollDelta.y.toDouble() * config.zoomSpeed, invariant) }.pointerInput(Unit) { detectDragGestures { _: PointerInputChange, dragAmount: Offset -> - viewPoint = viewPoint.move(-dragAmount.x, +dragAmount.y) + viewPoint = viewPoint.move(-dragAmount.x.toDp().value / tileScale, +dragAmount.y.toDp().value / tileScale) } }.fillMaxSize() // Load tiles asynchronously LaunchedEffect(viewPoint, canvasSize) { - val left = centerCoordinates.x - canvasSize.width.value / 2 - val right = centerCoordinates.x + canvasSize.width.value / 2 + val left = centerCoordinates.x - canvasSize.width.value / 2 / tileScale + val right = centerCoordinates.x + canvasSize.width.value / 2 / tileScale val horizontalIndices = mapTileProvider.toIndex(left)..mapTileProvider.toIndex(right) - val top = (centerCoordinates.y + canvasSize.height.value / 2) - val bottom = (centerCoordinates.y - canvasSize.height.value / 2) + val top = (centerCoordinates.y + canvasSize.height.value / 2 / tileScale) + val bottom = (centerCoordinates.y - canvasSize.height.value / 2 / tileScale) val verticalIndices = mapTileProvider.toIndex(bottom)..mapTileProvider.toIndex(top) mapTiles.clear() @@ -112,14 +118,15 @@ actual fun MapView( } - + // d Canvas(canvasModifier) { fun WebMercatorCoordinates.toOffset(): Offset = Offset( - (canvasSize.width / 2 - centerCoordinates.x.dp + x.dp).toPx(), - (canvasSize.height / 2 - centerCoordinates.y.dp + y.dp).toPx() + (canvasSize.width / 2 + (x.dp - centerCoordinates.x.dp) * tileScale.toFloat()).toPx(), + (canvasSize.height / 2 + (y.dp - centerCoordinates.y.dp) * tileScale.toFloat()).toPx() ) + //Convert GMC to offset in pixels (not DP), adjusting for zoom fun GeodeticMapCoordinates.toOffset(): Offset = WebMercatorProjection.toMercator(this, zoom).toOffset() @@ -160,12 +167,15 @@ actual fun MapView( logger.debug { "Recalculate canvas. Size: $size" } } clipRect { - val tileSize = IntSize(mapTileProvider.tileSize.dp.roundToPx(), mapTileProvider.tileSize.dp.roundToPx()) + val tileSize = IntSize( + (mapTileProvider.tileSize.dp * tileScale.toFloat()).roundToPx(), + (mapTileProvider.tileSize.dp * tileScale.toFloat()).roundToPx() + ) mapTiles.forEach { (id, image) -> //converting back from tile index to screen offset 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() + (canvasSize.width / 2 + (mapTileProvider.toCoordinate(id.i).dp - centerCoordinates.x.dp) * tileScale.toFloat()).roundToPx(), + (canvasSize.height / 2 + (mapTileProvider.toCoordinate(id.j).dp - centerCoordinates.y.dp) * tileScale.toFloat()).roundToPx() ) drawImage( image = image,