Full refacor of map state and arguments
This commit is contained in:
parent
56ccd66db5
commit
9e3eec9533
@ -2,8 +2,9 @@ import androidx.compose.desktop.ui.tooling.preview.Preview
|
|||||||
import androidx.compose.material.MaterialTheme
|
import androidx.compose.material.MaterialTheme
|
||||||
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.Composable
|
||||||
import androidx.compose.ui.geometry.Offset
|
import androidx.compose.runtime.remember
|
||||||
|
import androidx.compose.runtime.rememberCoroutineScope
|
||||||
import androidx.compose.ui.graphics.Color
|
import androidx.compose.ui.graphics.Color
|
||||||
import androidx.compose.ui.graphics.PointMode
|
import androidx.compose.ui.graphics.PointMode
|
||||||
import androidx.compose.ui.unit.DpSize
|
import androidx.compose.ui.unit.DpSize
|
||||||
@ -17,6 +18,10 @@ import center.sciprog.maps.geojson.geoJson
|
|||||||
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
|
||||||
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
|
import kotlinx.coroutines.flow.filterNotNull
|
||||||
|
import kotlinx.coroutines.flow.launchIn
|
||||||
|
import kotlinx.coroutines.flow.onEach
|
||||||
import java.net.URL
|
import java.net.URL
|
||||||
import java.nio.file.Path
|
import java.nio.file.Path
|
||||||
import kotlin.math.PI
|
import kotlin.math.PI
|
||||||
@ -32,6 +37,7 @@ fun App() {
|
|||||||
MaterialTheme {
|
MaterialTheme {
|
||||||
|
|
||||||
val scope = rememberCoroutineScope()
|
val scope = rememberCoroutineScope()
|
||||||
|
|
||||||
val mapTileProvider = remember {
|
val mapTileProvider = remember {
|
||||||
OpenStreetMapTileProvider(
|
OpenStreetMapTileProvider(
|
||||||
client = HttpClient(CIO),
|
client = HttpClient(CIO),
|
||||||
@ -39,7 +45,7 @@ fun App() {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
var centerCoordinates by remember { mutableStateOf<Gmc?>(null) }
|
val centerCoordinates = MutableStateFlow<Gmc?>(null)
|
||||||
|
|
||||||
|
|
||||||
val pointOne = 55.568548 to 37.568604
|
val pointOne = 55.568548 to 37.568604
|
||||||
@ -48,17 +54,9 @@ fun App() {
|
|||||||
|
|
||||||
MapView(
|
MapView(
|
||||||
mapTileProvider = mapTileProvider,
|
mapTileProvider = mapTileProvider,
|
||||||
// initialViewPoint = MapViewPoint(
|
|
||||||
// GeodeticMapCoordinates.ofDegrees(55.7558, 37.6173),
|
|
||||||
// 8.0
|
|
||||||
// ),
|
|
||||||
// initialRectangle = GmcRectangle.square(
|
|
||||||
// GeodeticMapCoordinates.ofDegrees(55.7558, 37.6173),
|
|
||||||
// 50.kilometers,
|
|
||||||
// 50.kilometers
|
|
||||||
// ),
|
|
||||||
config = ViewConfig(
|
config = ViewConfig(
|
||||||
onViewChange = { centerCoordinates = focus },
|
onViewChange = { centerCoordinates.value = focus },
|
||||||
|
onClick = { _, viewPoint -> println(viewPoint) }
|
||||||
)
|
)
|
||||||
) {
|
) {
|
||||||
|
|
||||||
@ -95,22 +93,22 @@ fun App() {
|
|||||||
it.copy(color = Color(Random.nextFloat(), Random.nextFloat(), Random.nextFloat()))
|
it.copy(color = Color(Random.nextFloat(), Random.nextFloat(), Random.nextFloat()))
|
||||||
}
|
}
|
||||||
|
|
||||||
draw(position = pointThree) {
|
// draw(position = pointThree) {
|
||||||
drawLine(start = Offset(-10f, -10f), end = Offset(10f, 10f), color = Color.Red)
|
// drawLine(start = Offset(-10f, -10f), end = Offset(10f, 10f), color = Color.Red)
|
||||||
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 })
|
||||||
|
|
||||||
centerCoordinates?.let {
|
centerCoordinates.filterNotNull().onEach {
|
||||||
group(id = "center") {
|
group(id = "center") {
|
||||||
circle(center = it, color = Color.Blue, id = "circle", size = 1.dp)
|
circle(center = it, color = Color.Blue, id = "circle", size = 1.dp)
|
||||||
text(position = it, it.toShortString(), id = "text", color = Color.Blue)
|
text(position = it, it.toShortString(), id = "text", color = Color.Blue)
|
||||||
}
|
}
|
||||||
}
|
}.launchIn(scope)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -79,15 +79,20 @@ fun App() {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
) {
|
) {
|
||||||
SchemeView(
|
val mapState: XYViewScope = rememberMapState(
|
||||||
initialViewPoint = initialViewPoint,
|
ViewConfig(
|
||||||
featuresState = schemeFeaturesState,
|
onClick = {_, click ->
|
||||||
config = ViewConfig(
|
println("${click.focus.x}, ${click.focus.y}")
|
||||||
onClick = {
|
|
||||||
println("${focus.x}, ${focus.y}")
|
|
||||||
},
|
},
|
||||||
onViewChange = { viewPoint = this }
|
onViewChange = { viewPoint = this }
|
||||||
),
|
),
|
||||||
|
schemeFeaturesState.features.values,
|
||||||
|
initialViewPoint = initialViewPoint,
|
||||||
|
)
|
||||||
|
|
||||||
|
SchemeView(
|
||||||
|
mapState,
|
||||||
|
schemeFeaturesState,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -21,9 +21,9 @@ public interface MapTileProvider {
|
|||||||
|
|
||||||
public val tileSize: Int get() = DEFAULT_TILE_SIZE
|
public val tileSize: Int get() = DEFAULT_TILE_SIZE
|
||||||
|
|
||||||
public fun toIndex(d: Double): Int = floor(d / tileSize).toInt()
|
public fun toIndex(d: Float): Int = floor(d / tileSize).toInt()
|
||||||
|
|
||||||
public fun toCoordinate(i: Int): Double = (i * tileSize).toDouble()
|
public fun toCoordinate(i: Int): Float = (i * tileSize).toFloat()
|
||||||
|
|
||||||
public companion object {
|
public companion object {
|
||||||
public const val DEFAULT_TILE_SIZE: Int = 256
|
public const val DEFAULT_TILE_SIZE: Int = 256
|
||||||
|
@ -2,42 +2,19 @@ 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.Composable
|
||||||
import androidx.compose.runtime.key
|
|
||||||
import androidx.compose.runtime.remember
|
import androidx.compose.runtime.remember
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.unit.DpSize
|
|
||||||
import androidx.compose.ui.unit.dp
|
|
||||||
import center.sciprog.maps.coordinates.Gmc
|
import center.sciprog.maps.coordinates.Gmc
|
||||||
import center.sciprog.maps.features.*
|
import center.sciprog.maps.features.*
|
||||||
import kotlin.math.PI
|
|
||||||
import kotlin.math.log2
|
|
||||||
import kotlin.math.min
|
|
||||||
|
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
public expect fun MapView(
|
public expect fun MapView(
|
||||||
mapTileProvider: MapTileProvider,
|
mapState: MapViewScope,
|
||||||
initialViewPoint: MapViewPoint,
|
|
||||||
featuresState: FeatureCollection<Gmc>,
|
featuresState: FeatureCollection<Gmc>,
|
||||||
config: ViewConfig<Gmc> = ViewConfig(),
|
|
||||||
modifier: Modifier = Modifier.fillMaxSize(),
|
modifier: Modifier = Modifier.fillMaxSize(),
|
||||||
)
|
)
|
||||||
|
|
||||||
internal val defaultCanvasSize = DpSize(512.dp, 512.dp)
|
|
||||||
|
|
||||||
public fun Rectangle<Gmc>.computeViewPoint(
|
|
||||||
mapTileProvider: MapTileProvider,
|
|
||||||
canvasSize: DpSize = defaultCanvasSize,
|
|
||||||
): MapViewPoint {
|
|
||||||
val zoom = log2(
|
|
||||||
min(
|
|
||||||
canvasSize.width.value / longitudeDelta.radians.value,
|
|
||||||
canvasSize.height.value / latitudeDelta.radians.value
|
|
||||||
) * PI / mapTileProvider.tileSize
|
|
||||||
)
|
|
||||||
return MapViewPoint(center, zoom.toFloat())
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A builder for a Map with static features.
|
* A builder for a Map with static features.
|
||||||
*/
|
*/
|
||||||
@ -50,20 +27,22 @@ public fun MapView(
|
|||||||
config: ViewConfig<Gmc> = ViewConfig(),
|
config: ViewConfig<Gmc> = ViewConfig(),
|
||||||
modifier: Modifier = Modifier.fillMaxSize(),
|
modifier: Modifier = Modifier.fillMaxSize(),
|
||||||
) {
|
) {
|
||||||
val featuresState = key(featureMap) {
|
|
||||||
FeatureCollection.build(GmcCoordinateSpace) {
|
val featuresState = remember(featureMap) {
|
||||||
|
FeatureCollection.build(WebMercatorSpace) {
|
||||||
featureMap.forEach { feature(it.key.id, it.value) }
|
featureMap.forEach { feature(it.key.id, it.value) }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
val viewPointOverride: MapViewPoint = remember(initialViewPoint, initialRectangle) {
|
val mapState: MapViewScope = rememberMapState(
|
||||||
initialViewPoint
|
mapTileProvider,
|
||||||
?: initialRectangle?.computeViewPoint(mapTileProvider)
|
config,
|
||||||
?: featureMap.values.computeBoundingBox(GmcCoordinateSpace, 1f)?.computeViewPoint(mapTileProvider)
|
featuresState.features.values,
|
||||||
?: MapViewPoint.globe
|
initialViewPoint = initialViewPoint,
|
||||||
}
|
initialRectangle = initialRectangle,
|
||||||
|
)
|
||||||
|
|
||||||
MapView(mapTileProvider, viewPointOverride, featuresState, config, modifier)
|
MapView(mapState, featuresState, modifier)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -82,15 +61,15 @@ public fun MapView(
|
|||||||
modifier: Modifier = Modifier.fillMaxSize(),
|
modifier: Modifier = Modifier.fillMaxSize(),
|
||||||
buildFeatures: FeatureCollection<Gmc>.() -> Unit = {},
|
buildFeatures: FeatureCollection<Gmc>.() -> Unit = {},
|
||||||
) {
|
) {
|
||||||
val featureState = FeatureCollection.remember(GmcCoordinateSpace, buildFeatures)
|
|
||||||
|
|
||||||
val viewPointOverride: MapViewPoint = remember(initialViewPoint, initialRectangle) {
|
val featureState = FeatureCollection.remember(WebMercatorSpace, buildFeatures)
|
||||||
initialViewPoint
|
val mapState: MapViewScope = rememberMapState(
|
||||||
?: initialRectangle?.computeViewPoint(mapTileProvider)
|
mapTileProvider,
|
||||||
?: featureState.features.values.computeBoundingBox(GmcCoordinateSpace, 1f)
|
config,
|
||||||
?.computeViewPoint(mapTileProvider)
|
featureState.features.values,
|
||||||
?: MapViewPoint.globe
|
initialViewPoint = initialViewPoint,
|
||||||
}
|
initialRectangle = initialRectangle,
|
||||||
|
)
|
||||||
|
|
||||||
val featureDrag: DragHandle<Gmc> = DragHandle.withPrimaryButton { event, start, end ->
|
val featureDrag: DragHandle<Gmc> = DragHandle.withPrimaryButton { event, start, end ->
|
||||||
featureState.forEachWithAttribute(DraggableAttribute) { _, handle ->
|
featureState.forEachWithAttribute(DraggableAttribute) { _, handle ->
|
||||||
@ -107,16 +86,18 @@ public fun MapView(
|
|||||||
DragResult(end)
|
DragResult(end)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
val featureClick: ClickHandle<Gmc> = ClickHandle.withPrimaryButton { event, click ->
|
||||||
|
featureState.forEachWithAttribute(SelectableAttribute) { _, handle ->
|
||||||
|
@Suppress("UNCHECKED_CAST")
|
||||||
|
(handle as ClickHandle<Gmc>).handle(event, click)
|
||||||
|
config.onClick?.handle(event, click)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
val newConfig = config.copy(
|
val newConfig = config.copy(
|
||||||
dragHandle = DragHandle.combine(featureDrag, config.dragHandle)
|
dragHandle = config.dragHandle?.let { DragHandle.combine(featureDrag, it) } ?: featureDrag,
|
||||||
|
onClick = featureClick
|
||||||
)
|
)
|
||||||
|
|
||||||
MapView(
|
MapView(mapState, featureState, modifier)
|
||||||
mapTileProvider = mapTileProvider,
|
|
||||||
initialViewPoint = viewPointOverride,
|
|
||||||
featuresState = featureState,
|
|
||||||
config = newConfig,
|
|
||||||
modifier = modifier,
|
|
||||||
)
|
|
||||||
}
|
}
|
@ -11,7 +11,7 @@ public data class MapViewPoint(
|
|||||||
override val focus: GeodeticMapCoordinates,
|
override val focus: GeodeticMapCoordinates,
|
||||||
override val zoom: Float,
|
override val zoom: Float,
|
||||||
) : ViewPoint<Gmc>{
|
) : ViewPoint<Gmc>{
|
||||||
val scaleFactor: Double by lazy { WebMercatorProjection.scaleFactor(zoom) }
|
val scaleFactor: Float by lazy { WebMercatorProjection.scaleFactor(zoom) }
|
||||||
|
|
||||||
public companion object{
|
public companion object{
|
||||||
public val globe: MapViewPoint = MapViewPoint(GeodeticMapCoordinates(0.0.radians, 0.0.radians), 1f)
|
public val globe: MapViewPoint = MapViewPoint(GeodeticMapCoordinates(0.0.radians, 0.0.radians), 1f)
|
||||||
|
@ -2,29 +2,30 @@ package center.sciprog.maps.compose
|
|||||||
|
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.remember
|
import androidx.compose.runtime.remember
|
||||||
import androidx.compose.ui.unit.*
|
import androidx.compose.ui.unit.Dp
|
||||||
|
import androidx.compose.ui.unit.DpOffset
|
||||||
|
import androidx.compose.ui.unit.DpRect
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
import center.sciprog.maps.coordinates.*
|
import center.sciprog.maps.coordinates.*
|
||||||
import center.sciprog.maps.features.*
|
import center.sciprog.maps.features.*
|
||||||
import kotlin.math.*
|
import kotlin.math.*
|
||||||
|
|
||||||
internal class MapViewState internal constructor(
|
public class MapViewScope internal constructor(
|
||||||
|
public val mapTileProvider: MapTileProvider,
|
||||||
config: ViewConfig<Gmc>,
|
config: ViewConfig<Gmc>,
|
||||||
canvasSize: DpSize,
|
) : CoordinateViewScope<Gmc>(config) {
|
||||||
viewPoint: ViewPoint<Gmc>,
|
override val space: CoordinateSpace<Gmc> get() = WebMercatorSpace
|
||||||
val tileSize: Int,
|
|
||||||
) : CoordinateViewState<Gmc>(config, canvasSize, viewPoint) {
|
|
||||||
override val space: CoordinateSpace<Gmc> get() = GmcCoordinateSpace
|
|
||||||
|
|
||||||
val scaleFactor: Double
|
public val scaleFactor: Float
|
||||||
get() = WebMercatorProjection.scaleFactor(viewPoint.zoom)
|
get() = WebMercatorProjection.scaleFactor(zoom)
|
||||||
|
|
||||||
val intZoom: Int get() = floor(zoom).toInt()
|
public val intZoom: Int get() = floor(zoom).toInt()
|
||||||
|
|
||||||
val centerCoordinates: WebMercatorCoordinates
|
public val centerCoordinates: WebMercatorCoordinates
|
||||||
get() = WebMercatorProjection.toMercator(viewPoint.focus, intZoom)
|
get() = WebMercatorProjection.toMercator(viewPoint.focus, intZoom)
|
||||||
|
|
||||||
val tileScale: Float
|
public val tileScale: Float
|
||||||
get() = 2f.pow(viewPoint.zoom - floor(viewPoint.zoom))
|
get() = 2f.pow(zoom - floor(zoom))
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* Convert screen independent offset to GMC, adjusting for fractional zoom
|
* Convert screen independent offset to GMC, adjusting for fractional zoom
|
||||||
@ -41,8 +42,8 @@ internal class MapViewState internal constructor(
|
|||||||
override fun Gmc.toDpOffset(): DpOffset {
|
override fun Gmc.toDpOffset(): DpOffset {
|
||||||
val mercator = WebMercatorProjection.toMercator(this, intZoom)
|
val mercator = WebMercatorProjection.toMercator(this, intZoom)
|
||||||
return DpOffset(
|
return DpOffset(
|
||||||
(canvasSize.width / 2 + (mercator.x.dp - centerCoordinates.x.dp) * tileScale.toFloat()),
|
(canvasSize.width / 2 + (mercator.x.dp - centerCoordinates.x.dp) * tileScale),
|
||||||
(canvasSize.height / 2 + (mercator.y.dp - centerCoordinates.y.dp) * tileScale.toFloat())
|
(canvasSize.height / 2 + (mercator.y.dp - centerCoordinates.y.dp) * tileScale)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -52,12 +53,12 @@ internal class MapViewState internal constructor(
|
|||||||
return DpRect(topLeft.x, topLeft.y, bottomRight.x, bottomRight.y)
|
return DpRect(topLeft.x, topLeft.y, bottomRight.x, bottomRight.y)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun viewPointFor(rectangle: Rectangle<Gmc>): ViewPoint<Gmc> {
|
override fun computeViewPoint(rectangle: Rectangle<Gmc>): ViewPoint<Gmc> {
|
||||||
val zoom = log2(
|
val zoom = log2(
|
||||||
min(
|
min(
|
||||||
canvasSize.width.value / rectangle.longitudeDelta.radians.value,
|
canvasSize.width.value / rectangle.longitudeDelta.radians.value,
|
||||||
canvasSize.height.value / rectangle.latitudeDelta.radians.value
|
canvasSize.height.value / rectangle.latitudeDelta.radians.value
|
||||||
) * PI / tileSize
|
) * PI / mapTileProvider.tileSize
|
||||||
)
|
)
|
||||||
return MapViewPoint(rectangle.center, zoom.toFloat())
|
return MapViewPoint(rectangle.center, zoom.toFloat())
|
||||||
}
|
}
|
||||||
@ -78,10 +79,21 @@ internal class MapViewState internal constructor(
|
|||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
internal fun rememberMapState(
|
internal fun rememberMapState(
|
||||||
|
mapTileProvider: MapTileProvider,
|
||||||
config: ViewConfig<Gmc>,
|
config: ViewConfig<Gmc>,
|
||||||
canvasSize: DpSize,
|
features: Collection<Feature<Gmc>> = emptyList(),
|
||||||
viewPoint: ViewPoint<Gmc>,
|
initialViewPoint: MapViewPoint? = null,
|
||||||
tileSize: Int,
|
initialRectangle: Rectangle<Gmc>? = null,
|
||||||
): MapViewState = remember {
|
): MapViewScope = remember {
|
||||||
MapViewState(config, canvasSize, viewPoint, tileSize)
|
MapViewScope(mapTileProvider, config).also { mapState->
|
||||||
|
if (initialViewPoint != null) {
|
||||||
|
mapState.viewPoint = initialViewPoint
|
||||||
|
} else if (initialRectangle != null) {
|
||||||
|
mapState.viewPoint = mapState.computeViewPoint(initialRectangle)
|
||||||
|
} else {
|
||||||
|
features.computeBoundingBox(WebMercatorSpace, 1f)?.let {
|
||||||
|
mapState.viewPoint = mapState.computeViewPoint(it)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
@ -1,13 +1,21 @@
|
|||||||
package center.sciprog.maps.compose
|
package center.sciprog.maps.compose
|
||||||
|
|
||||||
|
import androidx.compose.ui.unit.DpOffset
|
||||||
import androidx.compose.ui.unit.DpSize
|
import androidx.compose.ui.unit.DpSize
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
import center.sciprog.maps.coordinates.*
|
import center.sciprog.maps.coordinates.*
|
||||||
import center.sciprog.maps.features.CoordinateSpace
|
import center.sciprog.maps.features.CoordinateSpace
|
||||||
import center.sciprog.maps.features.Rectangle
|
import center.sciprog.maps.features.Rectangle
|
||||||
import center.sciprog.maps.features.ViewPoint
|
import center.sciprog.maps.features.ViewPoint
|
||||||
|
import kotlin.math.floor
|
||||||
import kotlin.math.pow
|
import kotlin.math.pow
|
||||||
|
|
||||||
public object GmcCoordinateSpace : CoordinateSpace<Gmc> {
|
public object WebMercatorSpace : CoordinateSpace<Gmc> {
|
||||||
|
|
||||||
|
private fun intZoom(zoom: Float): Int = floor(zoom).toInt()
|
||||||
|
private fun tileScale(zoom: Float): Float = 2f.pow(zoom - floor(zoom))
|
||||||
|
|
||||||
|
|
||||||
override fun Rectangle(first: Gmc, second: Gmc): Rectangle<Gmc> = GmcRectangle(first, second)
|
override fun Rectangle(first: Gmc, second: Gmc): Rectangle<Gmc> = GmcRectangle(first, second)
|
||||||
|
|
||||||
override fun Rectangle(center: Gmc, zoom: Float, size: DpSize): Rectangle<Gmc> {
|
override fun Rectangle(center: Gmc, zoom: Float, size: DpSize): Rectangle<Gmc> {
|
||||||
@ -15,6 +23,8 @@ public object GmcCoordinateSpace : CoordinateSpace<Gmc> {
|
|||||||
return Rectangle(center, (size.width.value / scale).radians, (size.height.value / scale).radians)
|
return Rectangle(center, (size.width.value / scale).radians, (size.height.value / scale).radians)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override val defaultViewPoint: ViewPoint<Gmc> = MapViewPoint.globe
|
||||||
|
|
||||||
override fun ViewPoint(center: Gmc, zoom: Float): ViewPoint<Gmc> = MapViewPoint(center, zoom)
|
override fun ViewPoint(center: Gmc, zoom: Float): ViewPoint<Gmc> = MapViewPoint(center, zoom)
|
||||||
|
|
||||||
override fun ViewPoint<Gmc>.moveBy(delta: Gmc): ViewPoint<Gmc> {
|
override fun ViewPoint<Gmc>.moveBy(delta: Gmc): ViewPoint<Gmc> {
|
||||||
@ -29,7 +39,7 @@ public object GmcCoordinateSpace : CoordinateSpace<Gmc> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
override fun ViewPoint<Gmc>.zoomBy(zoomDelta: Float, invariant: Gmc): ViewPoint<Gmc> = if (invariant == focus) {
|
override fun ViewPoint<Gmc>.zoomBy(zoomDelta: Float, invariant: Gmc): ViewPoint<Gmc> = if (invariant == focus) {
|
||||||
ViewPoint(focus, (zoom + zoomDelta).coerceIn(2f, 18f) )
|
ViewPoint(focus, (zoom + zoomDelta).coerceIn(2f, 18f))
|
||||||
} else {
|
} else {
|
||||||
val difScale = (1 - 2f.pow(-zoomDelta))
|
val difScale = (1 - 2f.pow(-zoomDelta))
|
||||||
val newCenter = GeodeticMapCoordinates(
|
val newCenter = GeodeticMapCoordinates(
|
||||||
@ -61,6 +71,17 @@ public object GmcCoordinateSpace : CoordinateSpace<Gmc> {
|
|||||||
val maxLong = maxOf { it.longitude }
|
val maxLong = maxOf { it.longitude }
|
||||||
return GmcRectangle(GeodeticMapCoordinates(minLat, minLong), GeodeticMapCoordinates(maxLat, maxLong))
|
return GmcRectangle(GeodeticMapCoordinates(minLat, minLong), GeodeticMapCoordinates(maxLat, maxLong))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun Gmc.offsetTo(b: Gmc, zoom: Float): DpOffset {
|
||||||
|
val intZoom = intZoom(zoom)
|
||||||
|
val mercatorA = WebMercatorProjection.toMercator(this, intZoom)
|
||||||
|
val mercatorB = WebMercatorProjection.toMercator(b, intZoom)
|
||||||
|
val tileScale = tileScale(zoom)
|
||||||
|
return DpOffset(
|
||||||
|
(mercatorA.x - mercatorB.x).dp * tileScale,
|
||||||
|
(mercatorA.y - mercatorB.y).dp * tileScale
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
@ -15,7 +15,10 @@ import androidx.compose.ui.unit.IntOffset
|
|||||||
import androidx.compose.ui.unit.IntSize
|
import androidx.compose.ui.unit.IntSize
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import center.sciprog.maps.coordinates.Gmc
|
import center.sciprog.maps.coordinates.Gmc
|
||||||
import center.sciprog.maps.features.*
|
import center.sciprog.maps.features.FeatureCollection
|
||||||
|
import center.sciprog.maps.features.PainterFeature
|
||||||
|
import center.sciprog.maps.features.drawFeature
|
||||||
|
import center.sciprog.maps.features.z
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import kotlinx.coroutines.supervisorScope
|
import kotlinx.coroutines.supervisorScope
|
||||||
import mu.KotlinLogging
|
import mu.KotlinLogging
|
||||||
@ -40,21 +43,10 @@ private val logger = KotlinLogging.logger("MapView")
|
|||||||
*/
|
*/
|
||||||
@Composable
|
@Composable
|
||||||
public actual fun MapView(
|
public actual fun MapView(
|
||||||
mapTileProvider: MapTileProvider,
|
mapState: MapViewScope,
|
||||||
initialViewPoint: MapViewPoint,
|
|
||||||
featuresState: FeatureCollection<Gmc>,
|
featuresState: FeatureCollection<Gmc>,
|
||||||
config: ViewConfig<Gmc>,
|
|
||||||
modifier: Modifier,
|
modifier: Modifier,
|
||||||
): Unit = key(initialViewPoint) {
|
): Unit = with(mapState) {
|
||||||
|
|
||||||
val state = rememberMapState(
|
|
||||||
config,
|
|
||||||
defaultCanvasSize,
|
|
||||||
initialViewPoint,
|
|
||||||
mapTileProvider.tileSize
|
|
||||||
)
|
|
||||||
|
|
||||||
with(state) {
|
|
||||||
|
|
||||||
val mapTiles = remember(mapTileProvider) { mutableStateListOf<MapTile>() }
|
val mapTiles = remember(mapTileProvider) { mutableStateListOf<MapTile>() }
|
||||||
|
|
||||||
@ -101,7 +93,7 @@ public actual fun MapView(
|
|||||||
featuresState.features.values.filterIsInstance<PainterFeature<Gmc>>().associateWith { it.getPainter() }
|
featuresState.features.values.filterIsInstance<PainterFeature<Gmc>>().associateWith { it.getPainter() }
|
||||||
}
|
}
|
||||||
|
|
||||||
Canvas(modifier = modifier.mapControls(state).fillMaxSize()) {
|
Canvas(modifier = modifier.mapControls(mapState).fillMaxSize()) {
|
||||||
|
|
||||||
if (canvasSize != size.toDpSize()) {
|
if (canvasSize != size.toDpSize()) {
|
||||||
logger.debug { "Recalculate canvas. Size: $size" }
|
logger.debug { "Recalculate canvas. Size: $size" }
|
||||||
@ -127,8 +119,9 @@ public actual fun MapView(
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
featuresState.features.values.filter { viewPoint.zoom in it.zoomRange }.sortedBy { it.z }.forEach { feature ->
|
featuresState.features.values.filter { viewPoint.zoom in it.zoomRange }.sortedBy { it.z }
|
||||||
drawFeature(state, painterCache, feature)
|
.forEach { feature ->
|
||||||
|
drawFeature(mapState, painterCache, feature)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -146,5 +139,5 @@ public actual fun MapView(
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -7,14 +7,14 @@ package center.sciprog.maps.coordinates
|
|||||||
|
|
||||||
import kotlin.math.*
|
import kotlin.math.*
|
||||||
|
|
||||||
public data class WebMercatorCoordinates(val zoom: Int, val x: Double, val y: Double)
|
public data class WebMercatorCoordinates(val zoom: Int, val x: Float, val y: Float)
|
||||||
|
|
||||||
public object WebMercatorProjection {
|
public object WebMercatorProjection {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Compute radians to projection coordinates ratio for given [zoom] factor
|
* Compute radians to projection coordinates ratio for given [zoom] factor
|
||||||
*/
|
*/
|
||||||
public fun scaleFactor(zoom: Float): Double = 256.0 / 2 / PI * 2f.pow(zoom)
|
public fun scaleFactor(zoom: Float): Float = (256.0 / 2 / PI * 2f.pow(zoom)).toFloat()
|
||||||
|
|
||||||
public fun toGeodetic(mercator: WebMercatorCoordinates): GeodeticMapCoordinates {
|
public fun toGeodetic(mercator: WebMercatorCoordinates): GeodeticMapCoordinates {
|
||||||
val scaleFactor = scaleFactor(mercator.zoom.toFloat())
|
val scaleFactor = scaleFactor(mercator.zoom.toFloat())
|
||||||
@ -32,8 +32,8 @@ public object WebMercatorProjection {
|
|||||||
val scaleFactor = scaleFactor(zoom.toFloat())
|
val scaleFactor = scaleFactor(zoom.toFloat())
|
||||||
return WebMercatorCoordinates(
|
return WebMercatorCoordinates(
|
||||||
zoom = zoom,
|
zoom = zoom,
|
||||||
x = scaleFactor * (gmc.longitude.radians.value + PI),
|
x = scaleFactor * (gmc.longitude.radians.value + PI).toFloat(),
|
||||||
y = scaleFactor * (PI - ln(tan(PI / 4 + gmc.latitude.radians.value / 2)))
|
y = scaleFactor * (PI - ln(tan(PI / 4 + gmc.latitude.radians.value / 2))).toFloat()
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -5,7 +5,11 @@ import androidx.compose.ui.graphics.Color
|
|||||||
|
|
||||||
public object ZAttribute : Feature.Attribute<Float>
|
public object ZAttribute : Feature.Attribute<Float>
|
||||||
|
|
||||||
public object SelectableAttribute : Feature.Attribute<(FeatureId<*>, SelectableFeature<*>) -> Unit>
|
public object DraggableAttribute : Feature.Attribute<DragHandle<Any>>
|
||||||
|
|
||||||
|
public object DragListenerAttribute : Feature.Attribute<Set<(begin: Any, end: Any) -> Unit>>
|
||||||
|
|
||||||
|
public object SelectableAttribute : Feature.Attribute<ClickHandle<Any>>
|
||||||
|
|
||||||
public object VisibleAttribute : Feature.Attribute<Boolean>
|
public object VisibleAttribute : Feature.Attribute<Boolean>
|
||||||
|
|
||||||
|
@ -1,6 +1,11 @@
|
|||||||
package center.sciprog.maps.features
|
package center.sciprog.maps.features
|
||||||
|
|
||||||
|
import androidx.compose.ui.unit.Dp
|
||||||
|
import androidx.compose.ui.unit.DpOffset
|
||||||
import androidx.compose.ui.unit.DpSize
|
import androidx.compose.ui.unit.DpSize
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import kotlin.math.abs
|
||||||
|
import kotlin.math.sqrt
|
||||||
|
|
||||||
|
|
||||||
public interface Area<T : Any> {
|
public interface Area<T : Any> {
|
||||||
@ -31,6 +36,11 @@ public interface CoordinateSpace<T : Any> {
|
|||||||
*/
|
*/
|
||||||
public fun Rectangle(center: T, zoom: Float, size: DpSize): Rectangle<T>
|
public fun Rectangle(center: T, zoom: Float, size: DpSize): Rectangle<T>
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A view point used by default
|
||||||
|
*/
|
||||||
|
public val defaultViewPoint: ViewPoint<T>
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Create a [ViewPoint] associated with this coordinate space.
|
* Create a [ViewPoint] associated with this coordinate space.
|
||||||
*/
|
*/
|
||||||
@ -52,6 +62,20 @@ public interface CoordinateSpace<T : Any> {
|
|||||||
|
|
||||||
public fun Collection<T>.wrapPoints(): Rectangle<T>?
|
public fun Collection<T>.wrapPoints(): Rectangle<T>?
|
||||||
|
|
||||||
|
public fun T.offsetTo(b: T, zoom: Float): DpOffset
|
||||||
|
|
||||||
|
public fun T.distanceTo(b: T, zoom: Float): Dp {
|
||||||
|
val offset = offsetTo(b, zoom)
|
||||||
|
return sqrt(offset.x.value * offset.x.value + offset.y.value * offset.y.value).dp
|
||||||
|
}
|
||||||
|
|
||||||
|
public fun T.distanceToLine(a: T, b: T, zoom: Float): Dp {
|
||||||
|
val d12 = a.offsetTo(b, zoom)
|
||||||
|
val d01 = offsetTo(a, zoom)
|
||||||
|
val distanceVale = abs(d12.x.value * d01.y.value - d12.y.value * d01.x.value) / a.distanceTo(b, zoom).value
|
||||||
|
|
||||||
|
return distanceVale.dp
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public fun <T : Any> CoordinateSpace<T>.Rectangle(viewPoint: ViewPoint<T>, size: DpSize): Rectangle<T> =
|
public fun <T : Any> CoordinateSpace<T>.Rectangle(viewPoint: ViewPoint<T>, size: DpSize): Rectangle<T> =
|
||||||
|
@ -6,32 +6,43 @@ import androidx.compose.runtime.mutableStateOf
|
|||||||
import androidx.compose.runtime.setValue
|
import androidx.compose.runtime.setValue
|
||||||
import androidx.compose.ui.geometry.Offset
|
import androidx.compose.ui.geometry.Offset
|
||||||
import androidx.compose.ui.unit.*
|
import androidx.compose.ui.unit.*
|
||||||
|
import kotlin.math.pow
|
||||||
|
import kotlin.math.sqrt
|
||||||
|
|
||||||
public abstract class CoordinateViewState<T : Any>(
|
private fun distanceBetween(a: DpOffset, b: DpOffset): Dp = sqrt((b.x - a.x).value.pow(2) + (b.y - a.y).value.pow(2)).dp
|
||||||
|
|
||||||
|
public abstract class CoordinateViewScope<T : Any>(
|
||||||
public val config: ViewConfig<T>,
|
public val config: ViewConfig<T>,
|
||||||
canvasSize: DpSize,
|
|
||||||
viewPoint: ViewPoint<T>,
|
|
||||||
) {
|
) {
|
||||||
|
|
||||||
public abstract val space: CoordinateSpace<T>
|
public abstract val space: CoordinateSpace<T>
|
||||||
|
|
||||||
public var canvasSize: DpSize by mutableStateOf(canvasSize)
|
protected var canvasSizeState: MutableState<DpSize?> = mutableStateOf(null)
|
||||||
protected var viewPointState: MutableState<ViewPoint<T>> = mutableStateOf(viewPoint)
|
protected var viewPointState: MutableState<ViewPoint<T>?> = mutableStateOf(null)
|
||||||
|
|
||||||
|
public var canvasSize: DpSize
|
||||||
|
get() = canvasSizeState.value ?: DpSize(512.dp, 512.dp)
|
||||||
|
set(value) {
|
||||||
|
canvasSizeState.value = value
|
||||||
|
}
|
||||||
|
|
||||||
public var viewPoint: ViewPoint<T>
|
public var viewPoint: ViewPoint<T>
|
||||||
get() = viewPointState.value
|
get() = viewPointState.value ?: space.defaultViewPoint
|
||||||
set(value) {
|
set(value) {
|
||||||
config.onViewChange(value)
|
|
||||||
viewPointState.value = value
|
viewPointState.value = value
|
||||||
}
|
}
|
||||||
|
|
||||||
public val zoom: Float get() = viewPoint.zoom
|
public val zoom: Float get() = viewPoint.zoom
|
||||||
|
|
||||||
|
|
||||||
|
// Selection rectangle. If null - no selection
|
||||||
|
public var selectRect: DpRect? by mutableStateOf(null)
|
||||||
|
|
||||||
public abstract fun DpOffset.toCoordinates(): T
|
public abstract fun DpOffset.toCoordinates(): T
|
||||||
|
|
||||||
public abstract fun T.toDpOffset(): DpOffset
|
public abstract fun T.toDpOffset(): DpOffset
|
||||||
|
|
||||||
public fun T.toOffset(density: Density): Offset = with(density){
|
public fun T.toOffset(density: Density): Offset = with(density) {
|
||||||
val dpOffset = this@toOffset.toDpOffset()
|
val dpOffset = this@toOffset.toDpOffset()
|
||||||
Offset(dpOffset.x.toPx(), dpOffset.y.toPx())
|
Offset(dpOffset.x.toPx(), dpOffset.y.toPx())
|
||||||
}
|
}
|
||||||
@ -40,10 +51,7 @@ public abstract class CoordinateViewState<T : Any>(
|
|||||||
|
|
||||||
public abstract fun ViewPoint<T>.moveBy(x: Dp, y: Dp): ViewPoint<T>
|
public abstract fun ViewPoint<T>.moveBy(x: Dp, y: Dp): ViewPoint<T>
|
||||||
|
|
||||||
public abstract fun viewPointFor(rectangle: Rectangle<T>): ViewPoint<T>
|
public abstract fun computeViewPoint(rectangle: Rectangle<T>): ViewPoint<T>
|
||||||
|
|
||||||
// Selection rectangle. If null - no selection
|
|
||||||
public var selectRect: DpRect? by mutableStateOf(null)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -1,6 +0,0 @@
|
|||||||
package center.sciprog.maps.features
|
|
||||||
|
|
||||||
|
|
||||||
public object DraggableAttribute : Feature.Attribute<DragHandle<Any>>
|
|
||||||
|
|
||||||
public object DragListenerAttribute : Feature.Attribute<Set<(begin: Any, end: Any) -> Unit>>
|
|
@ -29,14 +29,14 @@ public interface Feature<T : Any> {
|
|||||||
public fun getBoundingBox(zoom: Float): Rectangle<T>?
|
public fun getBoundingBox(zoom: Float): Rectangle<T>?
|
||||||
}
|
}
|
||||||
|
|
||||||
public interface PainterFeature<T:Any>: Feature<T> {
|
public interface PainterFeature<T : Any> : Feature<T> {
|
||||||
@Composable
|
@Composable
|
||||||
public fun getPainter(): Painter
|
public fun getPainter(): Painter
|
||||||
}
|
}
|
||||||
|
|
||||||
public interface SelectableFeature<T : Any> : Feature<T> {
|
public interface SelectableFeature<T : Any> : Feature<T> {
|
||||||
public operator fun contains(point: ViewPoint<T>): Boolean = getBoundingBox(point.zoom)?.let {
|
public fun contains(point: T, zoom: Float): Boolean = getBoundingBox(zoom)?.let {
|
||||||
point.focus in it
|
point in it
|
||||||
} ?: false
|
} ?: false
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -47,7 +47,7 @@ public interface DraggableFeature<T : Any> : SelectableFeature<T> {
|
|||||||
/**
|
/**
|
||||||
* A draggable marker feature. Other features could be bound to this one.
|
* A draggable marker feature. Other features could be bound to this one.
|
||||||
*/
|
*/
|
||||||
public interface MarkerFeature<T: Any>: DraggableFeature<T>{
|
public interface MarkerFeature<T : Any> : DraggableFeature<T> {
|
||||||
public val center: T
|
public val center: T
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -72,7 +72,6 @@ public class FeatureSelector<T : Any>(
|
|||||||
public val selector: (zoom: Float) -> Feature<T>,
|
public val selector: (zoom: Float) -> Feature<T>,
|
||||||
) : Feature<T> {
|
) : Feature<T> {
|
||||||
|
|
||||||
|
|
||||||
override fun getBoundingBox(zoom: Float): Rectangle<T>? = selector(zoom).getBoundingBox(zoom)
|
override fun getBoundingBox(zoom: Float): Rectangle<T>? = selector(zoom).getBoundingBox(zoom)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -157,8 +156,8 @@ public class LineFeature<T : Any>(
|
|||||||
override fun getBoundingBox(zoom: Float): Rectangle<T> =
|
override fun getBoundingBox(zoom: Float): Rectangle<T> =
|
||||||
space.Rectangle(a, b)
|
space.Rectangle(a, b)
|
||||||
|
|
||||||
override fun contains(point: ViewPoint<T>): Boolean {
|
override fun contains(point: T, zoom: Float): Boolean = with(space) {
|
||||||
return super.contains(point)
|
point in space.Rectangle(a, b) && point.distanceToLine(a, b, zoom).value < 5f
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -228,17 +227,17 @@ public data class VectorImageFeature<T : Any>(
|
|||||||
*
|
*
|
||||||
* @param rectangle the size of background in scheme size units. The screen units to scheme units ratio equals scale.
|
* @param rectangle the size of background in scheme size units. The screen units to scheme units ratio equals scale.
|
||||||
*/
|
*/
|
||||||
public class ScalableImageFeature<T: Any>(
|
public class ScalableImageFeature<T : Any>(
|
||||||
override val space: CoordinateSpace<T>,
|
override val space: CoordinateSpace<T>,
|
||||||
public val rectangle: Rectangle<T>,
|
public val rectangle: Rectangle<T>,
|
||||||
override val zoomRange: FloatRange,
|
override val zoomRange: FloatRange,
|
||||||
override val attributes: AttributeMap = AttributeMap(),
|
override val attributes: AttributeMap = AttributeMap(),
|
||||||
public val painter: @Composable () -> Painter,
|
public val painter: @Composable () -> Painter,
|
||||||
) : Feature<T>, PainterFeature<T>{
|
) : Feature<T>, PainterFeature<T> {
|
||||||
@Composable
|
@Composable
|
||||||
override fun getPainter(): Painter = painter.invoke()
|
override fun getPainter(): Painter = painter.invoke()
|
||||||
|
|
||||||
override fun getBoundingBox(zoom: Float): Rectangle<T> =rectangle
|
override fun getBoundingBox(zoom: Float): Rectangle<T> = rectangle
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@ -119,9 +119,14 @@ public class FeatureCollection<T : Any>(
|
|||||||
|
|
||||||
@Suppress("UNCHECKED_CAST")
|
@Suppress("UNCHECKED_CAST")
|
||||||
public fun <F : SelectableFeature<T>> FeatureId<F>.selectable(
|
public fun <F : SelectableFeature<T>> FeatureId<F>.selectable(
|
||||||
onSelect: (FeatureId<F>, F) -> Unit,
|
onSelect: () -> Unit,
|
||||||
) {
|
) {
|
||||||
setAttribute(this, SelectableAttribute) { id, feature -> onSelect(id as FeatureId<F>, feature as F) }
|
// val handle = ClickHandle<Any> { event, click ->
|
||||||
|
// val feature: F = get(this@selectable)
|
||||||
|
// if (feature.contains(this, click.focus))
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// setAttribute(this, SelectableAttribute, handle)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@ -1,12 +1,27 @@
|
|||||||
package center.sciprog.maps.features
|
package center.sciprog.maps.features
|
||||||
|
|
||||||
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.unit.DpSize
|
import androidx.compose.ui.unit.DpSize
|
||||||
|
|
||||||
|
public fun interface ClickHandle<T : Any> {
|
||||||
|
public fun handle(event: PointerEvent, click: ViewPoint<T>): Unit
|
||||||
|
|
||||||
|
public companion object {
|
||||||
|
public fun <T : Any> withPrimaryButton(
|
||||||
|
block: (event: PointerEvent, click: ViewPoint<T>) -> Unit,
|
||||||
|
): ClickHandle<T> = ClickHandle { event, click ->
|
||||||
|
if (event.buttons.isPrimaryPressed) {
|
||||||
|
block(event, click)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public data class ViewConfig<T : Any>(
|
public data class ViewConfig<T : Any>(
|
||||||
val zoomSpeed: Float = 1f / 3f,
|
val zoomSpeed: Float = 1f / 3f,
|
||||||
val onClick: ViewPoint<T>.(PointerEvent) -> Unit = {},
|
val onClick: ClickHandle<T>? = null,
|
||||||
val dragHandle: DragHandle<T> = DragHandle.bypass(),
|
val dragHandle: DragHandle<T>? = null,
|
||||||
val onViewChange: ViewPoint<T>.() -> Unit = {},
|
val onViewChange: ViewPoint<T>.() -> Unit = {},
|
||||||
val onSelect: (Rectangle<T>) -> Unit = {},
|
val onSelect: (Rectangle<T>) -> Unit = {},
|
||||||
val zoomOnSelect: Boolean = true,
|
val zoomOnSelect: Boolean = true,
|
||||||
|
@ -1,14 +1,14 @@
|
|||||||
package center.sciprog.maps.compose
|
package center.sciprog.maps.compose
|
||||||
|
|
||||||
import androidx.compose.foundation.gestures.drag
|
import androidx.compose.foundation.gestures.drag
|
||||||
import androidx.compose.foundation.gestures.forEachGesture
|
|
||||||
import androidx.compose.runtime.*
|
|
||||||
import androidx.compose.ui.ExperimentalComposeUiApi
|
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.input.pointer.*
|
import androidx.compose.ui.input.pointer.*
|
||||||
import androidx.compose.ui.unit.*
|
import androidx.compose.ui.unit.DpOffset
|
||||||
import center.sciprog.maps.features.CoordinateViewState
|
import androidx.compose.ui.unit.DpRect
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import center.sciprog.maps.features.CoordinateViewScope
|
||||||
import center.sciprog.maps.features.bottomRight
|
import center.sciprog.maps.features.bottomRight
|
||||||
import center.sciprog.maps.features.topLeft
|
import center.sciprog.maps.features.topLeft
|
||||||
import kotlin.math.max
|
import kotlin.math.max
|
||||||
@ -17,16 +17,37 @@ import kotlin.math.min
|
|||||||
|
|
||||||
@OptIn(ExperimentalComposeUiApi::class)
|
@OptIn(ExperimentalComposeUiApi::class)
|
||||||
public fun <T : Any> Modifier.mapControls(
|
public fun <T : Any> Modifier.mapControls(
|
||||||
state: CoordinateViewState<T>,
|
state: CoordinateViewScope<T>,
|
||||||
): Modifier = with(state) {
|
): Modifier = with(state) {
|
||||||
pointerInput(Unit) {
|
pointerInput(Unit) {
|
||||||
forEachGesture {
|
|
||||||
awaitPointerEventScope {
|
|
||||||
fun Offset.toDpOffset() = DpOffset(x.toDp(), y.toDp())
|
fun Offset.toDpOffset() = DpOffset(x.toDp(), y.toDp())
|
||||||
|
awaitPointerEventScope {
|
||||||
|
while (true) {
|
||||||
|
val event = awaitPointerEvent()
|
||||||
|
if (event.type == PointerEventType.Release ) {
|
||||||
|
config.onClick?.handle(
|
||||||
|
event,
|
||||||
|
space.ViewPoint(event.changes.first().position.toDpOffset().toCoordinates(), zoom)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}.pointerInput(Unit) {
|
||||||
|
fun Offset.toDpOffset() = DpOffset(x.toDp(), y.toDp())
|
||||||
|
awaitPointerEventScope {
|
||||||
|
while (true) {
|
||||||
val event: PointerEvent = awaitPointerEvent()
|
val event: PointerEvent = awaitPointerEvent()
|
||||||
|
|
||||||
event.changes.forEach { change ->
|
event.changes.forEach { change ->
|
||||||
|
|
||||||
|
if (event.type == PointerEventType.Scroll) {
|
||||||
|
val (xPos, yPos) = change.position
|
||||||
|
//compute invariant point of translation
|
||||||
|
val invariant = DpOffset(xPos.toDp(), yPos.toDp()).toCoordinates()
|
||||||
|
viewPoint = with(space) {
|
||||||
|
viewPoint.zoomBy(-change.scrollDelta.y * config.zoomSpeed, invariant)
|
||||||
|
}
|
||||||
|
change.consume()
|
||||||
|
}
|
||||||
//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())
|
||||||
|
|
||||||
@ -45,12 +66,12 @@ public fun <T : Any> Modifier.mapControls(
|
|||||||
|
|
||||||
//apply drag handle and check if it prohibits the drag even propagation
|
//apply drag handle and check if it prohibits the drag even propagation
|
||||||
if (selectionStart == null) {
|
if (selectionStart == null) {
|
||||||
val dragResult = config.dragHandle.handle(
|
val dragResult = config.dragHandle?.handle(
|
||||||
event,
|
event,
|
||||||
space.ViewPoint(dpStart.toCoordinates(), viewPoint.zoom),
|
space.ViewPoint(dpStart.toCoordinates(), zoom),
|
||||||
space.ViewPoint(dpEnd.toCoordinates(), viewPoint.zoom)
|
space.ViewPoint(dpEnd.toCoordinates(), zoom)
|
||||||
)
|
)
|
||||||
if(!dragResult.handleNext) return@drag
|
if (dragResult?.handleNext == false) return@drag
|
||||||
}
|
}
|
||||||
|
|
||||||
if (event.buttons.isPrimaryPressed) {
|
if (event.buttons.isPrimaryPressed) {
|
||||||
@ -85,20 +106,12 @@ public fun <T : Any> Modifier.mapControls(
|
|||||||
)
|
)
|
||||||
config.onSelect(coordinateRect)
|
config.onSelect(coordinateRect)
|
||||||
if (config.zoomOnSelect) {
|
if (config.zoomOnSelect) {
|
||||||
viewPoint = viewPointFor(coordinateRect)
|
viewPoint = computeViewPoint(coordinateRect)
|
||||||
}
|
}
|
||||||
selectRect = null
|
selectRect = null
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}.onPointerEvent(PointerEventType.Scroll) {
|
|
||||||
val change = it.changes.first()
|
|
||||||
val (xPos, yPos) = change.position
|
|
||||||
//compute invariant point of translation
|
|
||||||
val invariant = DpOffset(xPos.toDp(), yPos.toDp()).toCoordinates()
|
|
||||||
viewPoint = with(space) {
|
|
||||||
viewPoint.zoomBy(-change.scrollDelta.y * config.zoomSpeed, invariant)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -21,7 +21,7 @@ internal fun Color.toPaint(): Paint = Paint().apply {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public fun <T : Any> DrawScope.drawFeature(
|
public fun <T : Any> DrawScope.drawFeature(
|
||||||
state: CoordinateViewState<T>,
|
state: CoordinateViewScope<T>,
|
||||||
painterCache: Map<PainterFeature<T>, Painter>,
|
painterCache: Map<PainterFeature<T>, Painter>,
|
||||||
feature: Feature<T>,
|
feature: Feature<T>,
|
||||||
): Unit = with(state) {
|
): Unit = with(state) {
|
||||||
|
@ -1,6 +1,8 @@
|
|||||||
package center.sciprog.maps.scheme
|
package center.sciprog.maps.scheme
|
||||||
|
|
||||||
|
import androidx.compose.ui.unit.DpOffset
|
||||||
import androidx.compose.ui.unit.DpSize
|
import androidx.compose.ui.unit.DpSize
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
import center.sciprog.maps.features.CoordinateSpace
|
import center.sciprog.maps.features.CoordinateSpace
|
||||||
import center.sciprog.maps.features.Rectangle
|
import center.sciprog.maps.features.Rectangle
|
||||||
import center.sciprog.maps.features.ViewPoint
|
import center.sciprog.maps.features.ViewPoint
|
||||||
@ -61,4 +63,11 @@ object XYCoordinateSpace : CoordinateSpace<XY> {
|
|||||||
XY(maxX, maxY)
|
XY(maxX, maxY)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override val defaultViewPoint: ViewPoint<XY> = XYViewPoint(XY(0f, 0f), 1f)
|
||||||
|
|
||||||
|
override fun XY.offsetTo(b: XY, zoom: Float): DpOffset = DpOffset(
|
||||||
|
(b.x - x).dp * zoom,
|
||||||
|
(b.y - y).dp * zoom
|
||||||
|
)
|
||||||
}
|
}
|
@ -1,14 +1,17 @@
|
|||||||
package center.sciprog.maps.scheme
|
package center.sciprog.maps.scheme
|
||||||
|
|
||||||
import androidx.compose.ui.unit.*
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.remember
|
||||||
|
import androidx.compose.ui.unit.Dp
|
||||||
|
import androidx.compose.ui.unit.DpOffset
|
||||||
|
import androidx.compose.ui.unit.DpRect
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
import center.sciprog.maps.features.*
|
import center.sciprog.maps.features.*
|
||||||
import kotlin.math.min
|
import kotlin.math.min
|
||||||
|
|
||||||
class XYViewState(
|
class XYViewScope(
|
||||||
config: ViewConfig<XY>,
|
config: ViewConfig<XY>,
|
||||||
canvasSize: DpSize,
|
) : CoordinateViewScope<XY>(config) {
|
||||||
viewPoint: ViewPoint<XY>,
|
|
||||||
) : CoordinateViewState<XY>(config, canvasSize, viewPoint) {
|
|
||||||
override val space: CoordinateSpace<XY>
|
override val space: CoordinateSpace<XY>
|
||||||
get() = XYCoordinateSpace
|
get() = XYCoordinateSpace
|
||||||
|
|
||||||
@ -21,8 +24,7 @@ class XYViewState(
|
|||||||
(canvasSize.width / 2 + (x.dp - viewPoint.focus.x.dp) * viewPoint.zoom),
|
(canvasSize.width / 2 + (x.dp - viewPoint.focus.x.dp) * viewPoint.zoom),
|
||||||
(canvasSize.height / 2 + (viewPoint.focus.y.dp - y.dp) * viewPoint.zoom)
|
(canvasSize.height / 2 + (viewPoint.focus.y.dp - y.dp) * viewPoint.zoom)
|
||||||
)
|
)
|
||||||
|
override fun computeViewPoint(rectangle: Rectangle<XY>): ViewPoint<XY> {
|
||||||
override fun viewPointFor(rectangle: Rectangle<XY>): ViewPoint<XY> {
|
|
||||||
val scale = min(
|
val scale = min(
|
||||||
canvasSize.width.value / rectangle.width,
|
canvasSize.width.value / rectangle.width,
|
||||||
canvasSize.height.value / rectangle.height
|
canvasSize.height.value / rectangle.height
|
||||||
@ -41,5 +43,24 @@ class XYViewState(
|
|||||||
val bottomRight = rightBottom.toDpOffset()
|
val bottomRight = rightBottom.toDpOffset()
|
||||||
return DpRect(topLeft.x, topLeft.y, bottomRight.x, bottomRight.y)
|
return DpRect(topLeft.x, topLeft.y, bottomRight.x, bottomRight.y)
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
public fun rememberMapState(
|
||||||
|
config: ViewConfig<XY>,
|
||||||
|
features: Collection<Feature<XY>> = emptyList(),
|
||||||
|
initialViewPoint: ViewPoint<XY>? = null,
|
||||||
|
initialRectangle: Rectangle<XY>? = null,
|
||||||
|
): XYViewScope = remember {
|
||||||
|
XYViewScope(config).also { mapState->
|
||||||
|
if (initialViewPoint != null) {
|
||||||
|
mapState.viewPoint = initialViewPoint
|
||||||
|
} else if (initialRectangle != null) {
|
||||||
|
mapState.viewPoint = mapState.computeViewPoint(initialRectangle)
|
||||||
|
} else {
|
||||||
|
features.computeBoundingBox(XYCoordinateSpace, 1f)?.let {
|
||||||
|
mapState.viewPoint = mapState.computeViewPoint(it)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
@ -4,7 +4,6 @@ import androidx.compose.foundation.Canvas
|
|||||||
import androidx.compose.foundation.layout.fillMaxSize
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.key
|
import androidx.compose.runtime.key
|
||||||
import androidx.compose.runtime.remember
|
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.graphics.Color
|
import androidx.compose.ui.graphics.Color
|
||||||
import androidx.compose.ui.graphics.PathEffect
|
import androidx.compose.ui.graphics.PathEffect
|
||||||
@ -22,19 +21,10 @@ private val logger = KotlinLogging.logger("SchemeView")
|
|||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
public fun SchemeView(
|
public fun SchemeView(
|
||||||
initialViewPoint: ViewPoint<XY>,
|
state: XYViewScope,
|
||||||
featuresState: FeatureCollection<XY>,
|
featuresState: FeatureCollection<XY>,
|
||||||
config: ViewConfig<XY>,
|
|
||||||
modifier: Modifier = Modifier.fillMaxSize(),
|
modifier: Modifier = Modifier.fillMaxSize(),
|
||||||
) = key(initialViewPoint) {
|
) {
|
||||||
|
|
||||||
val state = remember {
|
|
||||||
XYViewState(
|
|
||||||
config,
|
|
||||||
defaultCanvasSize,
|
|
||||||
initialViewPoint,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
with(state) {
|
with(state) {
|
||||||
val painterCache: Map<PainterFeature<XY>, Painter> = key(featuresState) {
|
val painterCache: Map<PainterFeature<XY>, Painter> = key(featuresState) {
|
||||||
featuresState.features.values.filterIsInstance<PainterFeature<XY>>().associateWith { it.getPainter() }
|
featuresState.features.values.filterIsInstance<PainterFeature<XY>>().associateWith { it.getPainter() }
|
||||||
@ -95,20 +85,22 @@ public fun SchemeView(
|
|||||||
config: ViewConfig<XY> = ViewConfig(),
|
config: ViewConfig<XY> = ViewConfig(),
|
||||||
modifier: Modifier = Modifier.fillMaxSize(),
|
modifier: Modifier = Modifier.fillMaxSize(),
|
||||||
) {
|
) {
|
||||||
|
|
||||||
|
|
||||||
val featuresState = key(featureMap) {
|
val featuresState = key(featureMap) {
|
||||||
FeatureCollection.build(XYCoordinateSpace) {
|
FeatureCollection.build(XYCoordinateSpace) {
|
||||||
featureMap.forEach { feature(it.key.id, it.value) }
|
featureMap.forEach { feature(it.key.id, it.value) }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
val viewPointOverride: ViewPoint<XY> = remember(initialViewPoint, initialRectangle) {
|
val state = rememberMapState(
|
||||||
initialViewPoint
|
config,
|
||||||
?: initialRectangle?.computeViewPoint()
|
featuresState.features.values,
|
||||||
?: featureMap.values.computeBoundingBox(XYCoordinateSpace, 1f)?.computeViewPoint()
|
initialViewPoint = initialViewPoint,
|
||||||
?: XYViewPoint(XY(0f, 0f), 1f)
|
initialRectangle = initialRectangle,
|
||||||
}
|
)
|
||||||
|
|
||||||
SchemeView(viewPointOverride, featuresState, config, modifier)
|
SchemeView(state, featuresState, modifier)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -127,37 +119,42 @@ public fun SchemeView(
|
|||||||
buildFeatures: FeatureCollection<XY>.() -> Unit = {},
|
buildFeatures: FeatureCollection<XY>.() -> Unit = {},
|
||||||
) {
|
) {
|
||||||
val featureState = FeatureCollection.remember(XYCoordinateSpace, buildFeatures)
|
val featureState = FeatureCollection.remember(XYCoordinateSpace, buildFeatures)
|
||||||
|
val mapState: XYViewScope = rememberMapState(
|
||||||
|
config,
|
||||||
|
featureState.features.values,
|
||||||
|
initialViewPoint = initialViewPoint,
|
||||||
|
initialRectangle = initialRectangle,
|
||||||
|
)
|
||||||
|
|
||||||
val viewPointOverride: ViewPoint<XY> = remember(initialViewPoint, initialRectangle) {
|
val featureDrag: DragHandle<XY> = DragHandle.withPrimaryButton { event, start, end ->
|
||||||
initialViewPoint
|
|
||||||
?: initialRectangle?.computeViewPoint()
|
|
||||||
?: featureState.features.values.computeBoundingBox(XYCoordinateSpace, 1f)?.computeViewPoint()
|
|
||||||
?: XYViewPoint(XY(0f, 0f), 1f)
|
|
||||||
}
|
|
||||||
|
|
||||||
val featureDrag: DragHandle<XY> =
|
|
||||||
DragHandle.withPrimaryButton { event, start: ViewPoint<XY>, end: ViewPoint<XY> ->
|
|
||||||
featureState.forEachWithAttribute(DraggableAttribute) { _, handle ->
|
featureState.forEachWithAttribute(DraggableAttribute) { _, handle ->
|
||||||
//TODO add safety
|
@Suppress("UNCHECKED_CAST")
|
||||||
(handle as DragHandle<XY>)
|
(handle as DragHandle<XY>)
|
||||||
.handle(event, start, end)
|
.handle(event, start, end)
|
||||||
.takeIf { !it.handleNext }
|
.takeIf { !it.handleNext }
|
||||||
?.let { return@withPrimaryButton it }
|
?.let {
|
||||||
|
//we expect it already have no bypass
|
||||||
|
return@withPrimaryButton it
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
//bypass
|
||||||
DragResult(end)
|
DragResult(end)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
val featureClick: ClickHandle<XY> = ClickHandle.withPrimaryButton { event, click ->
|
||||||
|
featureState.forEachWithAttribute(SelectableAttribute) { _, handle ->
|
||||||
|
@Suppress("UNCHECKED_CAST")
|
||||||
|
(handle as ClickHandle<XY>).handle(event, click)
|
||||||
|
config.onClick?.handle(event, click)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
val newConfig = config.copy(
|
val newConfig = config.copy(
|
||||||
dragHandle = DragHandle.combine(featureDrag, config.dragHandle)
|
dragHandle = config.dragHandle?.let { DragHandle.combine(featureDrag, it) } ?: featureDrag,
|
||||||
|
onClick = featureClick
|
||||||
)
|
)
|
||||||
|
|
||||||
SchemeView(
|
SchemeView(mapState, featureState, modifier)
|
||||||
initialViewPoint = viewPointOverride,
|
|
||||||
featuresState = featureState,
|
|
||||||
config = newConfig,
|
|
||||||
modifier = modifier,
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
///**
|
///**
|
||||||
|
Loading…
Reference in New Issue
Block a user