Change state propagation and rectangle selection
This commit is contained in:
parent
4cabfcf16f
commit
6b0a2566bd
@ -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 })
|
||||||
|
@ -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
|
|
||||||
// )
|
|
||||||
//}
|
|
@ -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(),
|
||||||
|
@ -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 {
|
||||||
|
Loading…
Reference in New Issue
Block a user