Fully functional prototype

This commit is contained in:
Alexander Nozik 2022-07-10 16:53:16 +03:00
parent 3124073800
commit 18b7b91b6f
No known key found for this signature in database
GPG Key ID: F7FCF2DD25C71357
5 changed files with 95 additions and 33 deletions

View File

@ -4,9 +4,7 @@ import androidx.compose.material.MaterialTheme
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
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 centre.sciprog.maps.compose.GeodeticMapCoordinates import centre.sciprog.maps.compose.*
import centre.sciprog.maps.compose.MapView
import centre.sciprog.maps.compose.MapViewPoint
import java.nio.file.Path import java.nio.file.Path
@Composable @Composable
@ -17,7 +15,14 @@ fun App() {
GeodeticMapCoordinates.ofDegrees(55.7558, 37.6173), GeodeticMapCoordinates.ofDegrees(55.7558, 37.6173),
6.0 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<MapFeature> {
add(MapCircleFeature(pointOne))
add(MapCircleFeature(pointTwo))
add(MapLineFeature(pointOne, pointTwo))
}
MapView(viewPoint, features = features, cacheDirectory = Path.of("mapCache"))
} }
} }

View File

@ -1,6 +1,5 @@
package centre.sciprog.maps.compose package centre.sciprog.maps.compose
import kotlinx.coroutines.flow.Flow
import kotlin.math.PI 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<Unit> //public interface GeoToScreenConversion {
} // public fun getScreenX(gmc: GeodeticMapCoordinates): Double
// public fun getScreenY(gmc: GeodeticMapCoordinates): Double
public interface ScreenMapCoordinates { //
public val gmc: GeodeticMapCoordinates // public fun invalidationFlow(): Flow<Unit>
public val converter: GeoToScreenConversion //}
//
public val x: Double get() = converter.getScreenX(gmc) //public interface ScreenMapCoordinates {
public val y: Double get() = converter.getScreenX(gmc) // public val gmc: GeodeticMapCoordinates
} // public val converter: GeoToScreenConversion
//
// public val x: Double get() = converter.getScreenX(gmc)
// public val y: Double get() = converter.getScreenX(gmc)
//}

View File

@ -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<Double>)
private val defaultRange = 0.0..18.0
private fun Pair<Double, Double>.toCoordinates() = GeodeticMapCoordinates.ofDegrees(first, second)
class MapCircleFeature(
val center: GeodeticMapCoordinates,
zoomRange: ClosedFloatingPointRange<Double> = defaultRange,
val size: Float = 5f,
val color: Color = Color.Red,
) : MapFeature(zoomRange)
fun MapCircleFeature(
centerCoordinates: Pair<Double, Double>,
zoomRange: ClosedFloatingPointRange<Double> = defaultRange,
size: Float = 5f,
color: Color = Color.Red,
) = MapCircleFeature(
centerCoordinates.toCoordinates(),
zoomRange,
size,
color
)
class MapLineFeature(
val a: GeodeticMapCoordinates,
val b: GeodeticMapCoordinates,
zoomRange: ClosedFloatingPointRange<Double> = defaultRange,
val color: Color = Color.Red,
) : MapFeature(zoomRange)
fun MapLineFeature(
aCoordinates: Pair<Double, Double>,
bCoordinates: Pair<Double, Double>,
zoomRange: ClosedFloatingPointRange<Double> = defaultRange,
color: Color = Color.Red,
) = MapLineFeature(aCoordinates.toCoordinates(), bCoordinates.toCoordinates(), zoomRange, color)

View File

@ -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 Double.toIndex(): Int = floor(this / TILE_SIZE).toInt()
private fun Int.toCoordinate(): Double = (this * TILE_SIZE).toDouble() private fun Int.toCoordinate(): Double = (this * TILE_SIZE).toDouble()
private val logger = KotlinLogging.logger("MapView") private val logger = KotlinLogging.logger("MapView")
@OptIn(ExperimentalComposeUiApi::class) @OptIn(ExperimentalComposeUiApi::class)
@Composable @Composable
fun MapView( fun MapView(
initialViewPoint: MapViewPoint, initialViewPoint: MapViewPoint,
features: Collection<MapFeature> = emptyList(),
modifier: Modifier = Modifier.fillMaxSize(), modifier: Modifier = Modifier.fillMaxSize(),
client: HttpClient = remember { HttpClient(CIO) }, client: HttpClient = remember { HttpClient(CIO) },
cacheDirectory: Path? = null, cacheDirectory: Path? = null,
@ -117,7 +117,6 @@ fun MapView(
LaunchedEffect(viewPoint, canvasSize) { LaunchedEffect(viewPoint, canvasSize) {
val left = centerCoordinates.x - canvasSize.width / 2 val left = centerCoordinates.x - canvasSize.width / 2
val right = centerCoordinates.x + canvasSize.width / 2 val right = centerCoordinates.x + canvasSize.width / 2
val horizontalIndices = left.toIndex()..right.toIndex() val horizontalIndices = left.toIndex()..right.toIndex()
val top = (centerCoordinates.y + canvasSize.height / 2) 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<GeodeticMapCoordinates?>(null) } var coordinates by remember { mutableStateOf<GeodeticMapCoordinates?>(null) }
val canvasModifier = modifier.onPointerEvent(PointerEventType.Move) { val canvasModifier = modifier.onPointerEvent(PointerEventType.Move) {
val position = it.changes.first().position val position = it.changes.first().position
val screenCoordinates = TileWebMercatorCoordinates( coordinates = position.toGeodetic()
viewPoint.zoom,
position.x + centerCoordinates.x - canvasSize.width / 2,
position.y + centerCoordinates.y - canvasSize.height / 2,
)
coordinates = with(WebMercatorProjection) {
toGeodetic(screenCoordinates)
}
}.onPointerEvent(PointerEventType.Press) { }.onPointerEvent(PointerEventType.Press) {
println(coordinates) println(coordinates)
}.onPointerEvent(PointerEventType.Scroll) { }.onPointerEvent(PointerEventType.Scroll) {
viewPoint = viewPoint.zoom(-it.changes.first().scrollDelta.y.toDouble()) viewPoint = viewPoint.zoom(-it.changes.first().scrollDelta.y.toDouble())
}.pointerInput(Unit) { }.pointerInput(Unit) {
detectDragGestures { change: PointerInputChange, dragAmount: Offset -> detectDragGestures { _: PointerInputChange, dragAmount: Offset ->
viewPoint = viewPoint.move(-dragAmount.x, +dragAmount.y) viewPoint = viewPoint.move(-dragAmount.x, +dragAmount.y)
} }
}.fillMaxSize() }.fillMaxSize()
@ -179,6 +186,12 @@ fun MapView(
topLeft = offset 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())
}
}
} }
} }
} }

View File

@ -7,7 +7,7 @@ package centre.sciprog.maps.compose
import kotlin.math.* 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 { 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 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 scaleFactor = scaleFactor(mercator.zoom)
val longitude = mercator.x / scaleFactor - PI val longitude = mercator.x / scaleFactor - PI
val latitude = (atan(exp(PI - mercator.y / scaleFactor)) - PI / 4) * 2 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 * 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" } require(abs(gmc.latitude) <= MercatorProjection.MAXIMUM_LATITUDE) { "Latitude exceeds the maximum latitude for mercator coordinates" }
val scaleFactor = scaleFactor(zoom) val scaleFactor = scaleFactor(zoom)
return TileWebMercatorCoordinates( return WebMercatorCoordinates(
zoom = zoom, zoom = zoom,
x = scaleFactor * (gmc.longitude + PI), x = scaleFactor * (gmc.longitude + PI),
y = scaleFactor * (PI - ln(tan(PI / 4 + gmc.latitude / 2))) y = scaleFactor * (PI - ln(tan(PI / 4 + gmc.latitude / 2)))