Change state propagation and rectangle selection

This commit is contained in:
Alexander Nozik 2022-09-13 18:27:57 +03:00
parent 4cabfcf16f
commit 6b0a2566bd
No known key found for this signature in database
GPG Key ID: F7FCF2DD25C71357
4 changed files with 84 additions and 97 deletions

View File

@ -11,9 +11,7 @@ import androidx.compose.ui.unit.dp
import androidx.compose.ui.window.Window import androidx.compose.ui.window.Window
import androidx.compose.ui.window.application import androidx.compose.ui.window.application
import center.sciprog.maps.compose.* import center.sciprog.maps.compose.*
import center.sciprog.maps.coordinates.Distance import center.sciprog.maps.coordinates.*
import center.sciprog.maps.coordinates.GeodeticMapCoordinates
import center.sciprog.maps.coordinates.MapViewPoint
import io.ktor.client.HttpClient import io.ktor.client.HttpClient
import io.ktor.client.engine.cio.CIO import io.ktor.client.engine.cio.CIO
import kotlinx.coroutines.delay import kotlinx.coroutines.delay
@ -22,9 +20,6 @@ import kotlinx.coroutines.launch
import java.nio.file.Path import java.nio.file.Path
import kotlin.math.PI import kotlin.math.PI
import kotlin.random.Random import kotlin.random.Random
import center.sciprog.maps.coordinates.kilometers
import center.sciprog.maps.coordinates.radians
import center.sciprog.maps.coordinates.Angle
private fun GeodeticMapCoordinates.toShortString(): String = private fun GeodeticMapCoordinates.toShortString(): String =
"${(latitude.degrees.value).toString().take(6)}:${(longitude.degrees.value).toString().take(6)}" "${(latitude.degrees.value).toString().take(6)}:${(longitude.degrees.value).toString().take(6)}"
@ -34,13 +29,6 @@ private fun GeodeticMapCoordinates.toShortString(): String =
@Preview @Preview
fun App() { fun App() {
MaterialTheme { MaterialTheme {
//create a view point
// val viewPoint = remember {
// MapViewPoint(
// GeodeticMapCoordinates.ofDegrees(55.7558, 37.6173),
// 8.0
// )
// }
val scope = rememberCoroutineScope() val scope = rememberCoroutineScope()
val mapTileProvider = remember { val mapTileProvider = remember {
@ -50,7 +38,7 @@ fun App() {
) )
} }
var centerCoordinates by remember { mutableStateOf<GeodeticMapCoordinates?>(null) } var centerCoordinates by remember { mutableStateOf<Gmc?>(null) }
val pointOne = 55.568548 to 37.568604 val pointOne = 55.568548 to 37.568604
@ -61,7 +49,15 @@ fun App() {
MapView( MapView(
mapTileProvider = mapTileProvider, mapTileProvider = mapTileProvider,
initialViewPoint = null,// use null to infer view point from features // initialViewPoint = MapViewPoint(
// GeodeticMapCoordinates.ofDegrees(55.7558, 37.6173),
// 8.0
// ),
// initialRectangle = GmcRectangle.square(
// GeodeticMapCoordinates.ofDegrees(55.7558, 37.6173),
// 50.kilometers,
// 50.kilometers
// ),
config = MapViewConfig( config = MapViewConfig(
onViewChange = { centerCoordinates = focus }, onViewChange = { centerCoordinates = focus },
) )
@ -93,7 +89,7 @@ fun App() {
drawLine(start = Offset(-10f, 10f), end = Offset(10f, -10f), color = Color.Red) drawLine(start = Offset(-10f, 10f), end = Offset(10f, -10f), color = Color.Red)
} }
arc(pointOne, 10.0.kilometers, (PI/4).radians, -Angle.pi/2) arc(pointOne, 10.0.kilometers, (PI / 4).radians, -Angle.pi / 2)
line(pointOne, pointTwo, id = "line") line(pointOne, pointTwo, id = "line")
text(pointOne, "Home", font = { size = 32f }) text(pointOne, "Home", font = { size = 32f })

View File

@ -1,9 +1,8 @@
package center.sciprog.maps.compose package center.sciprog.maps.compose
import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.runtime.Composable import androidx.compose.runtime.*
import androidx.compose.runtime.mutableStateMapOf import androidx.compose.runtime.snapshots.SnapshotStateMap
import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.input.pointer.PointerEvent import androidx.compose.ui.input.pointer.PointerEvent
import androidx.compose.ui.input.pointer.isPrimaryPressed import androidx.compose.ui.input.pointer.isPrimaryPressed
@ -62,42 +61,18 @@ public data class MapViewConfig(
val onViewChange: MapViewPoint.() -> Unit = {}, val onViewChange: MapViewPoint.() -> Unit = {},
val onSelect: (GmcRectangle) -> Unit = {}, val onSelect: (GmcRectangle) -> Unit = {},
val zoomOnSelect: Boolean = true, val zoomOnSelect: Boolean = true,
val onCanvasSizeChange: (DpSize) -> Unit = {},
) )
@Composable @Composable
public expect fun MapView( public expect fun MapView(
mapTileProvider: MapTileProvider, mapTileProvider: MapTileProvider,
computeViewPoint: (canvasSize: DpSize) -> MapViewPoint, initialViewPoint: MapViewPoint,
features: Map<FeatureId, MapFeature>, features: Map<FeatureId, MapFeature>,
config: MapViewConfig = MapViewConfig(), config: MapViewConfig = MapViewConfig(),
modifier: Modifier = Modifier.fillMaxSize(), modifier: Modifier = Modifier.fillMaxSize(),
) )
private fun prepareConfig(initialConfig: MapViewConfig, featureBuilder: MapFeatureBuilder): MapViewConfig {
val draggableFeatureIds: Set<FeatureId> = featureBuilder.attributes().filterValues {
it[DraggableAttribute] ?: false
}.keys
val features = featureBuilder.features
val featureDrag = DragHandle.withPrimaryButton { _, start, end ->
val zoom = start.zoom
draggableFeatureIds.forEach { id ->
val feature = features[id] as? DraggableMapFeature ?: return@forEach
//val border = WebMercatorProjection.scaleFactor(zoom)
val boundingBox = feature.getBoundingBox(zoom) ?: return@forEach
if (start.focus in boundingBox) {
features[id] = feature.withCoordinates(end.focus)
return@withPrimaryButton false
}
}
return@withPrimaryButton true
}
return initialConfig.copy(
dragHandle = DragHandle.combine(featureDrag, initialConfig.dragHandle),
)
}
internal fun GmcRectangle.computeViewPoint( internal fun GmcRectangle.computeViewPoint(
mapTileProvider: MapTileProvider, mapTileProvider: MapTileProvider,
@ -128,44 +103,50 @@ public fun MapView(
modifier: Modifier = Modifier.fillMaxSize(), modifier: Modifier = Modifier.fillMaxSize(),
buildFeatures: @Composable (MapFeatureBuilder.() -> Unit) = {}, buildFeatures: @Composable (MapFeatureBuilder.() -> Unit) = {},
) { ) {
val featuresBuilder = MapFeatureBuilderImpl(mutableStateMapOf())
featuresBuilder.buildFeatures()
val features = remember { featuresBuilder.features }
val newConfig = remember(features) { var viewPointOverride by remember { mutableStateOf(initialViewPoint ?: MapViewPoint.globe) }
prepareConfig(config, featuresBuilder)
val featuresBuilder = MapFeatureBuilderImpl(mutableStateMapOf()).apply { buildFeatures() }
val features: SnapshotStateMap<FeatureId, MapFeature> = remember { featuresBuilder.features }
val attributes = remember { featuresBuilder.attributes() }
val featureDrag = remember {
DragHandle.withPrimaryButton { _, start, end ->
val zoom = start.zoom
attributes.filterValues {
it[DraggableAttribute] ?: false
}.keys.forEach { id ->
val feature = features[id] as? DraggableMapFeature ?: return@forEach
//val border = WebMercatorProjection.scaleFactor(zoom)
val boundingBox = feature.getBoundingBox(zoom) ?: return@forEach
if (start.focus in boundingBox) {
features[id] = feature.withCoordinates(end.focus)
return@withPrimaryButton false
}
}
return@withPrimaryButton true
}
} }
MapView( val newConfig = config.copy(
mapTileProvider, dragHandle = DragHandle.combine(featureDrag, config.dragHandle),
{ canvasSize -> onCanvasSizeChange = { canvasSize ->
initialViewPoint viewPointOverride = initialViewPoint
?: initialRectangle?.computeViewPoint(mapTileProvider, canvasSize) ?: initialRectangle?.computeViewPoint(mapTileProvider, canvasSize)
?: features.values.computeBoundingBox(1.0)?.computeViewPoint(mapTileProvider, canvasSize) ?: features.values.computeBoundingBox(1.0)?.computeViewPoint(mapTileProvider, canvasSize)
?: MapViewPoint(GeodeticMapCoordinates(0.0.radians, 0.0.radians), 1.0) ?: MapViewPoint.globe
},
features,
newConfig,
modifier
)
}
// config.onCanvasSizeChange(canvasSize)
//@Composable }
//public fun MapView( )
// mapTileProvider: MapTileProvider,
// box: GmcRectangle, MapView(
// config: MapViewConfig = MapViewConfig(), mapTileProvider = mapTileProvider,
// modifier: Modifier = Modifier.fillMaxSize(), initialViewPoint = viewPointOverride,
// buildFeatures: @Composable (MapFeatureBuilder.() -> Unit) = {}, features = features,
//) { config = newConfig,
// val builder by derivedStateOf { MapFeatureBuilderImpl().apply(buildFeatures) } modifier = modifier,
// )
// MapView( }
// mapTileProvider,
// box.computeViewPoint(mapTileProvider),
// builder.features,
// prepareConfig(config, builder),
// modifier
// )
//}

View File

@ -49,15 +49,24 @@ private val logger = KotlinLogging.logger("MapView")
@Composable @Composable
public actual fun MapView( public actual fun MapView(
mapTileProvider: MapTileProvider, mapTileProvider: MapTileProvider,
computeViewPoint: (canvasSize: DpSize) -> MapViewPoint, initialViewPoint: MapViewPoint,
features: Map<FeatureId, MapFeature>, features: Map<FeatureId, MapFeature>,
config: MapViewConfig, config: MapViewConfig,
modifier: Modifier, modifier: Modifier,
) { ) {
var canvasSize by remember { mutableStateOf(DpSize(512.dp, 512.dp)) } var canvasSize by remember { mutableStateOf(DpSize(512.dp, 512.dp)) }
var viewPoint: MapViewPoint by remember { var viewPointOverride by remember { mutableStateOf(initialViewPoint) }
mutableStateOf(computeViewPoint(canvasSize)) var viewPoint by remember { mutableStateOf(initialViewPoint) }
if (viewPointOverride != initialViewPoint) {
viewPoint = initialViewPoint
viewPointOverride = initialViewPoint
}
fun setViewPoint(newViewPoint: MapViewPoint) {
config.onViewChange(newViewPoint)
viewPoint = newViewPoint
} }
val zoom: Int by derivedStateOf { val zoom: Int by derivedStateOf {
@ -140,12 +149,12 @@ public actual fun MapView(
config.onClick(MapViewPoint(dpPos.toGeodetic(), viewPoint.zoom), event) config.onClick(MapViewPoint(dpPos.toGeodetic(), viewPoint.zoom), event)
val newViewPoint = viewPoint.move( setViewPoint(
-dragAmount.x.toDp().value / tileScale, viewPoint.move(
+dragAmount.y.toDp().value / tileScale -dragAmount.x.toDp().value / tileScale,
+dragAmount.y.toDp().value / tileScale
)
) )
config.onViewChange(newViewPoint)
viewPoint = newViewPoint
} }
} }
@ -158,10 +167,7 @@ public actual fun MapView(
) )
config.onSelect(gmcBox) config.onSelect(gmcBox)
if (config.zoomOnSelect) { if (config.zoomOnSelect) {
val newViewPoint = gmcBox.computeViewPoint(mapTileProvider, canvasSize) setViewPoint(gmcBox.computeViewPoint(mapTileProvider, canvasSize))
config.onViewChange(newViewPoint)
viewPoint = newViewPoint
} }
selectRect = null selectRect = null
} }
@ -173,9 +179,7 @@ public 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()
val newViewPoint = viewPoint.zoom(-change.scrollDelta.y.toDouble() * config.zoomSpeed, invariant) setViewPoint(viewPoint.zoom(-change.scrollDelta.y.toDouble() * config.zoomSpeed, invariant))
config.onViewChange(newViewPoint)
viewPoint = newViewPoint
}.fillMaxSize() }.fillMaxSize()
@ -314,9 +318,11 @@ public actual fun MapView(
} }
if (canvasSize != size.toDpSize()) { if (canvasSize != size.toDpSize()) {
canvasSize = size.toDpSize()
logger.debug { "Recalculate canvas. Size: $size" } logger.debug { "Recalculate canvas. Size: $size" }
config.onCanvasSizeChange(canvasSize)
canvasSize = size.toDpSize()
} }
clipRect { clipRect {
val tileSize = IntSize( val tileSize = IntSize(
ceil((mapTileProvider.tileSize.dp * tileScale.toFloat()).toPx()).toInt(), ceil((mapTileProvider.tileSize.dp * tileScale.toFloat()).toPx()).toInt(),

View File

@ -10,6 +10,10 @@ public data class MapViewPoint(
val zoom: Double, val zoom: Double,
) { ) {
val scaleFactor: Double by lazy { WebMercatorProjection.scaleFactor(zoom) } val scaleFactor: Double by lazy { WebMercatorProjection.scaleFactor(zoom) }
public companion object{
public val globe: MapViewPoint = MapViewPoint(GeodeticMapCoordinates(0.0.radians, 0.0.radians), 1.0)
}
} }
public fun MapViewPoint.move(delta: GeodeticMapCoordinates): MapViewPoint { public fun MapViewPoint.move(delta: GeodeticMapCoordinates): MapViewPoint {