State encapsulation #14

Open
ArystanK wants to merge 4 commits from state_encapsulation into main
7 changed files with 92 additions and 59 deletions
Showing only changes of commit 516d8d0233 - Show all commits

View File

@ -2,3 +2,5 @@ import kotlin.math.PI
fun Double.toDegrees() = this * 180 / PI fun Double.toDegrees() = this * 180 / PI
fun Double.toRadians() = this * PI / 180

View File

@ -5,16 +5,12 @@ 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.*
import androidx.compose.ui.geometry.Offset import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.geometry.Size
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.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.GmcBox
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
@ -54,24 +50,11 @@ fun App() {
val pointOne = 55.568548 to 37.568604 val pointOne = 55.568548 to 37.568604
var pointTwo by remember { mutableStateOf(55.929444 to 37.518434) } var pointTwo by remember { mutableStateOf(55.929444 to 37.518434) }
val pointThree = 60.929444 to 37.518434 val pointThree = 60.929444 to 37.518434
val state = MapViewState( val state = MapViewState(
mapTileProvider = mapTileProvider, mapTileProvider = mapTileProvider,
initialViewPoint = viewPoint, initialViewPoint = { viewPoint },
config = MapViewConfig(
inferViewBoxFromFeatures = true,
onViewChange = { centerCoordinates = focus },
onDrag = { start, end ->
if (start.focus.latitude.toDegrees() in (pointTwo.first - 0.05)..(pointTwo.first + 0.05) &&
start.focus.longitude.toDegrees() in (pointTwo.second - 0.05)..(pointTwo.second + 0.05)
) { ) {
pointTwo = pointTwo.first + (end.focus.latitude - start.focus.latitude).toDegrees() to
pointTwo.second + (end.focus.longitude - start.focus.longitude).toDegrees()
false// returning false, because when we are dragging circle we don't want to drag map
} else true
}
)
) {
image(pointOne, Icons.Filled.Home) image(pointOne, Icons.Filled.Home)
points( points(
@ -132,8 +115,28 @@ fun App() {
} }
} }
} }
val config = MapViewConfig(
onViewChange = { centerCoordinates = focus },
onDrag = { start, end ->
val markerRadius = 5f
val startPosition = with(state) { start.focus.toOffset(this@MapViewConfig) }
val markerLocation = with(state) {
GeodeticMapCoordinates.ofDegrees(pointTwo.first, pointTwo.second).toOffset(this@MapViewConfig)
}
if (startPosition.x in (markerLocation.x - markerRadius)..(markerLocation.x + markerRadius) &&
startPosition.y in (markerLocation.y - markerRadius)..(markerLocation.y + markerRadius)
) {
pointTwo = pointTwo.first + (end.focus.latitude - start.focus.latitude).toDegrees() to
pointTwo.second + (end.focus.longitude - start.focus.longitude).toDegrees()
false// returning false, because when we are dragging circle we don't want to drag map
} else true
}
)
MapView( MapView(
mapViewState = state mapViewState = state,
mapViewConfig = config
) )
} }
} }

View File

@ -1,10 +1,15 @@
package center.sciprog.maps.compose package center.sciprog.maps.compose
import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.NativeCanvas
import center.sciprog.maps.coordinates.GeodeticMapCoordinates import center.sciprog.maps.coordinates.GeodeticMapCoordinates
import center.sciprog.maps.coordinates.GmcBox import center.sciprog.maps.coordinates.GmcBox
import org.jetbrains.skia.Font
public expect class Font constructor() {
public var size: Float
}
public expect fun NativeCanvas.drawString(text: String, x: Float, y: Float, font: Font, color: Color)
public class MapTextFeature( public class MapTextFeature(
public val position: GeodeticMapCoordinates, public val position: GeodeticMapCoordinates,

View File

@ -3,6 +3,7 @@ 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.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.Density
import androidx.compose.ui.unit.DpSize import androidx.compose.ui.unit.DpSize
import center.sciprog.maps.coordinates.* import center.sciprog.maps.coordinates.*
import kotlin.math.PI import kotlin.math.PI
@ -17,9 +18,8 @@ import kotlin.math.min
*/ */
public data class MapViewConfig( public data class MapViewConfig(
val zoomSpeed: Double = 1.0 / 3.0, val zoomSpeed: Double = 1.0 / 3.0,
val inferViewBoxFromFeatures: Boolean = false,
val onClick: MapViewPoint.() -> Unit = {}, val onClick: MapViewPoint.() -> Unit = {},
val onDrag: (start: MapViewPoint, end: MapViewPoint) -> Boolean = { _, _ -> true }, val onDrag: Density.(start: MapViewPoint, end: MapViewPoint) -> Boolean = { _, _ -> true },
val onViewChange: MapViewPoint.() -> Unit = {}, val onViewChange: MapViewPoint.() -> Unit = {},
val onSelect: (GmcBox) -> Unit = {}, val onSelect: (GmcBox) -> Unit = {},
val zoomOnSelect: Boolean = true, val zoomOnSelect: Boolean = true,
@ -28,8 +28,9 @@ public data class MapViewConfig(
@Composable @Composable
public expect fun MapView( public expect fun MapView(
mapViewState: MapViewState,
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
mapViewState: MapViewState,
mapViewConfig: MapViewConfig,
) )
@Composable @Composable
@ -46,10 +47,10 @@ public fun MapView(
MapView( MapView(
mapViewState = MapViewState( mapViewState = MapViewState(
mapTileProvider = mapTileProvider, mapTileProvider = mapTileProvider,
computeViewPoint = { initialViewPoint }, initialViewPoint = { initialViewPoint },
features = featuresBuilder.build(), features = featuresBuilder.build(),
config = config,
), ),
mapViewConfig = config,
modifier = modifier modifier = modifier
) )
} }
@ -78,11 +79,11 @@ public fun MapView(
featuresBuilder.buildFeatures() featuresBuilder.buildFeatures()
MapView( MapView(
mapViewState = MapViewState( mapViewState = MapViewState(
config = config,
mapTileProvider = mapTileProvider, mapTileProvider = mapTileProvider,
features = featuresBuilder.build(), features = featuresBuilder.build(),
computeViewPoint = box.computeViewPoint(mapTileProvider), initialViewPoint = box.computeViewPoint(mapTileProvider),
), ),
modifier modifier = modifier,
mapViewConfig = config,
) )
} }

View File

@ -15,37 +15,37 @@ 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 androidx.compose.ui.unit.dp
import center.sciprog.maps.coordinates.* import center.sciprog.maps.coordinates.*
import center.sciprog.maps.coordinates.MercatorProjection.Companion.toMercator import mu.KotlinLogging
import kotlin.math.* import kotlin.math.*
@Composable @Composable
public fun MapViewState( public fun MapViewState(
computeViewPoint: (canvasSize: DpSize) -> MapViewPoint, initialViewPoint: (canvasSize: DpSize) -> MapViewPoint,
mapTileProvider: MapTileProvider, mapTileProvider: MapTileProvider,
config: MapViewConfig = MapViewConfig(),
features: Map<FeatureId, MapFeature> = emptyMap(), features: Map<FeatureId, MapFeature> = emptyMap(),
inferViewBoxFromFeatures: Boolean = false,
buildFeatures: @Composable (MapFeatureBuilder.() -> Unit) = {}, buildFeatures: @Composable (MapFeatureBuilder.() -> Unit) = {},
): MapViewState { ): MapViewState {
val featuresBuilder = MapFeatureBuilderImpl(features) val featuresBuilder = MapFeatureBuilderImpl(features)
featuresBuilder.buildFeatures() featuresBuilder.buildFeatures()
return MapViewState( return MapViewState(
computeViewPoint = computeViewPoint, initialViewPoint = initialViewPoint,
mapTileProvider = mapTileProvider, mapTileProvider = mapTileProvider,
config = config, features = featuresBuilder.build(),
features = featuresBuilder.build() inferViewBoxFromFeatures = inferViewBoxFromFeatures
) )
} }
public class MapViewState( public class MapViewState(
public val computeViewPoint: (canvasSize: DpSize) -> MapViewPoint, public val initialViewPoint: (canvasSize: DpSize) -> MapViewPoint,
public val mapTileProvider: MapTileProvider, public val mapTileProvider: MapTileProvider,
public val config: MapViewConfig = MapViewConfig(),
public val features: Map<FeatureId, MapFeature> = emptyMap(), public val features: Map<FeatureId, MapFeature> = emptyMap(),
inferViewBoxFromFeatures: Boolean = false,
) { ) {
public var canvasSize: DpSize by mutableStateOf(DpSize(512.dp, 512.dp)) public var canvasSize: DpSize by mutableStateOf(DpSize(512.dp, 512.dp))
public var viewPointInternal: MapViewPoint? by mutableStateOf(null) public var viewPointInternal: MapViewPoint? by mutableStateOf(null)
public val viewPoint: MapViewPoint by derivedStateOf { public val viewPoint: MapViewPoint by derivedStateOf {
viewPointInternal ?: if (config.inferViewBoxFromFeatures) { viewPointInternal ?: if (inferViewBoxFromFeatures) {
features.values.computeBoundingBox(1)?.let { box -> features.values.computeBoundingBox(1)?.let { box ->
val zoom = log2( val zoom = log2(
min( min(
@ -54,9 +54,9 @@ public class MapViewState(
) * PI / mapTileProvider.tileSize ) * PI / mapTileProvider.tileSize
) )
MapViewPoint(box.center, zoom) MapViewPoint(box.center, zoom)
} ?: computeViewPoint(canvasSize) } ?: initialViewPoint(canvasSize)
} else { } else {
computeViewPoint(canvasSize) initialViewPoint(canvasSize)
} }
} }
public val zoom: Int by derivedStateOf { floor(viewPoint.zoom).toInt() } public val zoom: Int by derivedStateOf { floor(viewPoint.zoom).toInt() }
@ -101,6 +101,8 @@ public class MapViewState(
public fun GeodeticMapCoordinates.toOffset(density: Density): Offset = public fun GeodeticMapCoordinates.toOffset(density: Density): Offset =
WebMercatorProjection.toMercator(this, zoom).toOffset(density) WebMercatorProjection.toMercator(this, zoom).toOffset(density)
private val logger = KotlinLogging.logger("MapViewState")
public fun DrawScope.drawFeature(zoom: Int, feature: MapFeature) { public fun DrawScope.drawFeature(zoom: Int, feature: MapFeature) {
when (feature) { when (feature) {
is MapFeatureSelector -> drawFeature(zoom, feature.selector(zoom)) is MapFeatureSelector -> drawFeature(zoom, feature.selector(zoom))
@ -147,17 +149,17 @@ public class MapViewState(
} }
} }
is MapTextFeature -> drawIntoCanvas { canvas -> is MapTextFeature -> drawIntoCanvas { canvas ->
val offset = toOffset(feature.position, mapViewState) val offset = feature.position.toOffset(this@drawFeature)
canvas.nativeCanvas.drawString( canvas.nativeCanvas.drawString(
feature.text, feature.text,
offset.x + 5, offset.x + 5,
offset.y - 5, offset.y - 5,
Font().apply(feature.fontConfig), Font().apply(feature.fontConfig),
feature.color.toPaint() feature.color
) )
} }
is MapDrawFeature -> { is MapDrawFeature -> {
val offset = toOffset(feature.position, mapViewState) val offset = feature.position.toOffset(this)
translate(offset.x, offset.y) { translate(offset.x, offset.y) {
feature.drawFeature(this) feature.drawFeature(this)
} }

View File

@ -0,0 +1,23 @@
package center.sciprog.maps.compose
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.NativeCanvas
import androidx.compose.ui.graphics.toArgb
import org.jetbrains.skia.Paint
public actual typealias Font = org.jetbrains.skia.Font
public actual fun NativeCanvas.drawString(
text: String,
x: Float,
y: Float,
font: Font,
color: Color
) {
drawString(text, x, y, font, color.toPaint())
}
private fun Color.toPaint(): Paint = Paint().apply {
isAntiAlias = true
color = toArgb()
}

View File

@ -17,15 +17,10 @@ import center.sciprog.maps.coordinates.*
import kotlinx.coroutines.CancellationException import kotlinx.coroutines.CancellationException
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import mu.KotlinLogging import mu.KotlinLogging
import org.jetbrains.skia.Font
import org.jetbrains.skia.Paint
import kotlin.math.* import kotlin.math.*
private fun Color.toPaint(): Paint = Paint().apply {
isAntiAlias = true
color = toArgb()
}
private fun IntRange.intersect(other: IntRange) = max(first, other.first)..min(last, other.last) private fun IntRange.intersect(other: IntRange) = max(first, other.first)..min(last, other.last)
@ -48,8 +43,9 @@ private val logger = KotlinLogging.logger("MapView")
@Composable @Composable
public actual fun MapView( public actual fun MapView(
mapViewState: MapViewState,
modifier: Modifier, modifier: Modifier,
mapViewState: MapViewState,
mapViewConfig: MapViewConfig,
) { ) {
with(mapViewState) { with(mapViewState) {
@OptIn(ExperimentalComposeUiApi::class) @OptIn(ExperimentalComposeUiApi::class)
@ -82,11 +78,11 @@ public actual fun MapView(
rect.bottomRight.toDpOffset().toGeodetic() rect.bottomRight.toDpOffset().toGeodetic()
) )
config.onSelect(gmcBox) mapViewConfig.onSelect(gmcBox)
if (config.zoomOnSelect) { if (mapViewConfig.zoomOnSelect) {
val newViewPoint = gmcBox.computeViewPoint(mapTileProvider).invoke(canvasSize) val newViewPoint = gmcBox.computeViewPoint(mapTileProvider).invoke(canvasSize)
config.onViewChange(newViewPoint) mapViewConfig.onViewChange(newViewPoint)
viewPointInternal = newViewPoint viewPointInternal = newViewPoint
} }
selectRect = null selectRect = null
@ -94,7 +90,7 @@ public actual fun MapView(
} 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())
config.onClick( mapViewConfig.onClick(
MapViewPoint( MapViewPoint(
dpPos.toGeodetic() , dpPos.toGeodetic() ,
viewPoint.zoom viewPoint.zoom
@ -108,7 +104,8 @@ public actual fun MapView(
dragChange.previousPosition.y.toDp() dragChange.previousPosition.y.toDp()
) )
val dpEnd = DpOffset(dragChange.position.x.toDp(), dragChange.position.y.toDp()) val dpEnd = DpOffset(dragChange.position.x.toDp(), dragChange.position.y.toDp())
if (!config.onDrag( if (!mapViewConfig.onDrag(
this,
MapViewPoint(dpStart.toGeodetic(), viewPoint.zoom), MapViewPoint(dpStart.toGeodetic(), viewPoint.zoom),
MapViewPoint(dpEnd.toGeodetic(), viewPoint.zoom) MapViewPoint(dpEnd.toGeodetic(), viewPoint.zoom)
) )
@ -117,7 +114,7 @@ public actual fun MapView(
-dragAmount.x.toDp().value / tileScale, -dragAmount.x.toDp().value / tileScale,
+dragAmount.y.toDp().value / tileScale +dragAmount.y.toDp().value / tileScale
) )
config.onViewChange(newViewPoint) mapViewConfig.onViewChange(newViewPoint)
viewPointInternal = newViewPoint viewPointInternal = newViewPoint
} }
} }
@ -130,8 +127,8 @@ 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) val newViewPoint = viewPoint.zoom(-change.scrollDelta.y.toDouble() * mapViewConfig.zoomSpeed, invariant)
config.onViewChange(newViewPoint) mapViewConfig.onViewChange(newViewPoint)
viewPointInternal = newViewPoint viewPointInternal = newViewPoint
}.fillMaxSize() }.fillMaxSize()