From 9392c0f991b9440824b74e482a67aa2df8174a0c Mon Sep 17 00:00:00 2001 From: Alexander Nozik Date: Thu, 14 Jul 2022 20:19:57 +0300 Subject: [PATCH] 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()