State encapsulation #14
4
demo/maps/src/jvmMain/kotlin/AngleConversion.kt
Normal file
4
demo/maps/src/jvmMain/kotlin/AngleConversion.kt
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
import kotlin.math.PI
|
||||||
|
|
||||||
|
fun Double.toDegrees() = this * 180 / PI
|
||||||
|
|
@ -7,6 +7,7 @@ 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.geometry.Size
|
||||||
import androidx.compose.ui.graphics.Color
|
import androidx.compose.ui.graphics.Color
|
||||||
|
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.*
|
||||||
@ -49,41 +50,42 @@ fun App() {
|
|||||||
|
|
||||||
var centerCoordinates by remember { mutableStateOf<GeodeticMapCoordinates?>(null) }
|
var centerCoordinates by remember { mutableStateOf<GeodeticMapCoordinates?>(null) }
|
||||||
|
|
||||||
// val markers = (1..1_000_000).map {
|
|
||||||
// val position = GeodeticMapCoordinates.ofDegrees(
|
val pointOne = 55.568548 to 37.568604
|
||||||
// latitude = Random.nextDouble(-90.0, 90.0),
|
var pointTwo by remember { mutableStateOf(55.929444 to 37.518434) }
|
||||||
// longitude = Random.nextDouble(0.0, 180.0)
|
val pointThree = 60.929444 to 37.518434
|
||||||
// )
|
|
||||||
// MapDrawFeature(
|
|
||||||
// position = position,
|
|
||||||
// computeBoundingBox = {
|
|
||||||
// GmcBox.withCenter(
|
|
||||||
// center = position,
|
|
||||||
// width = Distance(0.001),
|
|
||||||
// height = Distance(0.001)
|
|
||||||
// )
|
|
||||||
// }
|
|
||||||
// ) {
|
|
||||||
// drawRoundRect(
|
|
||||||
// color = Color.Yellow,
|
|
||||||
// size = Size(1f, 1f)
|
|
||||||
// )
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
val state = MapViewState(
|
val state = MapViewState(
|
||||||
mapTileProvider = mapTileProvider,
|
mapTileProvider = mapTileProvider,
|
||||||
computeViewPoint = { viewPoint },
|
initialViewPoint = viewPoint,
|
||||||
config = MapViewConfig(
|
config = MapViewConfig(
|
||||||
inferViewBoxFromFeatures = true,
|
inferViewBoxFromFeatures = true,
|
||||||
onViewChange = { centerCoordinates = focus },
|
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
|
||||||
|
}
|
||||||
)
|
)
|
||||||
) {
|
) {
|
||||||
val pointOne = 55.568548 to 37.568604
|
|
||||||
val pointTwo = 55.929444 to 37.518434
|
|
||||||
val pointThree = 60.929444 to 37.518434
|
|
||||||
|
|
||||||
image(pointOne, Icons.Filled.Home)
|
image(pointOne, Icons.Filled.Home)
|
||||||
|
|
||||||
|
points(
|
||||||
|
points = listOf(
|
||||||
|
55.742465 to 37.615812,
|
||||||
|
55.742713 to 37.616370,
|
||||||
|
55.742815 to 37.616659,
|
||||||
|
55.742320 to 37.617132,
|
||||||
|
55.742086 to 37.616566,
|
||||||
|
55.741715 to 37.616716
|
||||||
|
),
|
||||||
|
pointMode = PointMode.Polygon
|
||||||
|
)
|
||||||
|
|
||||||
//remember feature Id
|
//remember feature Id
|
||||||
val circleId: FeatureId = circle(
|
val circleId: FeatureId = circle(
|
||||||
centerCoordinates = pointTwo,
|
centerCoordinates = pointTwo,
|
||||||
@ -106,15 +108,9 @@ fun App() {
|
|||||||
drawLine(start = Offset(-10f, 10f), end = Offset(10f, -10f), color = Color.Red)
|
drawLine(start = Offset(-10f, 10f), end = Offset(10f, -10f), color = Color.Red)
|
||||||
}
|
}
|
||||||
|
|
||||||
// markers.forEach { feature ->
|
|
||||||
// featureSelector {
|
|
||||||
// feature
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
|
|
||||||
arc(pointOne, Distance(10.0), 0f, PI)
|
arc(pointOne, Distance(10.0), 0f, PI)
|
||||||
|
|
||||||
line(pointOne, pointTwo)
|
line(pointOne, pointTwo, id = "line")
|
||||||
text(pointOne, "Home", font = { size = 32f })
|
text(pointOne, "Home", font = { size = 32f })
|
||||||
|
|
||||||
centerCoordinates?.let {
|
centerCoordinates?.let {
|
||||||
|
@ -3,6 +3,7 @@ package center.sciprog.maps.compose
|
|||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.ui.graphics.Color
|
import androidx.compose.ui.graphics.Color
|
||||||
import androidx.compose.ui.graphics.ImageBitmap
|
import androidx.compose.ui.graphics.ImageBitmap
|
||||||
|
import androidx.compose.ui.graphics.PointMode
|
||||||
import androidx.compose.ui.graphics.drawscope.DrawScope
|
import androidx.compose.ui.graphics.drawscope.DrawScope
|
||||||
import androidx.compose.ui.graphics.painter.Painter
|
import androidx.compose.ui.graphics.painter.Painter
|
||||||
import androidx.compose.ui.graphics.vector.ImageVector
|
import androidx.compose.ui.graphics.vector.ImageVector
|
||||||
@ -46,6 +47,18 @@ public class MapDrawFeature(
|
|||||||
override fun getBoundingBox(zoom: Int): GmcBox = computeBoundingBox(zoom)
|
override fun getBoundingBox(zoom: Int): GmcBox = computeBoundingBox(zoom)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public class MapPointsFeature(
|
||||||
|
public val points: List<GeodeticMapCoordinates>,
|
||||||
|
override val zoomRange: IntRange = defaultZoomRange,
|
||||||
|
public val stroke: Float = 2f,
|
||||||
|
public val color: Color = Color.Red,
|
||||||
|
public val pointMode: PointMode = PointMode.Points
|
||||||
|
) : MapFeature {
|
||||||
|
override fun getBoundingBox(zoom: Int): GmcBox {
|
||||||
|
return GmcBox(points.first(), points.last())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public class MapCircleFeature(
|
public class MapCircleFeature(
|
||||||
public val center: GeodeticMapCoordinates,
|
public val center: GeodeticMapCoordinates,
|
||||||
override val zoomRange: IntRange = defaultZoomRange,
|
override val zoomRange: IntRange = defaultZoomRange,
|
||||||
|
@ -4,6 +4,7 @@ import androidx.compose.runtime.Composable
|
|||||||
import androidx.compose.runtime.mutableStateMapOf
|
import androidx.compose.runtime.mutableStateMapOf
|
||||||
import androidx.compose.runtime.snapshots.SnapshotStateMap
|
import androidx.compose.runtime.snapshots.SnapshotStateMap
|
||||||
import androidx.compose.ui.graphics.Color
|
import androidx.compose.ui.graphics.Color
|
||||||
|
import androidx.compose.ui.graphics.PointMode
|
||||||
import androidx.compose.ui.graphics.drawscope.DrawScope
|
import androidx.compose.ui.graphics.drawscope.DrawScope
|
||||||
import androidx.compose.ui.graphics.vector.ImageVector
|
import androidx.compose.ui.graphics.vector.ImageVector
|
||||||
import androidx.compose.ui.unit.DpSize
|
import androidx.compose.ui.unit.DpSize
|
||||||
@ -117,6 +118,15 @@ public fun MapFeatureBuilder.arc(
|
|||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
public fun MapFeatureBuilder.points(
|
||||||
|
points: List<Pair<Double, Double>>,
|
||||||
|
zoomRange: IntRange = defaultZoomRange,
|
||||||
|
stroke: Float = 2f,
|
||||||
|
color: Color = Color.Red,
|
||||||
|
pointMode: PointMode = PointMode.Points,
|
||||||
|
id: FeatureId? = null
|
||||||
|
): FeatureId = addFeature(id, MapPointsFeature(points.map { it.toCoordinates() }, zoomRange, stroke, color, pointMode))
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
public fun MapFeatureBuilder.image(
|
public fun MapFeatureBuilder.image(
|
||||||
position: Pair<Double, Double>,
|
position: Pair<Double, Double>,
|
||||||
|
@ -11,10 +11,15 @@ import kotlin.math.min
|
|||||||
|
|
||||||
|
|
||||||
//TODO consider replacing by modifier
|
//TODO consider replacing by modifier
|
||||||
|
/**
|
||||||
|
* @param onDrag - returns true if you want to drag a map and false, if you want to make map stationary.
|
||||||
|
* start - is a point where drag begins, end is a point where drag ends
|
||||||
|
*/
|
||||||
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 inferViewBoxFromFeatures: Boolean = false,
|
||||||
val onClick: MapViewPoint.() -> Unit = {},
|
val onClick: MapViewPoint.() -> Unit = {},
|
||||||
|
val onDrag: (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,
|
||||||
|
@ -51,6 +51,7 @@ public actual fun MapView(
|
|||||||
mapViewState: MapViewState,
|
mapViewState: MapViewState,
|
||||||
modifier: Modifier,
|
modifier: Modifier,
|
||||||
) {
|
) {
|
||||||
|
with(mapViewState) {
|
||||||
@OptIn(ExperimentalComposeUiApi::class)
|
@OptIn(ExperimentalComposeUiApi::class)
|
||||||
val canvasModifier = modifier.pointerInput(Unit) {
|
val canvasModifier = modifier.pointerInput(Unit) {
|
||||||
forEachGesture {
|
forEachGesture {
|
||||||
@ -62,11 +63,11 @@ public actual fun MapView(
|
|||||||
if (event.buttons.isPrimaryPressed) {
|
if (event.buttons.isPrimaryPressed) {
|
||||||
//Evaluating selection frame
|
//Evaluating selection frame
|
||||||
if (event.keyboardModifiers.isShiftPressed) {
|
if (event.keyboardModifiers.isShiftPressed) {
|
||||||
mapViewState.selectRect = Rect(change.position, change.position)
|
selectRect = Rect(change.position, change.position)
|
||||||
drag(change.id) { dragChange ->
|
drag(change.id) { dragChange ->
|
||||||
mapViewState.selectRect?.let { rect ->
|
selectRect?.let { rect ->
|
||||||
val offset = dragChange.position
|
val offset = dragChange.position
|
||||||
mapViewState.selectRect = Rect(
|
selectRect = Rect(
|
||||||
min(offset.x, rect.left),
|
min(offset.x, rect.left),
|
||||||
min(offset.y, rect.top),
|
min(offset.y, rect.top),
|
||||||
max(offset.x, rect.right),
|
max(offset.x, rect.right),
|
||||||
@ -74,41 +75,50 @@ public actual fun MapView(
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
mapViewState.selectRect?.let { rect ->
|
selectRect?.let { rect ->
|
||||||
//Use selection override if it is defined
|
//Use selection override if it is defined
|
||||||
val gmcBox = with(mapViewState) {
|
val gmcBox = GmcBox(
|
||||||
GmcBox(
|
|
||||||
rect.topLeft.toDpOffset().toGeodetic(),
|
rect.topLeft.toDpOffset().toGeodetic(),
|
||||||
rect.bottomRight.toDpOffset().toGeodetic()
|
rect.bottomRight.toDpOffset().toGeodetic()
|
||||||
)
|
)
|
||||||
}
|
|
||||||
mapViewState.config.onSelect(gmcBox)
|
|
||||||
if (mapViewState.config.zoomOnSelect) {
|
|
||||||
val newViewPoint = gmcBox.computeViewPoint(mapViewState.mapTileProvider)
|
|
||||||
.invoke(mapViewState.canvasSize)
|
|
||||||
|
|
||||||
mapViewState.config.onViewChange(newViewPoint)
|
config.onSelect(gmcBox)
|
||||||
mapViewState.viewPointInternal = newViewPoint
|
if (config.zoomOnSelect) {
|
||||||
|
val newViewPoint = gmcBox.computeViewPoint(mapTileProvider).invoke(canvasSize)
|
||||||
|
|
||||||
|
config.onViewChange(newViewPoint)
|
||||||
|
viewPointInternal = newViewPoint
|
||||||
}
|
}
|
||||||
mapViewState.selectRect = null
|
selectRect = null
|
||||||
}
|
}
|
||||||
} 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())
|
||||||
mapViewState.config.onClick(
|
config.onClick(
|
||||||
MapViewPoint(
|
MapViewPoint(
|
||||||
with(mapViewState) { dpPos.toGeodetic() },
|
dpPos.toGeodetic() ,
|
||||||
mapViewState.viewPoint.zoom
|
viewPoint.zoom
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
drag(change.id) { dragChange ->
|
drag(change.id) { dragChange ->
|
||||||
val dragAmount = dragChange.position - dragChange.previousPosition
|
val dragAmount = dragChange.position - dragChange.previousPosition
|
||||||
val newViewPoint = mapViewState.viewPoint.move(
|
val dpStart =
|
||||||
-dragAmount.x.toDp().value / mapViewState.tileScale,
|
DpOffset(
|
||||||
+dragAmount.y.toDp().value / mapViewState.tileScale
|
dragChange.previousPosition.x.toDp(),
|
||||||
|
dragChange.previousPosition.y.toDp()
|
||||||
)
|
)
|
||||||
mapViewState.config.onViewChange(newViewPoint)
|
val dpEnd = DpOffset(dragChange.position.x.toDp(), dragChange.position.y.toDp())
|
||||||
mapViewState.viewPointInternal = newViewPoint
|
if (!config.onDrag(
|
||||||
|
MapViewPoint(dpStart.toGeodetic(), viewPoint.zoom),
|
||||||
|
MapViewPoint(dpEnd.toGeodetic(), viewPoint.zoom)
|
||||||
|
)
|
||||||
|
) return@drag
|
||||||
|
val newViewPoint = viewPoint.move(
|
||||||
|
-dragAmount.x.toDp().value / tileScale,
|
||||||
|
+dragAmount.y.toDp().value / tileScale
|
||||||
|
)
|
||||||
|
config.onViewChange(newViewPoint)
|
||||||
|
viewPointInternal = newViewPoint
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -119,42 +129,37 @@ public actual fun MapView(
|
|||||||
val change = it.changes.first()
|
val change = it.changes.first()
|
||||||
val (xPos, yPos) = change.position
|
val (xPos, yPos) = change.position
|
||||||
//compute invariant point of translation
|
//compute invariant point of translation
|
||||||
val invariant = with(mapViewState) { DpOffset(xPos.toDp(), yPos.toDp()).toGeodetic() }
|
val invariant = DpOffset(xPos.toDp(), yPos.toDp()).toGeodetic()
|
||||||
val newViewPoint =
|
val newViewPoint = viewPoint.zoom(-change.scrollDelta.y.toDouble() * config.zoomSpeed, invariant)
|
||||||
mapViewState.viewPoint.zoom(-change.scrollDelta.y.toDouble() * mapViewState.config.zoomSpeed, invariant)
|
config.onViewChange(newViewPoint)
|
||||||
mapViewState.config.onViewChange(newViewPoint)
|
viewPointInternal = newViewPoint
|
||||||
mapViewState.viewPointInternal = newViewPoint
|
|
||||||
}.fillMaxSize()
|
}.fillMaxSize()
|
||||||
|
|
||||||
|
|
||||||
// Load tiles asynchronously
|
// Load tiles asynchronously
|
||||||
LaunchedEffect(mapViewState.viewPoint, mapViewState.canvasSize) {
|
LaunchedEffect(viewPoint, canvasSize) {
|
||||||
with(mapViewState.mapTileProvider) {
|
with(mapTileProvider) {
|
||||||
val indexRange = 0 until 2.0.pow(mapViewState.zoom).toInt()
|
val indexRange = 0 until 2.0.pow(zoom).toInt()
|
||||||
|
|
||||||
val left =
|
val left = centerCoordinates.x - canvasSize.width.value / 2 / tileScale
|
||||||
mapViewState.centerCoordinates.x - mapViewState.canvasSize.width.value / 2 / mapViewState.tileScale
|
val right = centerCoordinates.x + canvasSize.width.value / 2 / tileScale
|
||||||
val right =
|
|
||||||
mapViewState.centerCoordinates.x + mapViewState.canvasSize.width.value / 2 / mapViewState.tileScale
|
|
||||||
val horizontalIndices: IntRange = (toIndex(left)..toIndex(right)).intersect(indexRange)
|
val horizontalIndices: IntRange = (toIndex(left)..toIndex(right)).intersect(indexRange)
|
||||||
|
|
||||||
val top =
|
val top = (centerCoordinates.y + canvasSize.height.value / 2 / tileScale)
|
||||||
(mapViewState.centerCoordinates.y + mapViewState.canvasSize.height.value / 2 / mapViewState.tileScale)
|
val bottom = (centerCoordinates.y - canvasSize.height.value / 2 / tileScale)
|
||||||
val bottom =
|
|
||||||
(mapViewState.centerCoordinates.y - mapViewState.canvasSize.height.value / 2 / mapViewState.tileScale)
|
|
||||||
val verticalIndices: IntRange = (toIndex(bottom)..toIndex(top)).intersect(indexRange)
|
val verticalIndices: IntRange = (toIndex(bottom)..toIndex(top)).intersect(indexRange)
|
||||||
|
|
||||||
mapViewState.mapTiles.clear()
|
mapTiles.clear()
|
||||||
|
|
||||||
for (j in verticalIndices) {
|
for (j in verticalIndices) {
|
||||||
for (i in horizontalIndices) {
|
for (i in horizontalIndices) {
|
||||||
val id = TileId(mapViewState.zoom, i, j)
|
val id = TileId(zoom, i, j)
|
||||||
//start all
|
//start all
|
||||||
val deferred = loadTileAsync(id)
|
val deferred = loadTileAsync(id)
|
||||||
//wait asynchronously for it to finish
|
//wait asynchronously for it to finish
|
||||||
launch {
|
launch {
|
||||||
try {
|
try {
|
||||||
mapViewState.mapTiles += deferred.await()
|
mapTiles += deferred.await()
|
||||||
} catch (ex: Exception) {
|
} catch (ex: Exception) {
|
||||||
if (ex !is CancellationException) {
|
if (ex !is CancellationException) {
|
||||||
//displaying the error is maps responsibility
|
//displaying the error is maps responsibility
|
||||||
@ -175,14 +180,14 @@ public actual fun MapView(
|
|||||||
}
|
}
|
||||||
clipRect {
|
clipRect {
|
||||||
val tileSize = IntSize(
|
val tileSize = IntSize(
|
||||||
ceil((mapViewState.mapTileProvider.tileSize.dp * mapViewState.tileScale.toFloat()).toPx()).toInt(),
|
ceil((mapTileProvider.tileSize.dp * tileScale.toFloat()).toPx()).toInt(),
|
||||||
ceil((mapViewState.mapTileProvider.tileSize.dp * mapViewState.tileScale.toFloat()).toPx()).toInt()
|
ceil((mapTileProvider.tileSize.dp * tileScale.toFloat()).toPx()).toInt()
|
||||||
)
|
)
|
||||||
mapViewState.mapTiles.forEach { (id, image) ->
|
mapTiles.forEach { (id, image) ->
|
||||||
//converting back from tile index to screen offset
|
//converting back from tile index to screen offset
|
||||||
val offset = IntOffset(
|
val offset = IntOffset(
|
||||||
(mapViewState.canvasSize.width / 2 + (mapViewState.mapTileProvider.toCoordinate(id.i).dp - mapViewState.centerCoordinates.x.dp) * mapViewState.tileScale.toFloat()).roundToPx(),
|
(canvasSize.width / 2 + (mapTileProvider.toCoordinate(id.i).dp - centerCoordinates.x.dp) * tileScale.toFloat()).roundToPx(),
|
||||||
(mapViewState.canvasSize.height / 2 + (mapViewState.mapTileProvider.toCoordinate(id.j).dp - mapViewState.centerCoordinates.y.dp) * mapViewState.tileScale.toFloat()).roundToPx()
|
(canvasSize.height / 2 + (mapTileProvider.toCoordinate(id.j).dp - centerCoordinates.y.dp) * tileScale.toFloat()).roundToPx()
|
||||||
)
|
)
|
||||||
drawImage(
|
drawImage(
|
||||||
image = image,
|
image = image,
|
||||||
@ -190,11 +195,11 @@ public actual fun MapView(
|
|||||||
dstSize = tileSize
|
dstSize = tileSize
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
mapViewState.features.values.filter { mapViewState.zoom in it.zoomRange }.forEach { feature ->
|
features.values.filter { zoom in it.zoomRange }.forEach { feature ->
|
||||||
drawFeature(mapViewState.zoom, feature)
|
drawFeature(zoom, feature)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
mapViewState.selectRect?.let { rect ->
|
selectRect?.let { rect ->
|
||||||
drawRect(
|
drawRect(
|
||||||
color = Color.Blue,
|
color = Color.Blue,
|
||||||
topLeft = rect.topLeft,
|
topLeft = rect.topLeft,
|
||||||
@ -207,4 +212,5 @@ public actual fun MapView(
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
}
|
}
|
Loading…
Reference in New Issue
Block a user