diff --git a/demo/maps/src/jvmMain/kotlin/AngleConversion.kt b/demo/maps/src/jvmMain/kotlin/AngleConversion.kt index a7ec5e5..d6c972a 100644 --- a/demo/maps/src/jvmMain/kotlin/AngleConversion.kt +++ b/demo/maps/src/jvmMain/kotlin/AngleConversion.kt @@ -2,3 +2,5 @@ import kotlin.math.PI fun Double.toDegrees() = this * 180 / PI +fun Double.toRadians() = this * PI / 180 + diff --git a/demo/maps/src/jvmMain/kotlin/Main.kt b/demo/maps/src/jvmMain/kotlin/Main.kt index cd24e90..e4f7955 100644 --- a/demo/maps/src/jvmMain/kotlin/Main.kt +++ b/demo/maps/src/jvmMain/kotlin/Main.kt @@ -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 ) } } diff --git a/maps-kt-compose/src/jvmMain/kotlin/center/sciprog/maps/compose/MapTextFeature.kt b/maps-kt-compose/src/commonMain/kotlin/center/sciprog/maps/compose/MapTextFeature.kt similarity index 84% rename from maps-kt-compose/src/jvmMain/kotlin/center/sciprog/maps/compose/MapTextFeature.kt rename to maps-kt-compose/src/commonMain/kotlin/center/sciprog/maps/compose/MapTextFeature.kt index fef40fb..9e4a187 100644 --- a/maps-kt-compose/src/jvmMain/kotlin/center/sciprog/maps/compose/MapTextFeature.kt +++ b/maps-kt-compose/src/commonMain/kotlin/center/sciprog/maps/compose/MapTextFeature.kt @@ -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, diff --git a/maps-kt-compose/src/commonMain/kotlin/center/sciprog/maps/compose/MapView.kt b/maps-kt-compose/src/commonMain/kotlin/center/sciprog/maps/compose/MapView.kt index b68cfc5..e97169d 100644 --- a/maps-kt-compose/src/commonMain/kotlin/center/sciprog/maps/compose/MapView.kt +++ b/maps-kt-compose/src/commonMain/kotlin/center/sciprog/maps/compose/MapView.kt @@ -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, ) } \ No newline at end of file diff --git a/maps-kt-compose/src/commonMain/kotlin/center/sciprog/maps/compose/MapViewState.kt b/maps-kt-compose/src/commonMain/kotlin/center/sciprog/maps/compose/MapViewState.kt index ec5b8ad..d5622a4 100644 --- a/maps-kt-compose/src/commonMain/kotlin/center/sciprog/maps/compose/MapViewState.kt +++ b/maps-kt-compose/src/commonMain/kotlin/center/sciprog/maps/compose/MapViewState.kt @@ -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 = 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 = 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) } diff --git a/maps-kt-compose/src/jvmMain/kotlin/center/sciprog/maps/compose/MapTextFeatureJvm.kt b/maps-kt-compose/src/jvmMain/kotlin/center/sciprog/maps/compose/MapTextFeatureJvm.kt new file mode 100644 index 0000000..b303332 --- /dev/null +++ b/maps-kt-compose/src/jvmMain/kotlin/center/sciprog/maps/compose/MapTextFeatureJvm.kt @@ -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() +} \ No newline at end of file diff --git a/maps-kt-compose/src/jvmMain/kotlin/center/sciprog/maps/compose/MapViewJvm.kt b/maps-kt-compose/src/jvmMain/kotlin/center/sciprog/maps/compose/MapViewJvm.kt index e9cc354..74dc4f5 100644 --- a/maps-kt-compose/src/jvmMain/kotlin/center/sciprog/maps/compose/MapViewJvm.kt +++ b/maps-kt-compose/src/jvmMain/kotlin/center/sciprog/maps/compose/MapViewJvm.kt @@ -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()