Refactor map arguments

This commit is contained in:
Alexander Nozik 2022-07-19 12:31:09 +03:00
parent 2fdec494fb
commit c7d1797617
No known key found for this signature in database
GPG Key ID: F7FCF2DD25C71357
4 changed files with 119 additions and 71 deletions

View File

@ -1,8 +1,6 @@
// Copyright 2000-2021 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file. // Copyright 2000-2021 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file.
import androidx.compose.desktop.ui.tooling.preview.Preview import androidx.compose.desktop.ui.tooling.preview.Preview
import androidx.compose.foundation.layout.Column
import androidx.compose.material.MaterialTheme import androidx.compose.material.MaterialTheme
import androidx.compose.material.Text
import androidx.compose.material.icons.Icons import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Home import androidx.compose.material.icons.filled.Home
import androidx.compose.runtime.* import androidx.compose.runtime.*
@ -14,14 +12,19 @@ import androidx.compose.ui.window.application
import centre.sciprog.maps.GeodeticMapCoordinates import centre.sciprog.maps.GeodeticMapCoordinates
import centre.sciprog.maps.MapViewPoint import centre.sciprog.maps.MapViewPoint
import centre.sciprog.maps.compose.* import centre.sciprog.maps.compose.*
import io.ktor.client.* import io.ktor.client.HttpClient
import io.ktor.client.engine.cio.* import io.ktor.client.engine.cio.CIO
import kotlinx.coroutines.delay import kotlinx.coroutines.delay
import kotlinx.coroutines.isActive import kotlinx.coroutines.isActive
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import java.nio.file.Path import java.nio.file.Path
import kotlin.math.PI
import kotlin.random.Random import kotlin.random.Random
private fun GeodeticMapCoordinates.toShortString(): String =
"${(latitude * 180.0 / PI).toString().take(6)}:${(longitude * 180.0 / PI).toString().take(6)}"
@Composable @Composable
@Preview @Preview
fun App() { fun App() {
@ -42,48 +45,55 @@ fun App() {
) )
} }
var coordinates by remember { mutableStateOf<GeodeticMapCoordinates?>(null) } var centerCoordinates by remember { mutableStateOf<GeodeticMapCoordinates?>(null) }
Column {
//display click coordinates
Text(coordinates?.toString() ?: "")
MapView(
mapTileProvider = mapTileProvider,
initialViewPoint = viewPoint,
onClick = { coordinates = focus },
config = MapViewConfig(inferViewBoxFromFeatures = true)
) {
val pointOne = 55.568548 to 37.568604
val pointTwo = 55.929444 to 37.518434
val pointThree = 60.929444 to 37.518434
image(pointOne, Icons.Filled.Home) MapView(
mapTileProvider = mapTileProvider,
initialViewPoint = viewPoint,
config = MapViewConfig(
inferViewBoxFromFeatures = true,
onViewChange = { centerCoordinates = focus }
)
) {
val pointOne = 55.568548 to 37.568604
val pointTwo = 55.929444 to 37.518434
val pointThree = 60.929444 to 37.518434
//remember feature Id image(pointOne, Icons.Filled.Home)
val circleId: FeatureId = circle(
centerCoordinates = pointTwo, //remember feature Id
val circleId: FeatureId = circle(
centerCoordinates = pointTwo,
)
custom(position = pointThree) {
drawRect(
color = Color.Red,
topLeft = Offset(-10f, -10f),
size = Size(20f, 20f)
) )
}
custom(position = pointThree) { line(pointOne, pointTwo)
drawRect( text(pointOne, "Home")
color = Color.Red,
topLeft = Offset(-10f, -10f), centerCoordinates?.let {
size = Size(20f, 20f) group(id = "center") {
) circle(center = it, color = Color.Blue, size = 1f)
text(position = it, it.toShortString(), color = Color.Blue)
} }
line(pointOne, pointTwo) }
text(pointOne, "Home")
scope.launch { scope.launch {
while (isActive) { while (isActive) {
delay(200) delay(200)
//Overwrite a feature with new color //Overwrite a feature with new color
circle( circle(
pointTwo, pointTwo,
id = circleId, id = circleId,
color = Color(Random.nextFloat(), Random.nextFloat(), Random.nextFloat()) color = Color(Random.nextFloat(), Random.nextFloat(), Random.nextFloat())
) )
}
} }
} }
} }

View File

@ -8,6 +8,7 @@ import androidx.compose.ui.graphics.drawscope.DrawScope
import androidx.compose.ui.graphics.vector.ImageVector import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.unit.DpSize import androidx.compose.ui.unit.DpSize
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import centre.sciprog.maps.GeodeticMapCoordinates
typealias FeatureId = String typealias FeatureId = String
@ -34,6 +35,16 @@ internal class MapFeatureBuilder(initialFeatures: Map<FeatureId, MapFeature>) :
override fun build(): SnapshotStateMap<FeatureId, MapFeature> = content override fun build(): SnapshotStateMap<FeatureId, MapFeature> = content
} }
fun FeatureBuilder.circle(
center: GeodeticMapCoordinates,
zoomRange: IntRange = defaultZoomRange,
size: Float = 5f,
color: Color = Color.Red,
id: FeatureId? = null,
) = addFeature(
id, MapCircleFeature(center, zoomRange, size, color)
)
fun FeatureBuilder.circle( fun FeatureBuilder.circle(
centerCoordinates: Pair<Double, Double>, centerCoordinates: Pair<Double, Double>,
zoomRange: IntRange = defaultZoomRange, zoomRange: IntRange = defaultZoomRange,
@ -59,6 +70,14 @@ fun FeatureBuilder.line(
id: FeatureId? = null, id: FeatureId? = null,
) = addFeature(id, MapLineFeature(aCoordinates.toCoordinates(), bCoordinates.toCoordinates(), zoomRange, color)) ) = addFeature(id, MapLineFeature(aCoordinates.toCoordinates(), bCoordinates.toCoordinates(), zoomRange, color))
fun FeatureBuilder.text(
position: GeodeticMapCoordinates,
text: String,
zoomRange: IntRange = defaultZoomRange,
color: Color = Color.Red,
id: FeatureId? = null,
) = addFeature(id, MapTextFeature(position, text, zoomRange, color))
fun FeatureBuilder.text( fun FeatureBuilder.text(
position: Pair<Double, Double>, position: Pair<Double, Double>,
text: String, text: String,

View File

@ -10,9 +10,14 @@ import kotlin.math.log2
import kotlin.math.min import kotlin.math.min
//TODO consider replacing by modifier
data class MapViewConfig( data class MapViewConfig(
val zoomSpeed: Double = 1.0 / 3.0, val zoomSpeed: Double = 1.0 / 3.0,
val inferViewBoxFromFeatures: Boolean = false val inferViewBoxFromFeatures: Boolean = false,
val onClick: MapViewPoint.() -> Unit = {},
val onViewChange: MapViewPoint.() -> Unit = {},
val onSelect: (GmcBox) -> Unit = {},
val zoomOnSelect: Boolean = true
) )
@Composable @Composable
@ -20,8 +25,6 @@ expect fun MapView(
mapTileProvider: MapTileProvider, mapTileProvider: MapTileProvider,
computeViewPoint: (canvasSize: DpSize) -> MapViewPoint, computeViewPoint: (canvasSize: DpSize) -> MapViewPoint,
features: Map<FeatureId, MapFeature>, features: Map<FeatureId, MapFeature>,
onClick: MapViewPoint.() -> Unit = {},
//TODO consider replacing by modifier
config: MapViewConfig = MapViewConfig(), config: MapViewConfig = MapViewConfig(),
modifier: Modifier = Modifier.fillMaxSize(), modifier: Modifier = Modifier.fillMaxSize(),
) )
@ -31,14 +34,29 @@ fun MapView(
mapTileProvider: MapTileProvider, mapTileProvider: MapTileProvider,
initialViewPoint: MapViewPoint, initialViewPoint: MapViewPoint,
features: Map<FeatureId, MapFeature> = emptyMap(), features: Map<FeatureId, MapFeature> = emptyMap(),
onClick: MapViewPoint.() -> Unit = {},
config: MapViewConfig = MapViewConfig(), config: MapViewConfig = MapViewConfig(),
modifier: Modifier = Modifier.fillMaxSize(), modifier: Modifier = Modifier.fillMaxSize(),
buildFeatures: @Composable (FeatureBuilder.() -> Unit) = {}, buildFeatures: @Composable (FeatureBuilder.() -> Unit) = {},
) { ) {
val featuresBuilder = MapFeatureBuilder(features) val featuresBuilder = MapFeatureBuilder(features)
featuresBuilder.buildFeatures() featuresBuilder.buildFeatures()
MapView(mapTileProvider, { initialViewPoint }, featuresBuilder.build(), onClick, config, modifier) MapView(
mapTileProvider,
{ initialViewPoint },
featuresBuilder.build(),
config,
modifier
)
}
internal fun GmcBox.getComputeViewPoint(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 @Composable
@ -46,21 +64,17 @@ fun MapView(
mapTileProvider: MapTileProvider, mapTileProvider: MapTileProvider,
box: GmcBox, box: GmcBox,
features: Map<FeatureId, MapFeature> = emptyMap(), features: Map<FeatureId, MapFeature> = emptyMap(),
onClick: MapViewPoint.() -> Unit = {},
config: MapViewConfig = MapViewConfig(), config: MapViewConfig = MapViewConfig(),
modifier: Modifier = Modifier.fillMaxSize(), modifier: Modifier = Modifier.fillMaxSize(),
buildFeatures: @Composable (FeatureBuilder.() -> Unit) = {}, buildFeatures: @Composable (FeatureBuilder.() -> Unit) = {},
) { ) {
val featuresBuilder = MapFeatureBuilder(features) val featuresBuilder = MapFeatureBuilder(features)
featuresBuilder.buildFeatures() featuresBuilder.buildFeatures()
val computeViewPoint: (canvasSize: DpSize) -> MapViewPoint = { canvasSize -> MapView(
val zoom = log2( mapTileProvider,
min( box.getComputeViewPoint(mapTileProvider),
canvasSize.width.value / box.width, featuresBuilder.build(),
canvasSize.height.value / box.height config,
) * PI / mapTileProvider.tileSize modifier
) )
MapViewPoint(box.center, zoom)
}
MapView(mapTileProvider, computeViewPoint, featuresBuilder.build(), onClick, config, modifier)
} }

View File

@ -9,7 +9,6 @@ import androidx.compose.ui.ExperimentalComposeUiApi
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.geometry.Offset import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.geometry.Rect import androidx.compose.ui.geometry.Rect
import androidx.compose.ui.geometry.Size
import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.PathEffect import androidx.compose.ui.graphics.PathEffect
import androidx.compose.ui.graphics.drawscope.* import androidx.compose.ui.graphics.drawscope.*
@ -55,7 +54,6 @@ actual fun MapView(
mapTileProvider: MapTileProvider, mapTileProvider: MapTileProvider,
computeViewPoint: (canvasSize: DpSize) -> MapViewPoint, computeViewPoint: (canvasSize: DpSize) -> MapViewPoint,
features: Map<FeatureId, MapFeature>, features: Map<FeatureId, MapFeature>,
onClick: MapViewPoint.() -> Unit,
config: MapViewConfig, config: MapViewConfig,
modifier: Modifier, modifier: Modifier,
) { ) {
@ -107,6 +105,8 @@ actual fun MapView(
val canvasModifier = modifier.pointerInput(Unit) { val canvasModifier = modifier.pointerInput(Unit) {
forEachGesture { forEachGesture {
awaitPointerEventScope { awaitPointerEventScope {
fun Offset.toDpOffset() = DpOffset(x.toDp(), y.toDp())
val event: PointerEvent = awaitPointerEvent() val event: PointerEvent = awaitPointerEvent()
event.changes.forEach { change -> event.changes.forEach { change ->
if (event.buttons.isPrimaryPressed) { if (event.buttons.isPrimaryPressed) {
@ -125,29 +125,32 @@ actual fun MapView(
} }
} }
selectRect?.let { rect -> selectRect?.let { rect ->
val (centerX, centerY) = rect.center //Use selection override if it is defined
val centerGmc = DpOffset(centerX.toDp(), centerY.toDp()).toGeodetic() val gmcBox = GmcBox(
rect.topLeft.toDpOffset().toGeodetic(),
val horizontalZoom: Float = log2(canvasSize.width.toPx() / rect.width) rect.bottomRight.toDpOffset().toGeodetic()
val verticalZoom: Float = log2(canvasSize.height.toPx() / rect.height)
viewPointOverride = MapViewPoint(
centerGmc,
viewPoint.zoom + min(verticalZoom, horizontalZoom)
) )
config.onSelect(gmcBox)
if(config.zoomOnSelect) {
val newViewPoint = gmcBox.getComputeViewPoint(mapTileProvider).invoke(canvasSize)
config.onViewChange(newViewPoint)
viewPointOverride = newViewPoint
}
selectRect = null selectRect = null
} }
} else { } else {
val dragStart = change.position val dragStart = change.position
val dpPos = DpOffset(dragStart.x.toDp(), dragStart.y.toDp()) val dpPos = DpOffset(dragStart.x.toDp(), dragStart.y.toDp())
onClick(MapViewPoint(dpPos.toGeodetic(), viewPoint.zoom)) config.onClick(MapViewPoint(dpPos.toGeodetic(), viewPoint.zoom))
drag(change.id) { dragChange -> drag(change.id) { dragChange ->
val dragAmount = dragChange.position - dragChange.previousPosition val dragAmount = dragChange.position - dragChange.previousPosition
viewPointOverride = viewPoint.move( val newViewPoint = viewPoint.move(
-dragAmount.x.toDp().value / tileScale, -dragAmount.x.toDp().value / tileScale,
+dragAmount.y.toDp().value / tileScale +dragAmount.y.toDp().value / tileScale
) )
config.onViewChange(newViewPoint)
viewPointOverride = newViewPoint
} }
} }
} }
@ -159,7 +162,9 @@ actual fun MapView(
val (xPos, yPos) = change.position val (xPos, yPos) = change.position
//compute invariant point of translation //compute invariant point of translation
val invariant = DpOffset(xPos.toDp(), yPos.toDp()).toGeodetic() val invariant = DpOffset(xPos.toDp(), yPos.toDp()).toGeodetic()
viewPointOverride = viewPoint.zoom(-change.scrollDelta.y.toDouble() * config.zoomSpeed, invariant) val newViewPoint = viewPoint.zoom(-change.scrollDelta.y.toDouble() * config.zoomSpeed, invariant)
config.onViewChange(newViewPoint)
viewPointOverride = newViewPoint
}.fillMaxSize() }.fillMaxSize()
@ -241,7 +246,7 @@ actual fun MapView(
} }
is MapDrawFeature -> { is MapDrawFeature -> {
val offset = feature.position.toOffset() val offset = feature.position.toOffset()
translate (offset.x, offset.y) { translate(offset.x, offset.y) {
feature.drawFeature(this) feature.drawFeature(this)
} }
} }