diff --git a/src/jvmMain/kotlin/Main.kt b/src/jvmMain/kotlin/Main.kt index fed4798..4ea671e 100644 --- a/src/jvmMain/kotlin/Main.kt +++ b/src/jvmMain/kotlin/Main.kt @@ -4,9 +4,7 @@ import androidx.compose.material.MaterialTheme import androidx.compose.runtime.Composable import androidx.compose.ui.window.Window import androidx.compose.ui.window.application -import centre.sciprog.maps.compose.GeodeticMapCoordinates -import centre.sciprog.maps.compose.MapView -import centre.sciprog.maps.compose.MapViewPoint +import centre.sciprog.maps.compose.* import java.nio.file.Path @Composable @@ -17,7 +15,14 @@ fun App() { GeodeticMapCoordinates.ofDegrees(55.7558, 37.6173), 6.0 ) - MapView(viewPoint, cacheDirectory = Path.of("mapCache")) + val pointOne = 55.568548 to 37.568604 + val pointTwo = 55.929444 to 37.518434 + val features = buildList { + add(MapCircleFeature(pointOne)) + add(MapCircleFeature(pointTwo)) + add(MapLineFeature(pointOne, pointTwo)) + } + MapView(viewPoint, features = features, cacheDirectory = Path.of("mapCache")) } } diff --git a/src/jvmMain/kotlin/centre/sciprog/maps/compose/coordinates.kt b/src/jvmMain/kotlin/centre/sciprog/maps/compose/GeodeticMapCoordinates.kt similarity index 74% rename from src/jvmMain/kotlin/centre/sciprog/maps/compose/coordinates.kt rename to src/jvmMain/kotlin/centre/sciprog/maps/compose/GeodeticMapCoordinates.kt index 923b35e..fe2743e 100644 --- a/src/jvmMain/kotlin/centre/sciprog/maps/compose/coordinates.kt +++ b/src/jvmMain/kotlin/centre/sciprog/maps/compose/GeodeticMapCoordinates.kt @@ -1,6 +1,5 @@ package centre.sciprog.maps.compose -import kotlinx.coroutines.flow.Flow import kotlin.math.PI /** @@ -44,17 +43,18 @@ public class GeodeticMapCoordinates private constructor(public val latitude: Dou } } -public interface GeoToScreenConversion { - public fun getScreenX(gmc: GeodeticMapCoordinates): Double - public fun getScreenY(gmc: GeodeticMapCoordinates): Double - public fun invalidationFlow(): Flow -} - -public interface ScreenMapCoordinates { - public val gmc: GeodeticMapCoordinates - public val converter: GeoToScreenConversion - - public val x: Double get() = converter.getScreenX(gmc) - public val y: Double get() = converter.getScreenX(gmc) -} \ No newline at end of file +//public interface GeoToScreenConversion { +// public fun getScreenX(gmc: GeodeticMapCoordinates): Double +// public fun getScreenY(gmc: GeodeticMapCoordinates): Double +// +// public fun invalidationFlow(): Flow +//} +// +//public interface ScreenMapCoordinates { +// public val gmc: GeodeticMapCoordinates +// public val converter: GeoToScreenConversion +// +// public val x: Double get() = converter.getScreenX(gmc) +// public val y: Double get() = converter.getScreenX(gmc) +//} \ No newline at end of file diff --git a/src/jvmMain/kotlin/centre/sciprog/maps/compose/MapFeature.kt b/src/jvmMain/kotlin/centre/sciprog/maps/compose/MapFeature.kt new file mode 100644 index 0000000..f8aa22a --- /dev/null +++ b/src/jvmMain/kotlin/centre/sciprog/maps/compose/MapFeature.kt @@ -0,0 +1,44 @@ +package centre.sciprog.maps.compose + +import androidx.compose.ui.graphics.Color + +//TODO replace zoom range with zoom-based representation change +sealed class MapFeature(val zoomRange: ClosedFloatingPointRange) + +private val defaultRange = 0.0..18.0 + +private fun Pair.toCoordinates() = GeodeticMapCoordinates.ofDegrees(first, second) + +class MapCircleFeature( + val center: GeodeticMapCoordinates, + zoomRange: ClosedFloatingPointRange = defaultRange, + val size: Float = 5f, + val color: Color = Color.Red, +) : MapFeature(zoomRange) + +fun MapCircleFeature( + centerCoordinates: Pair, + zoomRange: ClosedFloatingPointRange = defaultRange, + size: Float = 5f, + color: Color = Color.Red, +) = MapCircleFeature( + centerCoordinates.toCoordinates(), + zoomRange, + size, + color +) + + +class MapLineFeature( + val a: GeodeticMapCoordinates, + val b: GeodeticMapCoordinates, + zoomRange: ClosedFloatingPointRange = defaultRange, + val color: Color = Color.Red, +) : MapFeature(zoomRange) + +fun MapLineFeature( + aCoordinates: Pair, + bCoordinates: Pair, + zoomRange: ClosedFloatingPointRange = defaultRange, + color: Color = Color.Red, +) = MapLineFeature(aCoordinates.toCoordinates(), bCoordinates.toCoordinates(), zoomRange, color) \ No newline at end of file diff --git a/src/jvmMain/kotlin/centre/sciprog/maps/compose/MapView.kt b/src/jvmMain/kotlin/centre/sciprog/maps/compose/MapView.kt index 045b606..6d1b6c9 100644 --- a/src/jvmMain/kotlin/centre/sciprog/maps/compose/MapView.kt +++ b/src/jvmMain/kotlin/centre/sciprog/maps/compose/MapView.kt @@ -92,13 +92,13 @@ private class OsMapCache(val scope: CoroutineScope, val client: HttpClient, priv private fun Double.toIndex(): Int = floor(this / TILE_SIZE).toInt() private fun Int.toCoordinate(): Double = (this * TILE_SIZE).toDouble() - private val logger = KotlinLogging.logger("MapView") @OptIn(ExperimentalComposeUiApi::class) @Composable fun MapView( initialViewPoint: MapViewPoint, + features: Collection = emptyList(), modifier: Modifier = Modifier.fillMaxSize(), client: HttpClient = remember { HttpClient(CIO) }, cacheDirectory: Path? = null, @@ -117,7 +117,6 @@ fun MapView( LaunchedEffect(viewPoint, canvasSize) { val left = centerCoordinates.x - canvasSize.width / 2 val right = centerCoordinates.x + canvasSize.width / 2 - val horizontalIndices = left.toIndex()..right.toIndex() val top = (centerCoordinates.y + canvasSize.height / 2) @@ -136,24 +135,32 @@ fun MapView( } + fun Offset.toMercator(): WebMercatorCoordinates = WebMercatorCoordinates( + viewPoint.zoom, + x + centerCoordinates.x - canvasSize.width / 2, + y + centerCoordinates.y - canvasSize.height / 2, + ) + + fun Offset.toGeodetic() = WebMercatorProjection.toGeodetic(toMercator()) + + fun WebMercatorCoordinates.toOffset(): Offset = Offset( + (canvasSize.width / 2 - centerCoordinates.x + x).toFloat(), + (canvasSize.height / 2 - centerCoordinates.y + y).toFloat() + ) + + fun GeodeticMapCoordinates.toOffset(): Offset = WebMercatorProjection.toMercator(this, viewPoint.zoom).toOffset() + var coordinates by remember { mutableStateOf(null) } val canvasModifier = modifier.onPointerEvent(PointerEventType.Move) { val position = it.changes.first().position - val screenCoordinates = TileWebMercatorCoordinates( - viewPoint.zoom, - position.x + centerCoordinates.x - canvasSize.width / 2, - position.y + centerCoordinates.y - canvasSize.height / 2, - ) - coordinates = with(WebMercatorProjection) { - toGeodetic(screenCoordinates) - } + coordinates = position.toGeodetic() }.onPointerEvent(PointerEventType.Press) { println(coordinates) }.onPointerEvent(PointerEventType.Scroll) { viewPoint = viewPoint.zoom(-it.changes.first().scrollDelta.y.toDouble()) }.pointerInput(Unit) { - detectDragGestures { change: PointerInputChange, dragAmount: Offset -> + detectDragGestures { _: PointerInputChange, dragAmount: Offset -> viewPoint = viewPoint.move(-dragAmount.x, +dragAmount.y) } }.fillMaxSize() @@ -179,6 +186,12 @@ fun MapView( topLeft = offset ) } + features.filter { viewPoint.zoom in it.zoomRange }.forEach { + when (it) { + is MapCircleFeature -> drawCircle(it.color, it.size, center = it.center.toOffset()) + is MapLineFeature -> drawLine(it.color, it.a.toOffset(), it.b.toOffset()) + } + } } } } diff --git a/src/jvmMain/kotlin/centre/sciprog/maps/compose/WebMercatorProjection.kt b/src/jvmMain/kotlin/centre/sciprog/maps/compose/WebMercatorProjection.kt index ee7e186..487322c 100644 --- a/src/jvmMain/kotlin/centre/sciprog/maps/compose/WebMercatorProjection.kt +++ b/src/jvmMain/kotlin/centre/sciprog/maps/compose/WebMercatorProjection.kt @@ -7,7 +7,7 @@ package centre.sciprog.maps.compose import kotlin.math.* -public data class TileWebMercatorCoordinates(val zoom: Double, val x: Double, val y: Double) +public data class WebMercatorCoordinates(val zoom: Double, val x: Double, val y: Double) public object WebMercatorProjection { @@ -16,7 +16,7 @@ public object WebMercatorProjection { */ public fun scaleFactor(zoom: Double) = 256.0 / 2 / PI * 2.0.pow(zoom) - public fun toGeodetic(mercator: TileWebMercatorCoordinates): GeodeticMapCoordinates { + public fun toGeodetic(mercator: WebMercatorCoordinates): GeodeticMapCoordinates { val scaleFactor = scaleFactor(mercator.zoom) val longitude = mercator.x / scaleFactor - PI val latitude = (atan(exp(PI - mercator.y / scaleFactor)) - PI / 4) * 2 @@ -26,11 +26,11 @@ public object WebMercatorProjection { /** * https://en.wikipedia.org/wiki/Web_Mercator_projection#Formulas */ - public fun toMercator(gmc: GeodeticMapCoordinates, zoom: Double): TileWebMercatorCoordinates { + public fun toMercator(gmc: GeodeticMapCoordinates, zoom: Double): WebMercatorCoordinates { require(abs(gmc.latitude) <= MercatorProjection.MAXIMUM_LATITUDE) { "Latitude exceeds the maximum latitude for mercator coordinates" } val scaleFactor = scaleFactor(zoom) - return TileWebMercatorCoordinates( + return WebMercatorCoordinates( zoom = zoom, x = scaleFactor * (gmc.longitude + PI), y = scaleFactor * (PI - ln(tan(PI / 4 + gmc.latitude / 2)))