From 13b44a8091b1db1e0cd0132a8aed256fee60a0e1 Mon Sep 17 00:00:00 2001 From: "a.kalmakhanov" Date: Thu, 11 Aug 2022 11:58:26 +0600 Subject: [PATCH] State Encapsulation In progress --- demo/maps/src/jvmMain/kotlin/Main.kt | 60 ++--- .../center/sciprog/maps/compose/MapFeature.kt | 4 +- .../center/sciprog/maps/compose/MapView.kt | 48 ++-- .../sciprog/maps/compose/MapViewState.kt | 179 ++++++++++++++ .../center/sciprog/maps/compose/MapViewJvm.kt | 230 +++++------------- 5 files changed, 297 insertions(+), 224 deletions(-) create mode 100644 maps-kt-compose/src/commonMain/kotlin/center/sciprog/maps/compose/MapViewState.kt diff --git a/demo/maps/src/jvmMain/kotlin/Main.kt b/demo/maps/src/jvmMain/kotlin/Main.kt index 25721ff..866bf5b 100644 --- a/demo/maps/src/jvmMain/kotlin/Main.kt +++ b/demo/maps/src/jvmMain/kotlin/Main.kt @@ -49,34 +49,33 @@ fun App() { var centerCoordinates by remember { mutableStateOf(null) } - val markers = (1..1_000_000).map { - val position = GeodeticMapCoordinates.ofDegrees( - latitude = Random.nextDouble(-90.0, 90.0), - longitude = Random.nextDouble(0.0, 180.0) - ) - MapDrawFeature( - position = position, - getBoundingBox = { - GmcBox.withCenter( - center = position, - width = Distance(0.001), - height = Distance(0.001) - ) - } - ) { - drawRoundRect( - color = Color.Yellow, - size = Size(10f, 10f) - ) - } - } - - MapView( +// val markers = (1..1_000_000).map { +// val position = GeodeticMapCoordinates.ofDegrees( +// latitude = Random.nextDouble(-90.0, 90.0), +// longitude = Random.nextDouble(0.0, 180.0) +// ) +// MapDrawFeature( +// position = position, +// computeBoundingBox = { +// GmcBox.withCenter( +// center = position, +// width = Distance(0.001), +// height = Distance(0.001) +// ) +// } +// ) { +// drawRoundRect( +// color = Color.Yellow, +// size = Size(1f, 1f) +// ) +// } +// } + val state = MapViewState( mapTileProvider = mapTileProvider, - initialViewPoint = viewPoint, + computeViewPoint = { viewPoint }, config = MapViewConfig( inferViewBoxFromFeatures = true, - onViewChange = { centerCoordinates = focus } + onViewChange = { centerCoordinates = focus }, ) ) { val pointOne = 55.568548 to 37.568604 @@ -107,9 +106,11 @@ fun App() { drawLine(start = Offset(-10f, 10f), end = Offset(10f, -10f), color = Color.Red) } - featureSelector { zoom -> - markers.groupBy { } - } +// markers.forEach { feature -> +// featureSelector { +// feature +// } +// } arc(pointOne, Distance(10.0), 0f, PI) @@ -135,6 +136,9 @@ fun App() { } } } + MapView( + mapViewState = state + ) } } diff --git a/maps-kt-compose/src/commonMain/kotlin/center/sciprog/maps/compose/MapFeature.kt b/maps-kt-compose/src/commonMain/kotlin/center/sciprog/maps/compose/MapFeature.kt index c8fce51..eadd12e 100644 --- a/maps-kt-compose/src/commonMain/kotlin/center/sciprog/maps/compose/MapFeature.kt +++ b/maps-kt-compose/src/commonMain/kotlin/center/sciprog/maps/compose/MapFeature.kt @@ -40,10 +40,10 @@ public class MapFeatureSelector( public class MapDrawFeature( public val position: GeodeticMapCoordinates, override val zoomRange: IntRange = defaultZoomRange, - private val getBoundingBox: (zoom: Int) -> GmcBox, + private val computeBoundingBox: (zoom: Int) -> GmcBox, public val drawFeature: DrawScope.() -> Unit, ) : MapFeature { - override fun getBoundingBox(zoom: Int): GmcBox = getBoundingBox(zoom) + override fun getBoundingBox(zoom: Int): GmcBox = computeBoundingBox(zoom) } public class MapCircleFeature( diff --git a/maps-kt-compose/src/commonMain/kotlin/center/sciprog/maps/compose/MapView.kt b/maps-kt-compose/src/commonMain/kotlin/center/sciprog/maps/compose/MapView.kt index 80e5287..22dfff1 100644 --- a/maps-kt-compose/src/commonMain/kotlin/center/sciprog/maps/compose/MapView.kt +++ b/maps-kt-compose/src/commonMain/kotlin/center/sciprog/maps/compose/MapView.kt @@ -23,11 +23,8 @@ public data class MapViewConfig( @Composable public expect fun MapView( - mapTileProvider: MapTileProvider, - computeViewPoint: (canvasSize: DpSize) -> MapViewPoint, - features: Map, - config: MapViewConfig = MapViewConfig(), - modifier: Modifier = Modifier.fillMaxSize(), + mapViewState: MapViewState, + modifier: Modifier = Modifier, ) @Composable @@ -42,23 +39,26 @@ public fun MapView( val featuresBuilder = MapFeatureBuilderImpl(features) featuresBuilder.buildFeatures() MapView( - mapTileProvider, - { initialViewPoint }, - featuresBuilder.build(), - config, - modifier + mapViewState = MapViewState( + mapTileProvider = mapTileProvider, + computeViewPoint = { initialViewPoint }, + features = featuresBuilder.build(), + config = config, + ), + modifier = modifier ) } -internal fun GmcBox.computeViewPoint(mapTileProvider: MapTileProvider): (canvasSize: DpSize) -> MapViewPoint = { canvasSize -> - val zoom = log2( - min( - canvasSize.width.value / width, - canvasSize.height.value / height - ) * PI / mapTileProvider.tileSize - ) - MapViewPoint(center, zoom) -} +internal fun GmcBox.computeViewPoint(mapTileProvider: MapTileProvider): (canvasSize: DpSize) -> MapViewPoint = + { canvasSize -> + val zoom = log2( + min( + canvasSize.width.value / width, + canvasSize.height.value / height + ) * PI / mapTileProvider.tileSize + ) + MapViewPoint(center, zoom) + } @Composable public fun MapView( @@ -72,10 +72,12 @@ public fun MapView( val featuresBuilder = MapFeatureBuilderImpl(features) featuresBuilder.buildFeatures() MapView( - mapTileProvider, - box.computeViewPoint(mapTileProvider), - featuresBuilder.build(), - config, + mapViewState = MapViewState( + config = config, + mapTileProvider = mapTileProvider, + features = featuresBuilder.build(), + computeViewPoint = box.computeViewPoint(mapTileProvider), + ), modifier ) } \ No newline at end of file diff --git a/maps-kt-compose/src/commonMain/kotlin/center/sciprog/maps/compose/MapViewState.kt b/maps-kt-compose/src/commonMain/kotlin/center/sciprog/maps/compose/MapViewState.kt new file mode 100644 index 0000000..ec5b8ad --- /dev/null +++ b/maps-kt-compose/src/commonMain/kotlin/center/sciprog/maps/compose/MapViewState.kt @@ -0,0 +1,179 @@ +package center.sciprog.maps.compose + +import androidx.compose.runtime.* +import androidx.compose.runtime.snapshots.SnapshotStateList +import androidx.compose.ui.geometry.Offset +import androidx.compose.ui.geometry.Rect +import androidx.compose.ui.graphics.Path +import androidx.compose.ui.graphics.drawscope.DrawScope +import androidx.compose.ui.graphics.drawscope.Stroke +import androidx.compose.ui.graphics.drawscope.drawIntoCanvas +import androidx.compose.ui.graphics.drawscope.translate +import androidx.compose.ui.graphics.nativeCanvas +import androidx.compose.ui.unit.Density +import androidx.compose.ui.unit.DpOffset +import androidx.compose.ui.unit.DpSize +import androidx.compose.ui.unit.dp +import center.sciprog.maps.coordinates.* +import center.sciprog.maps.coordinates.MercatorProjection.Companion.toMercator +import kotlin.math.* + +@Composable +public fun MapViewState( + computeViewPoint: (canvasSize: DpSize) -> MapViewPoint, + mapTileProvider: MapTileProvider, + config: MapViewConfig = MapViewConfig(), + features: Map = emptyMap(), + buildFeatures: @Composable (MapFeatureBuilder.() -> Unit) = {}, +): MapViewState { + val featuresBuilder = MapFeatureBuilderImpl(features) + featuresBuilder.buildFeatures() + return MapViewState( + computeViewPoint = computeViewPoint, + mapTileProvider = mapTileProvider, + config = config, + features = featuresBuilder.build() + ) +} + +public class MapViewState( + public val computeViewPoint: (canvasSize: DpSize) -> MapViewPoint, + public val mapTileProvider: MapTileProvider, + public val config: MapViewConfig = MapViewConfig(), + public val features: Map = emptyMap(), +) { + public var canvasSize: DpSize by mutableStateOf(DpSize(512.dp, 512.dp)) + public var viewPointInternal: MapViewPoint? by mutableStateOf(null) + public val viewPoint: MapViewPoint by derivedStateOf { + viewPointInternal ?: if (config.inferViewBoxFromFeatures) { + features.values.computeBoundingBox(1)?.let { box -> + val zoom = log2( + min( + canvasSize.width.value / box.width, + canvasSize.height.value / box.height + ) * PI / mapTileProvider.tileSize + ) + MapViewPoint(box.center, zoom) + } ?: computeViewPoint(canvasSize) + } else { + computeViewPoint(canvasSize) + } + } + public val zoom: Int by derivedStateOf { floor(viewPoint.zoom).toInt() } + + public val tileScale: Double by derivedStateOf { 2.0.pow(viewPoint.zoom - zoom) } + + public val mapTiles: SnapshotStateList = mutableStateListOf() + + public val centerCoordinates: WebMercatorCoordinates by derivedStateOf { + WebMercatorProjection.toMercator( + viewPoint.focus, + zoom + ) + } + + public fun DpOffset.toMercator(): WebMercatorCoordinates = WebMercatorCoordinates( + zoom, + (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 + */ + public fun DpOffset.toGeodetic(): GeodeticMapCoordinates = + with(this@MapViewState) { WebMercatorProjection.toGeodetic(toMercator()) } + + // Selection rectangle. If null - no selection + public var selectRect: Rect? by mutableStateOf(null) + + public fun WebMercatorCoordinates.toOffset(density: Density): Offset = + with(density) { + with(this@MapViewState) { + Offset( + (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 + public fun GeodeticMapCoordinates.toOffset(density: Density): Offset = + WebMercatorProjection.toMercator(this, zoom).toOffset(density) + + public 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(this@drawFeature) + ) + is MapRectangleFeature -> drawRect( + feature.color, + topLeft = feature.center.toOffset(this@drawFeature) - Offset( + feature.size.width.toPx() / 2, + feature.size.height.toPx() / 2 + ), + size = feature.size.toSize() + ) + is MapLineFeature -> drawLine( + feature.color, + feature.a.toOffset(this@drawFeature), + feature.b.toOffset(this@drawFeature) + ) + is MapArcFeature -> { + val topLeft = feature.oval.topLeft.toOffset(this@drawFeature) + val bottomRight = feature.oval.bottomRight.toOffset(this@drawFeature) + + val path = Path().apply { + addArcRad(Rect(topLeft, bottomRight), feature.startAngle, feature.endAngle - feature.startAngle) + } + + drawPath(path, color = feature.color, style = Stroke()) + + } + is MapBitmapImageFeature -> drawImage( + image = feature.image, + topLeft = feature.position.toOffset(this@drawFeature) + ) + is MapVectorImageFeature -> { + val offset = feature.position.toOffset(this@drawFeature) + val size = feature.size.toSize() + translate(offset.x - size.width / 2, offset.y - size.height / 2) { + with(feature.painter) { + draw(size) + } + } + } + is MapTextFeature -> drawIntoCanvas { canvas -> + val offset = toOffset(feature.position, mapViewState) + canvas.nativeCanvas.drawString( + feature.text, + offset.x + 5, + offset.y - 5, + Font().apply(feature.fontConfig), + feature.color.toPaint() + ) + } + is MapDrawFeature -> { + val offset = toOffset(feature.position, mapViewState) + translate(offset.x, offset.y) { + feature.drawFeature(this) + } + } + is MapFeatureGroup -> { + feature.children.values.forEach { + drawFeature(zoom, it) + } + } + else -> { + logger.error { "Unrecognized feature type: ${feature::class}" } + } + } + } +} + + + + diff --git a/maps-kt-compose/src/jvmMain/kotlin/center/sciprog/maps/compose/MapViewJvm.kt b/maps-kt-compose/src/jvmMain/kotlin/center/sciprog/maps/compose/MapViewJvm.kt index d12d568..ccea0f1 100644 --- a/maps-kt-compose/src/jvmMain/kotlin/center/sciprog/maps/compose/MapViewJvm.kt +++ b/maps-kt-compose/src/jvmMain/kotlin/center/sciprog/maps/compose/MapViewJvm.kt @@ -48,60 +48,9 @@ private val logger = KotlinLogging.logger("MapView") @Composable public actual fun MapView( - mapTileProvider: MapTileProvider, - computeViewPoint: (canvasSize: DpSize) -> MapViewPoint, - features: Map, - config: MapViewConfig, + mapViewState: MapViewState, modifier: Modifier, ) { - var canvasSize by remember { mutableStateOf(DpSize(512.dp, 512.dp)) } - - var viewPointInternal: MapViewPoint? by remember { - mutableStateOf(null) - } - - if (config.resetViewPoint) { - viewPointInternal = null - } - - val viewPoint: MapViewPoint by derivedStateOf { - viewPointInternal ?: if (config.inferViewBoxFromFeatures) { - features.values.computeBoundingBox(1)?.let { box -> - val zoom = log2( - min( - canvasSize.width.value / box.width, - canvasSize.height.value / box.height - ) * PI / mapTileProvider.tileSize - ) - MapViewPoint(box.center, zoom) - } ?: computeViewPoint(canvasSize) - } else { - computeViewPoint(canvasSize) - } - } - - val zoom: Int by derivedStateOf { floor(viewPoint.zoom).toInt() } - - val tileScale: Double by derivedStateOf { 2.0.pow(viewPoint.zoom - zoom) } - - val mapTiles = remember { mutableStateListOf() } - - val centerCoordinates by derivedStateOf { WebMercatorProjection.toMercator(viewPoint.focus, zoom) } - - fun DpOffset.toMercator(): WebMercatorCoordinates = WebMercatorCoordinates( - zoom, - (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()) - - // Selection rectangle. If null - no selection - var selectRect by remember { mutableStateOf(null) } - @OptIn(ExperimentalComposeUiApi::class) val canvasModifier = modifier.pointerInput(Unit) { forEachGesture { @@ -113,11 +62,11 @@ public actual fun MapView( if (event.buttons.isPrimaryPressed) { //Evaluating selection frame if (event.keyboardModifiers.isShiftPressed) { - selectRect = Rect(change.position, change.position) + mapViewState.selectRect = Rect(change.position, change.position) drag(change.id) { dragChange -> - selectRect?.let { rect -> + mapViewState.selectRect?.let { rect -> val offset = dragChange.position - selectRect = Rect( + mapViewState.selectRect = Rect( min(offset.x, rect.left), min(offset.y, rect.top), max(offset.x, rect.right), @@ -125,33 +74,41 @@ public actual fun MapView( ) } } - selectRect?.let { rect -> + mapViewState.selectRect?.let { rect -> //Use selection override if it is defined - val gmcBox = GmcBox( - rect.topLeft.toDpOffset().toGeodetic(), - rect.bottomRight.toDpOffset().toGeodetic() - ) - config.onSelect(gmcBox) - if (config.zoomOnSelect) { - val newViewPoint = gmcBox.computeViewPoint(mapTileProvider).invoke(canvasSize) - - config.onViewChange(newViewPoint) - viewPointInternal = newViewPoint + val gmcBox = with(mapViewState) { + GmcBox( + rect.topLeft.toDpOffset().toGeodetic(), + rect.bottomRight.toDpOffset().toGeodetic() + ) } - selectRect = null + mapViewState.config.onSelect(gmcBox) + if (mapViewState.config.zoomOnSelect) { + val newViewPoint = gmcBox.computeViewPoint(mapViewState.mapTileProvider) + .invoke(mapViewState.canvasSize) + + mapViewState.config.onViewChange(newViewPoint) + mapViewState.viewPointInternal = newViewPoint + } + mapViewState.selectRect = null } } else { val dragStart = change.position val dpPos = DpOffset(dragStart.x.toDp(), dragStart.y.toDp()) - config.onClick(MapViewPoint(dpPos.toGeodetic(), viewPoint.zoom)) + mapViewState.config.onClick( + MapViewPoint( + with(mapViewState) { dpPos.toGeodetic() }, + mapViewState.viewPoint.zoom + ) + ) drag(change.id) { dragChange -> val dragAmount = dragChange.position - dragChange.previousPosition - val newViewPoint = viewPoint.move( - -dragAmount.x.toDp().value / tileScale, - +dragAmount.y.toDp().value / tileScale + val newViewPoint = mapViewState.viewPoint.move( + -dragAmount.x.toDp().value / mapViewState.tileScale, + +dragAmount.y.toDp().value / mapViewState.tileScale ) - config.onViewChange(newViewPoint) - viewPointInternal = newViewPoint + mapViewState.config.onViewChange(newViewPoint) + mapViewState.viewPointInternal = newViewPoint } } } @@ -162,37 +119,42 @@ public actual fun MapView( val change = it.changes.first() val (xPos, yPos) = change.position //compute invariant point of translation - val invariant = DpOffset(xPos.toDp(), yPos.toDp()).toGeodetic() - val newViewPoint = viewPoint.zoom(-change.scrollDelta.y.toDouble() * config.zoomSpeed, invariant) - config.onViewChange(newViewPoint) - viewPointInternal = newViewPoint + val invariant = with(mapViewState) { DpOffset(xPos.toDp(), yPos.toDp()).toGeodetic() } + val newViewPoint = + mapViewState.viewPoint.zoom(-change.scrollDelta.y.toDouble() * mapViewState.config.zoomSpeed, invariant) + mapViewState.config.onViewChange(newViewPoint) + mapViewState.viewPointInternal = newViewPoint }.fillMaxSize() // Load tiles asynchronously - LaunchedEffect(viewPoint, canvasSize) { - with(mapTileProvider) { - val indexRange = 0 until 2.0.pow(zoom).toInt() + LaunchedEffect(mapViewState.viewPoint, mapViewState.canvasSize) { + with(mapViewState.mapTileProvider) { + val indexRange = 0 until 2.0.pow(mapViewState.zoom).toInt() - val left = centerCoordinates.x - canvasSize.width.value / 2 / tileScale - val right = centerCoordinates.x + canvasSize.width.value / 2 / tileScale + val left = + mapViewState.centerCoordinates.x - mapViewState.canvasSize.width.value / 2 / mapViewState.tileScale + val right = + mapViewState.centerCoordinates.x + mapViewState.canvasSize.width.value / 2 / mapViewState.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 top = + (mapViewState.centerCoordinates.y + mapViewState.canvasSize.height.value / 2 / mapViewState.tileScale) + val bottom = + (mapViewState.centerCoordinates.y - mapViewState.canvasSize.height.value / 2 / mapViewState.tileScale) val verticalIndices: IntRange = (toIndex(bottom)..toIndex(top)).intersect(indexRange) - mapTiles.clear() + mapViewState.mapTiles.clear() for (j in verticalIndices) { for (i in horizontalIndices) { - val id = TileId(zoom, i, j) + val id = TileId(mapViewState.zoom, i, j) //start all val deferred = loadTileAsync(id) //wait asynchronously for it to finish launch { try { - mapTiles += deferred.await() + mapViewState.mapTiles += deferred.await() } catch (ex: Exception) { if (ex !is CancellationException) { //displaying the error is maps responsibility @@ -207,94 +169,20 @@ public actual fun MapView( Canvas(canvasModifier) { - fun WebMercatorCoordinates.toOffset(): Offset = Offset( - (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() - - - 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 MapRectangleFeature -> drawRect( - feature.color, - topLeft = feature.center.toOffset() - Offset( - feature.size.width.toPx() / 2, - feature.size.height.toPx() / 2 - ), - size = feature.size.toSize() - ) - is MapLineFeature -> drawLine(feature.color, feature.a.toOffset(), feature.b.toOffset()) - is MapArcFeature -> { - val topLeft = feature.oval.topLeft.toOffset() - val bottomRight = feature.oval.bottomRight.toOffset() - - val path = Path().apply { - addArcRad(Rect(topLeft, bottomRight), feature.startAngle, feature.endAngle - feature.startAngle) - } - - drawPath(path, color = feature.color, style = Stroke()) - - } - is MapBitmapImageFeature -> drawImage(feature.image, feature.position.toOffset()) - is MapVectorImageFeature -> { - val offset = feature.position.toOffset() - val size = feature.size.toSize() - translate(offset.x - size.width / 2, offset.y - size.height / 2) { - with(feature.painter) { - draw(size) - } - } - } - is MapTextFeature -> drawIntoCanvas { canvas -> - val offset = feature.position.toOffset() - canvas.nativeCanvas.drawString( - feature.text, - offset.x + 5, - offset.y - 5, - Font().apply(feature.fontConfig), - feature.color.toPaint() - ) - } - is MapDrawFeature -> { - val offset = feature.position.toOffset() - translate(offset.x, offset.y) { - feature.drawFeature(this) - } - } - is MapFeatureGroup -> { - feature.children.values.forEach { - drawFeature(zoom, it) - } - } - else -> { - logger.error { "Unrecognized feature type: ${feature::class}" } - } - } - } - - if (canvasSize != size.toDpSize()) { - canvasSize = size.toDpSize() + if (mapViewState.canvasSize != size.toDpSize()) { + mapViewState.canvasSize = size.toDpSize() logger.debug { "Recalculate canvas. Size: $size" } } clipRect { val tileSize = IntSize( - ceil((mapTileProvider.tileSize.dp * tileScale.toFloat()).toPx()).toInt(), - ceil((mapTileProvider.tileSize.dp * tileScale.toFloat()).toPx()).toInt() + ceil((mapViewState.mapTileProvider.tileSize.dp * mapViewState.tileScale.toFloat()).toPx()).toInt(), + ceil((mapViewState.mapTileProvider.tileSize.dp * mapViewState.tileScale.toFloat()).toPx()).toInt() ) - mapTiles.forEach { (id, image) -> + mapViewState.mapTiles.forEach { (id, image) -> //converting back from tile index to screen offset val offset = IntOffset( - (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() + (mapViewState.canvasSize.width / 2 + (mapViewState.mapTileProvider.toCoordinate(id.i).dp - mapViewState.centerCoordinates.x.dp) * mapViewState.tileScale.toFloat()).roundToPx(), + (mapViewState.canvasSize.height / 2 + (mapViewState.mapTileProvider.toCoordinate(id.j).dp - mapViewState.centerCoordinates.y.dp) * mapViewState.tileScale.toFloat()).roundToPx() ) drawImage( image = image, @@ -302,11 +190,11 @@ public actual fun MapView( dstSize = tileSize ) } - features.values.filter { zoom in it.zoomRange }.forEach { feature -> - drawFeature(zoom, feature) + mapViewState.features.values.filter { mapViewState.zoom in it.zoomRange }.forEach { feature -> + drawFeature(mapViewState.zoom, feature) } } - selectRect?.let { rect -> + mapViewState.selectRect?.let { rect -> drawRect( color = Color.Blue, topLeft = rect.topLeft,