From 1541fb4f3959c1327a6f380bd35bdaa1651a3cc4 Mon Sep 17 00:00:00 2001 From: Alexander Nozik Date: Thu, 14 Jul 2022 10:36:16 +0300 Subject: [PATCH 1/3] [WIP] bounding boxes --- .../sciprog/maps/GeodeticMapCoordinates.kt | 2 ++ .../kotlin/centre/sciprog/maps/GmcBox.kt | 35 +++++++++++++++++++ .../centre/sciprog/maps/compose/MapFeature.kt | 33 +++++++++++++---- 3 files changed, 63 insertions(+), 7 deletions(-) create mode 100644 src/commonMain/kotlin/centre/sciprog/maps/GmcBox.kt diff --git a/src/commonMain/kotlin/centre/sciprog/maps/GeodeticMapCoordinates.kt b/src/commonMain/kotlin/centre/sciprog/maps/GeodeticMapCoordinates.kt index 9f73bc2..d569e3e 100644 --- a/src/commonMain/kotlin/centre/sciprog/maps/GeodeticMapCoordinates.kt +++ b/src/commonMain/kotlin/centre/sciprog/maps/GeodeticMapCoordinates.kt @@ -43,6 +43,8 @@ public class GeodeticMapCoordinates private constructor(public val latitude: Dou } } +internal typealias Gmc = GeodeticMapCoordinates + //public interface GeoToScreenConversion { // public fun getScreenX(gmc: GeodeticMapCoordinates): Double diff --git a/src/commonMain/kotlin/centre/sciprog/maps/GmcBox.kt b/src/commonMain/kotlin/centre/sciprog/maps/GmcBox.kt new file mode 100644 index 0000000..501d600 --- /dev/null +++ b/src/commonMain/kotlin/centre/sciprog/maps/GmcBox.kt @@ -0,0 +1,35 @@ +package centre.sciprog.maps + +import kotlin.math.max +import kotlin.math.min + +class GmcBox(val a: GeodeticMapCoordinates, val b: GeodeticMapCoordinates) + +fun GmcBox(latitudes: ClosedFloatingPointRange, longitudes: ClosedFloatingPointRange) = GmcBox( + Gmc.ofRadians(latitudes.start, longitudes.start), + Gmc.ofRadians(latitudes.endInclusive, longitudes.endInclusive) +) + +val GmcBox.center + get() = GeodeticMapCoordinates.ofRadians( + (a.latitude + b.latitude) / 2, + (a.longitude + b.longitude) / 2 + ) + +val GmcBox.left get() = min(a.longitude, b.longitude) +val GmcBox.right get() = max(a.longitude, b.longitude) + +val GmcBox.top get() = max(a.latitude, b.latitude) +val GmcBox.bottom get() = min(a.latitude, b.latitude) + +/** + * Compute a minimal bounding box including all given boxes + */ +fun Iterable.wrapAll(): GmcBox { + //TODO optimize computation + val minLat = minOf { it.bottom } + val maxLat = maxOf { it.top } + val minLong = minOf { it.left } + val maxLong = maxOf { it.right } + return GmcBox(maxLat..maxLat, minLong..maxLong) +} \ No newline at end of file diff --git a/src/commonMain/kotlin/centre/sciprog/maps/compose/MapFeature.kt b/src/commonMain/kotlin/centre/sciprog/maps/compose/MapFeature.kt index 3ec5b0f..1dcc333 100644 --- a/src/commonMain/kotlin/centre/sciprog/maps/compose/MapFeature.kt +++ b/src/commonMain/kotlin/centre/sciprog/maps/compose/MapFeature.kt @@ -9,9 +9,15 @@ import androidx.compose.ui.graphics.vector.VectorPainter import androidx.compose.ui.graphics.vector.rememberVectorPainter import androidx.compose.ui.unit.IntSize import centre.sciprog.maps.GeodeticMapCoordinates +import centre.sciprog.maps.GmcBox +import centre.sciprog.maps.wrapAll //TODO replace zoom range with zoom-based representation change -sealed class MapFeature(val zoomRange: IntRange) +sealed class MapFeature(val zoomRange: IntRange) { + abstract fun getBoundingBox(zoom: Int): GmcBox +} + +fun Iterable.computeBoundingBox(zoom: Int): GmcBox = map { it.getBoundingBox(zoom) }.wrapAll() internal fun Pair.toCoordinates() = GeodeticMapCoordinates.ofDegrees(first, second) @@ -20,35 +26,46 @@ internal val defaultZoomRange = 1..18 /** * A feature that decides what to show depending on the zoom value (it could change size of shape) */ -class MapFeatureSelector(val selector: (zoom: Int) -> MapFeature) : MapFeature(defaultZoomRange) +class MapFeatureSelector(val selector: (zoom: Int) -> MapFeature) : MapFeature(defaultZoomRange) { + override fun getBoundingBox(zoom: Int): GmcBox = selector(zoom).getBoundingBox(zoom) + +} class MapCircleFeature( val center: GeodeticMapCoordinates, zoomRange: IntRange = defaultZoomRange, val size: Float = 5f, val color: Color = Color.Red, -) : MapFeature(zoomRange) +) : MapFeature(zoomRange) { + override fun getBoundingBox(zoom: Int): GmcBox = GmcBox(center, center) +} class MapLineFeature( val a: GeodeticMapCoordinates, val b: GeodeticMapCoordinates, zoomRange: IntRange = defaultZoomRange, val color: Color = Color.Red, -) : MapFeature(zoomRange) +) : MapFeature(zoomRange) { + override fun getBoundingBox(zoom: Int): GmcBox = GmcBox(a, b) +} class MapTextFeature( val position: GeodeticMapCoordinates, val text: String, zoomRange: IntRange = defaultZoomRange, val color: Color = Color.Red, -) : MapFeature(zoomRange) +) : MapFeature(zoomRange) { + override fun getBoundingBox(zoom: Int): GmcBox = GmcBox(position, position) +} class MapBitmapImageFeature( val position: GeodeticMapCoordinates, val image: ImageBitmap, val size: IntSize = IntSize(15, 15), zoomRange: IntRange = defaultZoomRange, -) : MapFeature(zoomRange) +) : MapFeature(zoomRange) { + override fun getBoundingBox(zoom: Int): GmcBox = GmcBox(position, position) +} class MapVectorImageFeature internal constructor( @@ -56,7 +73,9 @@ class MapVectorImageFeature internal constructor( val painter: VectorPainter, val size: Size, zoomRange: IntRange = defaultZoomRange, -) : MapFeature(zoomRange) +) : MapFeature(zoomRange) { + override fun getBoundingBox(zoom: Int): GmcBox = GmcBox(position, position) +} @Composable fun MapVectorImageFeature( From 9392c0f991b9440824b74e482a67aa2df8174a0c Mon Sep 17 00:00:00 2001 From: Alexander Nozik Date: Thu, 14 Jul 2022 20:19:57 +0300 Subject: [PATCH 2/3] Working box by features (mostly) --- .../kotlin/centre/sciprog/maps/GmcBox.kt | 7 +- .../sciprog/maps/compose/FeatureBuilder.kt | 6 +- .../centre/sciprog/maps/compose/MapView.kt | 67 ++++++++++++++++--- src/jvmMain/kotlin/Main.kt | 11 ++- .../centre/sciprog/maps/compose/MapViewJvm.kt | 19 +++--- 5 files changed, 89 insertions(+), 21 deletions(-) diff --git a/src/commonMain/kotlin/centre/sciprog/maps/GmcBox.kt b/src/commonMain/kotlin/centre/sciprog/maps/GmcBox.kt index 501d600..cb2dc24 100644 --- a/src/commonMain/kotlin/centre/sciprog/maps/GmcBox.kt +++ b/src/commonMain/kotlin/centre/sciprog/maps/GmcBox.kt @@ -1,5 +1,6 @@ package centre.sciprog.maps +import kotlin.math.abs import kotlin.math.max import kotlin.math.min @@ -22,6 +23,10 @@ val GmcBox.right get() = max(a.longitude, b.longitude) val GmcBox.top get() = max(a.latitude, b.latitude) val GmcBox.bottom get() = min(a.latitude, b.latitude) +//TODO take curvature into account +val GmcBox.width get() = abs(a.longitude - b.longitude) +val GmcBox.height get() = abs(a.latitude - b.latitude) + /** * Compute a minimal bounding box including all given boxes */ @@ -31,5 +36,5 @@ fun Iterable.wrapAll(): GmcBox { val maxLat = maxOf { it.top } val minLong = minOf { it.left } val maxLong = maxOf { it.right } - return GmcBox(maxLat..maxLat, minLong..maxLong) + return GmcBox(minLat..maxLat, minLong..maxLong) } \ No newline at end of file diff --git a/src/commonMain/kotlin/centre/sciprog/maps/compose/FeatureBuilder.kt b/src/commonMain/kotlin/centre/sciprog/maps/compose/FeatureBuilder.kt index 36f023d..040a110 100644 --- a/src/commonMain/kotlin/centre/sciprog/maps/compose/FeatureBuilder.kt +++ b/src/commonMain/kotlin/centre/sciprog/maps/compose/FeatureBuilder.kt @@ -15,7 +15,11 @@ interface FeatureBuilder { fun build(): SnapshotStateMap } -internal class MapFeatureBuilder(private val content: SnapshotStateMap = mutableStateMapOf()) : FeatureBuilder { +internal class MapFeatureBuilder(initialFeatures: Map) : FeatureBuilder { + + private val content: SnapshotStateMap = mutableStateMapOf().apply { + putAll(initialFeatures) + } private fun generateID(feature: MapFeature): FeatureId = "@feature[${feature.hashCode().toUInt()}]" override fun addFeature(id: FeatureId?, feature: MapFeature): FeatureId { diff --git a/src/commonMain/kotlin/centre/sciprog/maps/compose/MapView.kt b/src/commonMain/kotlin/centre/sciprog/maps/compose/MapView.kt index 91c2fca..6e3ba7b 100644 --- a/src/commonMain/kotlin/centre/sciprog/maps/compose/MapView.kt +++ b/src/commonMain/kotlin/centre/sciprog/maps/compose/MapView.kt @@ -3,8 +3,11 @@ package centre.sciprog.maps.compose import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier -import centre.sciprog.maps.GeodeticMapCoordinates -import centre.sciprog.maps.MapViewPoint +import androidx.compose.ui.unit.DpSize +import centre.sciprog.maps.* +import kotlin.math.PI +import kotlin.math.log2 +import kotlin.math.min data class MapViewConfig( @@ -13,8 +16,8 @@ data class MapViewConfig( @Composable expect fun MapView( - initialViewPoint: MapViewPoint, mapTileProvider: MapTileProvider, + computeViewPoint: (canvasSize: DpSize) -> MapViewPoint, features: Map, onClick: (GeodeticMapCoordinates) -> Unit = {}, //TODO consider replacing by modifier @@ -24,14 +27,62 @@ expect fun MapView( @Composable fun MapView( - initialViewPoint: MapViewPoint, mapTileProvider: MapTileProvider, + initialViewPoint: MapViewPoint, + features: Map = emptyMap(), onClick: (GeodeticMapCoordinates) -> Unit = {}, config: MapViewConfig = MapViewConfig(), modifier: Modifier = Modifier.fillMaxSize(), - addFeatures: @Composable() (FeatureBuilder.() -> Unit) = {}, + buildFeatures: @Composable (FeatureBuilder.() -> Unit) = {}, ) { - val featuresBuilder = MapFeatureBuilder() - featuresBuilder.addFeatures() - MapView(initialViewPoint, mapTileProvider, featuresBuilder.build(), onClick, config, modifier) + val featuresBuilder = MapFeatureBuilder(features) + featuresBuilder.buildFeatures() + MapView(mapTileProvider, { initialViewPoint }, featuresBuilder.build(), onClick, config, modifier) +} + +@Composable +fun MapView( + mapTileProvider: MapTileProvider, + box: GmcBox, + features: Map = emptyMap(), + onClick: (GeodeticMapCoordinates) -> Unit = {}, + config: MapViewConfig = MapViewConfig(), + modifier: Modifier = Modifier.fillMaxSize(), + buildFeatures: @Composable (FeatureBuilder.() -> Unit) = {}, +) { + val featuresBuilder = MapFeatureBuilder(features) + featuresBuilder.buildFeatures() + val computeViewPoint: (canvasSize: DpSize) -> MapViewPoint = { canvasSize -> + val zoom = log2( + min( + canvasSize.width.value / box.width, + canvasSize.height.value / box.height + ) * PI / mapTileProvider.tileSize + ) + MapViewPoint(box.center, zoom) + } + MapView(mapTileProvider, computeViewPoint, featuresBuilder.build(), onClick, config, modifier) +} + +/** + * Create a MapView with initial [MapViewPoint] inferred from features + * + * @param defaultZoom the zoom, for which the bounding box is computed + */ +@Composable +fun MapViewWithFeatures( + mapTileProvider: MapTileProvider, + features: Map = emptyMap(), + onClick: (GeodeticMapCoordinates) -> Unit = {}, + config: MapViewConfig = MapViewConfig(), + defaultZoom: Int = 1, + modifier: Modifier = Modifier.fillMaxSize(), + buildFeatures: @Composable (FeatureBuilder.() -> Unit), +) { + val featuresBuilder = MapFeatureBuilder(features) + featuresBuilder.buildFeatures() + val featureSet = featuresBuilder.build() + if(featureSet.isEmpty()) error("Can't create `MapViewWithFeatures` from empty feature set") + val box: GmcBox = featureSet.values.map { it.getBoundingBox(defaultZoom) }.wrapAll() + MapView(mapTileProvider, box, features, onClick, config, modifier, buildFeatures) } \ No newline at end of file diff --git a/src/jvmMain/kotlin/Main.kt b/src/jvmMain/kotlin/Main.kt index 756d00d..393bf95 100644 --- a/src/jvmMain/kotlin/Main.kt +++ b/src/jvmMain/kotlin/Main.kt @@ -10,6 +10,7 @@ import androidx.compose.ui.graphics.Color import androidx.compose.ui.window.Window import androidx.compose.ui.window.application import centre.sciprog.maps.GeodeticMapCoordinates +import centre.sciprog.maps.GmcBox import centre.sciprog.maps.MapViewPoint import centre.sciprog.maps.compose.* import io.ktor.client.HttpClient @@ -40,7 +41,7 @@ fun App() { Column { //display click coordinates Text(coordinates?.toString() ?: "") - MapView(viewPoint, mapTileProvider, onClick = { gmc: GeodeticMapCoordinates -> coordinates = gmc }) { + MapViewWithFeatures(mapTileProvider, onClick = { gmc: GeodeticMapCoordinates -> coordinates = gmc }) { val pointOne = 55.568548 to 37.568604 val pointTwo = 55.929444 to 37.518434 @@ -53,10 +54,14 @@ fun App() { text(pointOne, "Home") scope.launch { - while (isActive){ + while (isActive) { delay(200) //Overwrite a feature with new color - circle(pointTwo, id = circleId, color = Color(Random.nextFloat(), Random.nextFloat(), Random.nextFloat())) + circle( + pointTwo, + id = circleId, + color = Color(Random.nextFloat(), Random.nextFloat(), Random.nextFloat()) + ) } } } diff --git a/src/jvmMain/kotlin/centre/sciprog/maps/compose/MapViewJvm.kt b/src/jvmMain/kotlin/centre/sciprog/maps/compose/MapViewJvm.kt index 09cee4c..35bc9d6 100644 --- a/src/jvmMain/kotlin/centre/sciprog/maps/compose/MapViewJvm.kt +++ b/src/jvmMain/kotlin/centre/sciprog/maps/compose/MapViewJvm.kt @@ -39,15 +39,18 @@ private val logger = KotlinLogging.logger("MapView") @OptIn(ExperimentalComposeUiApi::class) @Composable actual fun MapView( - initialViewPoint: MapViewPoint, mapTileProvider: MapTileProvider, + computeViewPoint: (canvasSize: DpSize) -> MapViewPoint, features: Map, onClick: (GeodeticMapCoordinates) -> Unit, config: MapViewConfig, modifier: Modifier, ) { + var canvasSize by remember { mutableStateOf(DpSize(512.dp, 512.dp)) } - var viewPoint by remember { mutableStateOf(initialViewPoint) } + var viewPointOverride by remember { mutableStateOf(null) } + + val viewPoint by derivedStateOf { viewPointOverride ?: computeViewPoint(canvasSize) } val zoom: Int by derivedStateOf { floor(viewPoint.zoom).toInt() } @@ -55,9 +58,6 @@ actual fun MapView( val mapTiles = remember { mutableStateListOf() } - //var mapRectangle by remember { mutableStateOf(initialRectangle) } - var canvasSize by remember { mutableStateOf(DpSize(512.dp, 512.dp)) } - val centerCoordinates by derivedStateOf { WebMercatorProjection.toMercator(viewPoint.focus, zoom) } fun DpOffset.toMercator(): WebMercatorCoordinates = WebMercatorCoordinates( @@ -102,7 +102,10 @@ actual fun MapView( val verticalZoom: Float = log2(canvasSize.height.toPx() / rect.height) - viewPoint = MapViewPoint(centerGmc, viewPoint.zoom + kotlin.math.min(verticalZoom, horizontalZoom)) + viewPointOverride = MapViewPoint( + centerGmc, + viewPoint.zoom + kotlin.math.min(verticalZoom, horizontalZoom) + ) selectRect = null } } else { @@ -111,7 +114,7 @@ actual fun MapView( onClick(dpPos.toGeodetic()) drag(change.id) { dragChange -> val dragAmount = dragChange.position - dragChange.previousPosition - viewPoint = viewPoint.move( + viewPointOverride = viewPoint.move( -dragAmount.x.toDp().value / tileScale, +dragAmount.y.toDp().value / tileScale ) @@ -126,7 +129,7 @@ actual fun MapView( val (xPos, yPos) = change.position //compute invariant point of translation val invariant = DpOffset(xPos.toDp(), yPos.toDp()).toGeodetic() - viewPoint = viewPoint.zoom(-change.scrollDelta.y.toDouble() * config.zoomSpeed, invariant) + viewPointOverride = viewPoint.zoom(-change.scrollDelta.y.toDouble() * config.zoomSpeed, invariant) }.fillMaxSize() From d3809aca8de01e15872904e7e7e234aa6f69c844 Mon Sep 17 00:00:00 2001 From: Alexander Nozik Date: Thu, 14 Jul 2022 20:32:31 +0300 Subject: [PATCH 3/3] Working box by features --- .../kotlin/centre/sciprog/maps/GmcBox.kt | 11 +++++---- .../centre/sciprog/maps/compose/MapFeature.kt | 2 +- .../centre/sciprog/maps/compose/MapView.kt | 24 +------------------ src/jvmMain/kotlin/Main.kt | 8 +++++-- .../centre/sciprog/maps/compose/MapViewJvm.kt | 21 ++++++++++++---- 5 files changed, 30 insertions(+), 36 deletions(-) diff --git a/src/commonMain/kotlin/centre/sciprog/maps/GmcBox.kt b/src/commonMain/kotlin/centre/sciprog/maps/GmcBox.kt index cb2dc24..c44c9e9 100644 --- a/src/commonMain/kotlin/centre/sciprog/maps/GmcBox.kt +++ b/src/commonMain/kotlin/centre/sciprog/maps/GmcBox.kt @@ -1,8 +1,8 @@ package centre.sciprog.maps -import kotlin.math.abs -import kotlin.math.max -import kotlin.math.min +import androidx.compose.ui.unit.DpSize +import centre.sciprog.maps.compose.MapFeature +import kotlin.math.* class GmcBox(val a: GeodeticMapCoordinates, val b: GeodeticMapCoordinates) @@ -28,9 +28,10 @@ val GmcBox.width get() = abs(a.longitude - b.longitude) val GmcBox.height get() = abs(a.latitude - b.latitude) /** - * Compute a minimal bounding box including all given boxes + * Compute a minimal bounding box including all given boxes. Return null if collection is empty */ -fun Iterable.wrapAll(): GmcBox { +fun Collection.wrapAll(): GmcBox? { + if (isEmpty()) return null //TODO optimize computation val minLat = minOf { it.bottom } val maxLat = maxOf { it.top } diff --git a/src/commonMain/kotlin/centre/sciprog/maps/compose/MapFeature.kt b/src/commonMain/kotlin/centre/sciprog/maps/compose/MapFeature.kt index 1dcc333..8820edb 100644 --- a/src/commonMain/kotlin/centre/sciprog/maps/compose/MapFeature.kt +++ b/src/commonMain/kotlin/centre/sciprog/maps/compose/MapFeature.kt @@ -17,7 +17,7 @@ sealed class MapFeature(val zoomRange: IntRange) { abstract fun getBoundingBox(zoom: Int): GmcBox } -fun Iterable.computeBoundingBox(zoom: Int): GmcBox = map { it.getBoundingBox(zoom) }.wrapAll() +fun Iterable.computeBoundingBox(zoom: Int): GmcBox? = map { it.getBoundingBox(zoom) }.wrapAll() internal fun Pair.toCoordinates() = GeodeticMapCoordinates.ofDegrees(first, second) diff --git a/src/commonMain/kotlin/centre/sciprog/maps/compose/MapView.kt b/src/commonMain/kotlin/centre/sciprog/maps/compose/MapView.kt index 6e3ba7b..decc7b0 100644 --- a/src/commonMain/kotlin/centre/sciprog/maps/compose/MapView.kt +++ b/src/commonMain/kotlin/centre/sciprog/maps/compose/MapView.kt @@ -12,6 +12,7 @@ import kotlin.math.min data class MapViewConfig( val zoomSpeed: Double = 1.0 / 3.0, + val inferViewBoxFromFeatures: Boolean = false ) @Composable @@ -62,27 +63,4 @@ fun MapView( MapViewPoint(box.center, zoom) } MapView(mapTileProvider, computeViewPoint, featuresBuilder.build(), onClick, config, modifier) -} - -/** - * Create a MapView with initial [MapViewPoint] inferred from features - * - * @param defaultZoom the zoom, for which the bounding box is computed - */ -@Composable -fun MapViewWithFeatures( - mapTileProvider: MapTileProvider, - features: Map = emptyMap(), - onClick: (GeodeticMapCoordinates) -> Unit = {}, - config: MapViewConfig = MapViewConfig(), - defaultZoom: Int = 1, - modifier: Modifier = Modifier.fillMaxSize(), - buildFeatures: @Composable (FeatureBuilder.() -> Unit), -) { - val featuresBuilder = MapFeatureBuilder(features) - featuresBuilder.buildFeatures() - val featureSet = featuresBuilder.build() - if(featureSet.isEmpty()) error("Can't create `MapViewWithFeatures` from empty feature set") - val box: GmcBox = featureSet.values.map { it.getBoundingBox(defaultZoom) }.wrapAll() - MapView(mapTileProvider, box, features, onClick, config, modifier, buildFeatures) } \ No newline at end of file diff --git a/src/jvmMain/kotlin/Main.kt b/src/jvmMain/kotlin/Main.kt index 393bf95..5c19ea9 100644 --- a/src/jvmMain/kotlin/Main.kt +++ b/src/jvmMain/kotlin/Main.kt @@ -10,7 +10,6 @@ import androidx.compose.ui.graphics.Color import androidx.compose.ui.window.Window import androidx.compose.ui.window.application import centre.sciprog.maps.GeodeticMapCoordinates -import centre.sciprog.maps.GmcBox import centre.sciprog.maps.MapViewPoint import centre.sciprog.maps.compose.* import io.ktor.client.HttpClient @@ -41,7 +40,12 @@ fun App() { Column { //display click coordinates Text(coordinates?.toString() ?: "") - MapViewWithFeatures(mapTileProvider, onClick = { gmc: GeodeticMapCoordinates -> coordinates = gmc }) { + MapView( + mapTileProvider, + viewPoint, + onClick = { gmc -> coordinates = gmc }, + config = MapViewConfig(inferViewBoxFromFeatures = true) + ) { val pointOne = 55.568548 to 37.568604 val pointTwo = 55.929444 to 37.518434 diff --git a/src/jvmMain/kotlin/centre/sciprog/maps/compose/MapViewJvm.kt b/src/jvmMain/kotlin/centre/sciprog/maps/compose/MapViewJvm.kt index 35bc9d6..df5fad8 100644 --- a/src/jvmMain/kotlin/centre/sciprog/maps/compose/MapViewJvm.kt +++ b/src/jvmMain/kotlin/centre/sciprog/maps/compose/MapViewJvm.kt @@ -20,10 +20,7 @@ import centre.sciprog.maps.* import mu.KotlinLogging import org.jetbrains.skia.Font import org.jetbrains.skia.Paint -import kotlin.math.ceil -import kotlin.math.floor -import kotlin.math.log2 -import kotlin.math.pow +import kotlin.math.* private fun Color.toPaint(): Paint = Paint().apply { @@ -48,7 +45,21 @@ actual fun MapView( ) { var canvasSize by remember { mutableStateOf(DpSize(512.dp, 512.dp)) } - var viewPointOverride by remember { mutableStateOf(null) } + var viewPointOverride by remember { mutableStateOf( + 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) + } + } else { + null + } + ) } val viewPoint by derivedStateOf { viewPointOverride ?: computeViewPoint(canvasSize) }