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.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.runtime.*
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.geometry.Size
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.PointMode
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.GmcBox
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
@ -54,24 +50,11 @@ fun App() {
val pointOne = 55.568548 to 37.568604
var pointTwo by remember { mutableStateOf(55.929444 to 37.518434) }
val pointThree = 60.929444 to 37.518434
val state = MapViewState(
mapTileProvider = mapTileProvider,
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
}
)
initialViewPoint = { viewPoint },
) {
image(pointOne, Icons.Filled.Home)
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(
mapViewState = state
mapViewState = state,
mapViewConfig = config
)
}
}

View File

@ -1,10 +1,15 @@
package center.sciprog.maps.compose
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.NativeCanvas
import center.sciprog.maps.coordinates.GeodeticMapCoordinates
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 val position: GeodeticMapCoordinates,

View File

@ -3,6 +3,7 @@ package center.sciprog.maps.compose
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.Density
import androidx.compose.ui.unit.DpSize
import center.sciprog.maps.coordinates.*
import kotlin.math.PI
@ -17,9 +18,8 @@ import kotlin.math.min
*/
public data class MapViewConfig(
val zoomSpeed: Double = 1.0 / 3.0,
val inferViewBoxFromFeatures: Boolean = false,
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 onSelect: (GmcBox) -> Unit = {},
val zoomOnSelect: Boolean = true,
@ -28,8 +28,9 @@ public data class MapViewConfig(
@Composable
public expect fun MapView(
mapViewState: MapViewState,
modifier: Modifier = Modifier,
mapViewState: MapViewState,
mapViewConfig: MapViewConfig,
)
@Composable
@ -46,10 +47,10 @@ public fun MapView(
MapView(
mapViewState = MapViewState(
mapTileProvider = mapTileProvider,
computeViewPoint = { initialViewPoint },
initialViewPoint = { initialViewPoint },
features = featuresBuilder.build(),
config = config,
),
mapViewConfig = config,
modifier = modifier
)
}
@ -78,11 +79,11 @@ public fun MapView(
featuresBuilder.buildFeatures()
MapView(
mapViewState = MapViewState(
config = config,
mapTileProvider = mapTileProvider,
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.dp
import center.sciprog.maps.coordinates.*
import center.sciprog.maps.coordinates.MercatorProjection.Companion.toMercator
import mu.KotlinLogging
import kotlin.math.*
@Composable
public fun MapViewState(
computeViewPoint: (canvasSize: DpSize) -> MapViewPoint,
initialViewPoint: (canvasSize: DpSize) -> MapViewPoint,
mapTileProvider: MapTileProvider,
config: MapViewConfig = MapViewConfig(),
features: Map<FeatureId, MapFeature> = emptyMap(),
inferViewBoxFromFeatures: Boolean = false,
buildFeatures: @Composable (MapFeatureBuilder.() -> Unit) = {},
): MapViewState {
val featuresBuilder = MapFeatureBuilderImpl(features)
featuresBuilder.buildFeatures()
return MapViewState(
computeViewPoint = computeViewPoint,
initialViewPoint = initialViewPoint,
mapTileProvider = mapTileProvider,
config = config,
features = featuresBuilder.build()
features = featuresBuilder.build(),
inferViewBoxFromFeatures = inferViewBoxFromFeatures
)
}
public class MapViewState(
public val computeViewPoint: (canvasSize: DpSize) -> MapViewPoint,
public val initialViewPoint: (canvasSize: DpSize) -> MapViewPoint,
public val mapTileProvider: MapTileProvider,
public val config: MapViewConfig = MapViewConfig(),
public val features: Map<FeatureId, MapFeature> = emptyMap(),
inferViewBoxFromFeatures: Boolean = false,
) {
public var canvasSize: DpSize by mutableStateOf(DpSize(512.dp, 512.dp))
public var viewPointInternal: MapViewPoint? by mutableStateOf(null)
public val viewPoint: MapViewPoint by derivedStateOf {
viewPointInternal ?: if (config.inferViewBoxFromFeatures) {
viewPointInternal ?: if (inferViewBoxFromFeatures) {
features.values.computeBoundingBox(1)?.let { box ->
val zoom = log2(
min(
@ -54,9 +54,9 @@ public class MapViewState(
) * PI / mapTileProvider.tileSize
)
MapViewPoint(box.center, zoom)
} ?: computeViewPoint(canvasSize)
} ?: initialViewPoint(canvasSize)
} else {
computeViewPoint(canvasSize)
initialViewPoint(canvasSize)
}
}
public val zoom: Int by derivedStateOf { floor(viewPoint.zoom).toInt() }
@ -101,6 +101,8 @@ public class MapViewState(
public fun GeodeticMapCoordinates.toOffset(density: Density): Offset =
WebMercatorProjection.toMercator(this, zoom).toOffset(density)
private val logger = KotlinLogging.logger("MapViewState")
public fun DrawScope.drawFeature(zoom: Int, feature: MapFeature) {
when (feature) {
is MapFeatureSelector -> drawFeature(zoom, feature.selector(zoom))
@ -147,17 +149,17 @@ public class MapViewState(
}
}
is MapTextFeature -> drawIntoCanvas { canvas ->
val offset = toOffset(feature.position, mapViewState)
val offset = feature.position.toOffset(this@drawFeature)
canvas.nativeCanvas.drawString(
feature.text,
offset.x + 5,
offset.y - 5,
Font().apply(feature.fontConfig),
feature.color.toPaint()
feature.color
)
}
is MapDrawFeature -> {
val offset = toOffset(feature.position, mapViewState)
val offset = feature.position.toOffset(this)
translate(offset.x, offset.y) {
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.launch
import mu.KotlinLogging
import org.jetbrains.skia.Font
import org.jetbrains.skia.Paint
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)
@ -48,8 +43,9 @@ private val logger = KotlinLogging.logger("MapView")
@Composable
public actual fun MapView(
mapViewState: MapViewState,
modifier: Modifier,
mapViewState: MapViewState,
mapViewConfig: MapViewConfig,
) {
with(mapViewState) {
@OptIn(ExperimentalComposeUiApi::class)
@ -82,11 +78,11 @@ public actual fun MapView(
rect.bottomRight.toDpOffset().toGeodetic()
)
config.onSelect(gmcBox)
if (config.zoomOnSelect) {
mapViewConfig.onSelect(gmcBox)
if (mapViewConfig.zoomOnSelect) {
val newViewPoint = gmcBox.computeViewPoint(mapTileProvider).invoke(canvasSize)
config.onViewChange(newViewPoint)
mapViewConfig.onViewChange(newViewPoint)
viewPointInternal = newViewPoint
}
selectRect = null
@ -94,7 +90,7 @@ public actual fun MapView(
} else {
val dragStart = change.position
val dpPos = DpOffset(dragStart.x.toDp(), dragStart.y.toDp())
config.onClick(
mapViewConfig.onClick(
MapViewPoint(
dpPos.toGeodetic() ,
viewPoint.zoom
@ -108,7 +104,8 @@ public actual fun MapView(
dragChange.previousPosition.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(dpEnd.toGeodetic(), viewPoint.zoom)
)
@ -117,7 +114,7 @@ public actual fun MapView(
-dragAmount.x.toDp().value / tileScale,
+dragAmount.y.toDp().value / tileScale
)
config.onViewChange(newViewPoint)
mapViewConfig.onViewChange(newViewPoint)
viewPointInternal = newViewPoint
}
}
@ -130,8 +127,8 @@ 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)
val newViewPoint = viewPoint.zoom(-change.scrollDelta.y.toDouble() * mapViewConfig.zoomSpeed, invariant)
mapViewConfig.onViewChange(newViewPoint)
viewPointInternal = newViewPoint
}.fillMaxSize()