Fully functional prototype
This commit is contained in:
parent
3124073800
commit
18b7b91b6f
@ -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"))
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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)
|
||||
//}
|
44
src/jvmMain/kotlin/centre/sciprog/maps/compose/MapFeature.kt
Normal file
44
src/jvmMain/kotlin/centre/sciprog/maps/compose/MapFeature.kt
Normal 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)
|
@ -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())
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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)))
|
||||
|
Loading…
Reference in New Issue
Block a user