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.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"))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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)
|
||||||
|
//}
|
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 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())
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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)))
|
||||||
|
Loading…
Reference in New Issue
Block a user