State encapsulation #14
@ -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
|
||||||
|
|
||||||
|
@ -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
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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,
|
@ -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,
|
||||||
)
|
)
|
||||||
}
|
}
|
@ -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)
|
||||||
}
|
}
|
||||||
|
@ -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()
|
||||||
|
}
|
@ -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()
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user