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.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<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
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<Unit>
}
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)
}
//public interface GeoToScreenConversion {
// public fun getScreenX(gmc: GeodeticMapCoordinates): Double
// public fun getScreenY(gmc: GeodeticMapCoordinates): Double
//
// public fun invalidationFlow(): Flow<Unit>
//}
//
//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)
//}

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 Int.toCoordinate(): Double = (this * TILE_SIZE).toDouble()
private val logger = KotlinLogging.logger("MapView")
@OptIn(ExperimentalComposeUiApi::class)
@Composable
fun MapView(
initialViewPoint: MapViewPoint,
features: Collection<MapFeature> = 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<GeodeticMapCoordinates?>(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())
}
}
}
}
}

View File

@ -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)))