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.application
import center.sciprog.maps.compose.*
import center.sciprog.maps.coordinates.Distance
import center.sciprog.maps.coordinates.GeodeticMapCoordinates
import center.sciprog.maps.coordinates.MapViewPoint
import center.sciprog.maps.coordinates.*
import io.ktor.client.HttpClient
import io.ktor.client.engine.cio.CIO
import kotlinx.coroutines.delay
@ -22,9 +20,6 @@ import kotlinx.coroutines.launch
import java.nio.file.Path
import kotlin.math.PI
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 =
"${(latitude.degrees.value).toString().take(6)}:${(longitude.degrees.value).toString().take(6)}"
@ -34,13 +29,6 @@ private fun GeodeticMapCoordinates.toShortString(): String =
@Preview
fun App() {
MaterialTheme {
//create a view point
// val viewPoint = remember {
// MapViewPoint(
// GeodeticMapCoordinates.ofDegrees(55.7558, 37.6173),
// 8.0
// )
// }
val scope = rememberCoroutineScope()
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
@ -61,7 +49,15 @@ fun App() {
MapView(
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(
onViewChange = { centerCoordinates = focus },
)
@ -93,7 +89,7 @@ fun App() {
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")
text(pointOne, "Home", font = { size = 32f })

View File

@ -1,9 +1,8 @@
package center.sciprog.maps.compose
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.runtime.Composable
import androidx.compose.runtime.mutableStateMapOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.*
import androidx.compose.runtime.snapshots.SnapshotStateMap
import androidx.compose.ui.Modifier
import androidx.compose.ui.input.pointer.PointerEvent
import androidx.compose.ui.input.pointer.isPrimaryPressed
@ -62,42 +61,18 @@ public data class MapViewConfig(
val onViewChange: MapViewPoint.() -> Unit = {},
val onSelect: (GmcRectangle) -> Unit = {},
val zoomOnSelect: Boolean = true,
val onCanvasSizeChange: (DpSize) -> Unit = {},
)
@Composable
public expect fun MapView(
mapTileProvider: MapTileProvider,
computeViewPoint: (canvasSize: DpSize) -> MapViewPoint,
initialViewPoint: MapViewPoint,
features: Map<FeatureId, MapFeature>,
config: MapViewConfig = MapViewConfig(),
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(
mapTileProvider: MapTileProvider,
@ -128,44 +103,50 @@ public fun MapView(
modifier: Modifier = Modifier.fillMaxSize(),
buildFeatures: @Composable (MapFeatureBuilder.() -> Unit) = {},
) {
val featuresBuilder = MapFeatureBuilderImpl(mutableStateMapOf())
featuresBuilder.buildFeatures()
val features = remember { featuresBuilder.features }
val newConfig = remember(features) {
prepareConfig(config, featuresBuilder)
var viewPointOverride by remember { mutableStateOf(initialViewPoint ?: MapViewPoint.globe) }
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(
mapTileProvider,
{ canvasSize ->
initialViewPoint
val newConfig = config.copy(
dragHandle = DragHandle.combine(featureDrag, config.dragHandle),
onCanvasSizeChange = { canvasSize ->
viewPointOverride = initialViewPoint
?: initialRectangle?.computeViewPoint(mapTileProvider, canvasSize)
?: features.values.computeBoundingBox(1.0)?.computeViewPoint(mapTileProvider, canvasSize)
?: MapViewPoint(GeodeticMapCoordinates(0.0.radians, 0.0.radians), 1.0)
},
features,
newConfig,
modifier
)
}
?: features.values.computeBoundingBox(1.0)?.computeViewPoint(mapTileProvider, canvasSize)
?: MapViewPoint.globe
//
//@Composable
//public fun MapView(
// mapTileProvider: MapTileProvider,
// box: GmcRectangle,
// config: MapViewConfig = MapViewConfig(),
// modifier: Modifier = Modifier.fillMaxSize(),
// buildFeatures: @Composable (MapFeatureBuilder.() -> Unit) = {},
//) {
// val builder by derivedStateOf { MapFeatureBuilderImpl().apply(buildFeatures) }
//
// MapView(
// mapTileProvider,
// box.computeViewPoint(mapTileProvider),
// builder.features,
// prepareConfig(config, builder),
// modifier
// )
//}
config.onCanvasSizeChange(canvasSize)
}
)
MapView(
mapTileProvider = mapTileProvider,
initialViewPoint = viewPointOverride,
features = features,
config = newConfig,
modifier = modifier,
)
}

View File

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

View File

@ -10,6 +10,10 @@ public data class MapViewPoint(
val zoom: Double,
) {
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 {