Point of view model rework. Drag and zoom fully working.
This commit is contained in:
parent
ea16a02a48
commit
3124073800
4
.gitignore
vendored
4
.gitignore
vendored
@ -1,6 +1,6 @@
|
||||
build/
|
||||
.gradle/
|
||||
.idea/
|
||||
mapCache/
|
||||
*.iml
|
||||
|
||||
*.iml
|
||||
mapCache/
|
@ -1,4 +1,4 @@
|
||||
kotlin.code.style=official
|
||||
kotlin.version=1.6.10
|
||||
agp.version=4.2.2
|
||||
compose.version=1.1.0
|
||||
compose.version=1.1.1
|
@ -1,27 +1,23 @@
|
||||
// Copyright 2000-2021 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file.
|
||||
import androidx.compose.desktop.ui.tooling.preview.Preview
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.material.Button
|
||||
import androidx.compose.material.MaterialTheme
|
||||
import androidx.compose.material.Text
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.ui.Modifier
|
||||
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.MapRectangle
|
||||
import centre.sciprog.maps.compose.MapView
|
||||
import centre.sciprog.maps.compose.MapViewPoint
|
||||
import java.nio.file.Path
|
||||
|
||||
@Composable
|
||||
@Preview
|
||||
fun App() {
|
||||
MaterialTheme {
|
||||
val map = MapRectangle.of(
|
||||
GeodeticMapCoordinates.ofDegrees(66.513260, 0.0),
|
||||
GeodeticMapCoordinates.ofDegrees(40.979897, 44.999999),
|
||||
val viewPoint = MapViewPoint(
|
||||
GeodeticMapCoordinates.ofDegrees(55.7558, 37.6173),
|
||||
6.0
|
||||
)
|
||||
MapView(map, modifier = Modifier.fillMaxSize(), initialZoom = 4.0, cacheDirectory = Path.of("mapCache"))
|
||||
MapView(viewPoint, cacheDirectory = Path.of("mapCache"))
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -29,7 +29,7 @@ import org.jetbrains.skia.Image
|
||||
import java.net.URL
|
||||
import java.nio.file.Path
|
||||
import kotlin.io.path.*
|
||||
import kotlin.math.*
|
||||
import kotlin.math.floor
|
||||
|
||||
|
||||
private const val TILE_SIZE = 256
|
||||
@ -89,64 +89,46 @@ 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(
|
||||
initialRectangle: MapRectangle,
|
||||
modifier: Modifier,
|
||||
initialViewPoint: MapViewPoint,
|
||||
modifier: Modifier = Modifier.fillMaxSize(),
|
||||
client: HttpClient = remember { HttpClient(CIO) },
|
||||
cacheDirectory: Path? = null,
|
||||
initialZoom: Double? = null,
|
||||
) {
|
||||
var viewPoint by remember { mutableStateOf(initialViewPoint) }
|
||||
|
||||
val scope = rememberCoroutineScope()
|
||||
val mapCache = remember { OsMapCache(scope, client, cacheDirectory) }
|
||||
val mapTiles = remember { mutableStateListOf<OsMapTile>()}
|
||||
val mapTiles = remember { mutableStateListOf<OsMapTile>() }
|
||||
|
||||
var mapRectangle by remember { mutableStateOf(initialRectangle) }
|
||||
//var mapRectangle by remember { mutableStateOf(initialRectangle) }
|
||||
var canvasSize by remember { mutableStateOf(Size(512f, 512f)) }
|
||||
|
||||
//TODO provide override for tiling
|
||||
val numTilesHorizontal by derivedStateOf {
|
||||
ceil(canvasSize.width / TILE_SIZE).toInt()
|
||||
val centerCoordinates by derivedStateOf { viewPoint.toMercator() }
|
||||
|
||||
}
|
||||
val numTilesVertical by derivedStateOf {
|
||||
ceil(canvasSize.height / TILE_SIZE).toInt()
|
||||
}
|
||||
LaunchedEffect(viewPoint, canvasSize) {
|
||||
val left = centerCoordinates.x - canvasSize.width / 2
|
||||
val right = centerCoordinates.x + canvasSize.width / 2
|
||||
|
||||
val zoom by derivedStateOf {
|
||||
val xOffsetUnscaled = mapRectangle.bottomRight.longitude - mapRectangle.topLeft.longitude
|
||||
val yOffsetUnscaled = ln(
|
||||
tan(PI / 4 + mapRectangle.topLeft.latitude / 2) / tan(PI / 4 + mapRectangle.bottomRight.latitude / 2)
|
||||
)
|
||||
val horizontalIndices = left.toIndex()..right.toIndex()
|
||||
|
||||
initialZoom ?: ceil(
|
||||
log2(
|
||||
PI / max(
|
||||
abs(xOffsetUnscaled / numTilesHorizontal),
|
||||
abs(yOffsetUnscaled / numTilesVertical)
|
||||
)
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
val scaleFactor by derivedStateOf { WebMercatorProjection.scaleFactor(zoom) }
|
||||
|
||||
val topLeft by derivedStateOf { with(WebMercatorProjection) { mapRectangle.topLeft.toMercator(zoom) } }
|
||||
|
||||
LaunchedEffect(mapRectangle, canvasSize, zoom) {
|
||||
|
||||
val startIndexHorizontal = (topLeft.x / TILE_SIZE).toInt()
|
||||
val startIndexVertical = (topLeft.y / TILE_SIZE).toInt()
|
||||
val top = (centerCoordinates.y + canvasSize.height / 2)
|
||||
val bottom = (centerCoordinates.y - canvasSize.height / 2)
|
||||
val verticalIndices = bottom.toIndex()..top.toIndex()
|
||||
|
||||
mapTiles.clear()
|
||||
|
||||
for (j in 0 until numTilesVertical) {
|
||||
for (i in 0 until numTilesHorizontal) {
|
||||
val tileId = OsMapTileId(zoom.toInt(), startIndexHorizontal + i, startIndexVertical + j)
|
||||
for (j in verticalIndices) {
|
||||
for (i in horizontalIndices) {
|
||||
val tileId = OsMapTileId(viewPoint.zoom.toInt(), i, j)
|
||||
val tile = mapCache.loadTile(tileId)
|
||||
mapTiles.add(tile)
|
||||
}
|
||||
@ -159,18 +141,20 @@ fun MapView(
|
||||
val canvasModifier = modifier.onPointerEvent(PointerEventType.Move) {
|
||||
val position = it.changes.first().position
|
||||
val screenCoordinates = TileWebMercatorCoordinates(
|
||||
zoom,
|
||||
position.x.toDouble() + topLeft.x,
|
||||
position.y.toDouble() + topLeft.y
|
||||
viewPoint.zoom,
|
||||
position.x + centerCoordinates.x - canvasSize.width / 2,
|
||||
position.y + centerCoordinates.y - canvasSize.height / 2,
|
||||
)
|
||||
coordinates = with(WebMercatorProjection) {
|
||||
screenCoordinates.toGeodetic()
|
||||
toGeodetic(screenCoordinates)
|
||||
}
|
||||
}.onPointerEvent(PointerEventType.Press) {
|
||||
println(coordinates)
|
||||
}.onPointerEvent(PointerEventType.Scroll) {
|
||||
viewPoint = viewPoint.zoom(-it.changes.first().scrollDelta.y.toDouble())
|
||||
}.pointerInput(Unit) {
|
||||
detectDragGestures { change: PointerInputChange, dragAmount: Offset ->
|
||||
mapRectangle = mapRectangle.move(dragAmount.y / scaleFactor, -dragAmount.x / scaleFactor)
|
||||
viewPoint = viewPoint.move(-dragAmount.x, +dragAmount.y)
|
||||
}
|
||||
}.fillMaxSize()
|
||||
|
||||
@ -187,8 +171,8 @@ fun MapView(
|
||||
//converting back from tile index to screen offset
|
||||
logger.debug { "Drawing tile $id" }
|
||||
val offset = Offset(
|
||||
id.i.toFloat() * TILE_SIZE - topLeft.x.toFloat(),
|
||||
id.j.toFloat() * TILE_SIZE - topLeft.y.toFloat()
|
||||
(canvasSize.width / 2 - centerCoordinates.x + id.i.toCoordinate()).toFloat(),
|
||||
(canvasSize.height / 2 - centerCoordinates.y + id.j.toCoordinate()).toFloat()
|
||||
)
|
||||
drawImage(
|
||||
image = image,
|
||||
|
@ -0,0 +1,39 @@
|
||||
package centre.sciprog.maps.compose
|
||||
|
||||
/**
|
||||
* Observable position on the map
|
||||
*/
|
||||
data class MapViewPoint(
|
||||
val focus: GeodeticMapCoordinates,
|
||||
val zoom: Double,
|
||||
) {
|
||||
val scaleFactor by lazy { WebMercatorProjection.scaleFactor(zoom) }
|
||||
}
|
||||
|
||||
fun MapViewPoint.move(deltaX: Float, deltaY: Float): MapViewPoint {
|
||||
val newCoordinates = GeodeticMapCoordinates.ofRadians(
|
||||
(focus.latitude + deltaY / scaleFactor).coerceIn(
|
||||
-MercatorProjection.MAXIMUM_LATITUDE,
|
||||
MercatorProjection.MAXIMUM_LATITUDE
|
||||
),
|
||||
focus.longitude + deltaX / scaleFactor
|
||||
)
|
||||
return MapViewPoint(newCoordinates, zoom)
|
||||
}
|
||||
|
||||
fun MapViewPoint.move(delta: GeodeticMapCoordinates): MapViewPoint {
|
||||
val newCoordinates = GeodeticMapCoordinates.ofRadians(
|
||||
(focus.latitude + delta.latitude).coerceIn(
|
||||
-MercatorProjection.MAXIMUM_LATITUDE,
|
||||
MercatorProjection.MAXIMUM_LATITUDE
|
||||
),
|
||||
focus.longitude + delta.longitude
|
||||
)
|
||||
return MapViewPoint(newCoordinates, zoom)
|
||||
}
|
||||
|
||||
fun MapViewPoint.zoom(zoomDelta: Double): MapViewPoint {
|
||||
return copy(zoom = (zoom + zoomDelta).coerceIn(1.0, 18.0))
|
||||
}
|
||||
|
||||
fun MapViewPoint.toMercator() = WebMercatorProjection.toMercator(focus, zoom)
|
@ -16,47 +16,47 @@ public object WebMercatorProjection {
|
||||
*/
|
||||
public fun scaleFactor(zoom: Double) = 256.0 / 2 / PI * 2.0.pow(zoom)
|
||||
|
||||
public fun TileWebMercatorCoordinates.toGeodetic(): GeodeticMapCoordinates {
|
||||
val scaleFactor = scaleFactor(zoom)
|
||||
val longitude = x / scaleFactor - PI
|
||||
val latitude = (atan(exp(PI - y / scaleFactor)) - PI / 4) * 2
|
||||
public fun toGeodetic(mercator: TileWebMercatorCoordinates): GeodeticMapCoordinates {
|
||||
val scaleFactor = scaleFactor(mercator.zoom)
|
||||
val longitude = mercator.x / scaleFactor - PI
|
||||
val latitude = (atan(exp(PI - mercator.y / scaleFactor)) - PI / 4) * 2
|
||||
return GeodeticMapCoordinates.ofRadians(latitude, longitude)
|
||||
}
|
||||
|
||||
/**
|
||||
* https://en.wikipedia.org/wiki/Web_Mercator_projection#Formulas
|
||||
*/
|
||||
public fun GeodeticMapCoordinates.toMercator(zoom: Double): TileWebMercatorCoordinates {
|
||||
require(abs(latitude) <= MercatorProjection.MAXIMUM_LATITUDE) { "Latitude exceeds the maximum latitude for mercator coordinates" }
|
||||
public fun toMercator(gmc: GeodeticMapCoordinates, zoom: Double): TileWebMercatorCoordinates {
|
||||
require(abs(gmc.latitude) <= MercatorProjection.MAXIMUM_LATITUDE) { "Latitude exceeds the maximum latitude for mercator coordinates" }
|
||||
|
||||
val scaleFactor = scaleFactor(zoom)
|
||||
return TileWebMercatorCoordinates(
|
||||
zoom = zoom,
|
||||
x = scaleFactor * (longitude + PI),
|
||||
y = scaleFactor * (PI - ln(tan(PI / 4 + latitude / 2)))
|
||||
x = scaleFactor * (gmc.longitude + PI),
|
||||
y = scaleFactor * (PI - ln(tan(PI / 4 + gmc.latitude / 2)))
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Compute and offset of [target] coordinate relative to [base] coordinate. If [zoom] is null, then optimal zoom
|
||||
* will be computed to put the resulting x and y coordinates between -127.0 and 128.0
|
||||
*/
|
||||
public fun computeOffset(
|
||||
base: GeodeticMapCoordinates,
|
||||
target: GeodeticMapCoordinates,
|
||||
zoom: Double? = null,
|
||||
): TileWebMercatorCoordinates {
|
||||
val xOffsetUnscaled = target.longitude - base.longitude
|
||||
val yOffsetUnscaled = ln(
|
||||
tan(PI / 4 + target.latitude / 2) / tan(PI / 4 + base.latitude / 2)
|
||||
)
|
||||
|
||||
val computedZoom = zoom ?: ceil(log2(PI / max(abs(xOffsetUnscaled), abs(yOffsetUnscaled))))
|
||||
val scaleFactor = scaleFactor(computedZoom)
|
||||
return TileWebMercatorCoordinates(
|
||||
computedZoom,
|
||||
x = scaleFactor * xOffsetUnscaled,
|
||||
y = scaleFactor * yOffsetUnscaled
|
||||
)
|
||||
}
|
||||
// /**
|
||||
// * Compute and offset of [target] coordinate relative to [base] coordinate. If [zoom] is null, then optimal zoom
|
||||
// * will be computed to put the resulting x and y coordinates between -127.0 and 128.0
|
||||
// */
|
||||
// public fun computeOffset(
|
||||
// base: GeodeticMapCoordinates,
|
||||
// target: GeodeticMapCoordinates,
|
||||
// zoom: Double? = null,
|
||||
// ): TileWebMercatorCoordinates {
|
||||
// val xOffsetUnscaled = target.longitude - base.longitude
|
||||
// val yOffsetUnscaled = ln(
|
||||
// tan(PI / 4 + target.latitude / 2) / tan(PI / 4 + base.latitude / 2)
|
||||
// )
|
||||
//
|
||||
// val computedZoom = zoom ?: ceil(log2(PI / max(abs(xOffsetUnscaled), abs(yOffsetUnscaled))))
|
||||
// val scaleFactor = scaleFactor(computedZoom)
|
||||
// return TileWebMercatorCoordinates(
|
||||
// computedZoom,
|
||||
// x = scaleFactor * xOffsetUnscaled,
|
||||
// y = scaleFactor * yOffsetUnscaled
|
||||
// )
|
||||
// }
|
||||
}
|
@ -1,58 +0,0 @@
|
||||
package centre.sciprog.maps.compose
|
||||
|
||||
import kotlin.math.max
|
||||
import kotlin.math.min
|
||||
|
||||
class MapRectangle private constructor(
|
||||
var topLeft: GeodeticMapCoordinates,
|
||||
var bottomRight: GeodeticMapCoordinates,
|
||||
) {
|
||||
init {
|
||||
require(topLeft.latitude >= bottomRight.latitude)
|
||||
require(topLeft.longitude <= bottomRight.longitude)
|
||||
}
|
||||
|
||||
val topRight: GeodeticMapCoordinates get() = GeodeticMapCoordinates.ofRadians(topLeft.latitude, bottomRight.longitude)
|
||||
val bottomLeft: GeodeticMapCoordinates get() = GeodeticMapCoordinates.ofRadians(bottomRight.latitude, topLeft.longitude)
|
||||
|
||||
val topLatitude get() = topLeft.latitude
|
||||
val bottomLatitude get() = bottomRight.latitude
|
||||
|
||||
val leftLongitude get() = topLeft.longitude
|
||||
val rightLongitude get() = bottomRight.longitude
|
||||
|
||||
companion object{
|
||||
fun of(
|
||||
a: GeodeticMapCoordinates,
|
||||
b: GeodeticMapCoordinates
|
||||
) = MapRectangle(
|
||||
GeodeticMapCoordinates.ofRadians(max(a.latitude, b.latitude), min(a.longitude,b.longitude)),
|
||||
GeodeticMapCoordinates.ofRadians(min(a.latitude, b.latitude), max(a.longitude,b.longitude)),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
internal fun MapRectangle.move(latitudeDelta: Double, longitudeDelta: Double): MapRectangle {
|
||||
val safeLatitudeDelta: Double = if(topLatitude + latitudeDelta > MercatorProjection.MAXIMUM_LATITUDE){
|
||||
0.0
|
||||
} else if(bottomLatitude + latitudeDelta < -MercatorProjection.MAXIMUM_LATITUDE){
|
||||
0.0
|
||||
} else {
|
||||
latitudeDelta
|
||||
}
|
||||
return MapRectangle.of(
|
||||
GeodeticMapCoordinates.ofRadians(topLeft.latitude + safeLatitudeDelta, topLeft.longitude + longitudeDelta),
|
||||
GeodeticMapCoordinates.ofRadians(bottomRight.latitude + safeLatitudeDelta, bottomRight.longitude + longitudeDelta)
|
||||
)
|
||||
}
|
||||
|
||||
sealed interface MapFeature
|
||||
|
||||
class MapLine(
|
||||
val from: GeodeticMapCoordinates,
|
||||
val to: GeodeticMapCoordinates,
|
||||
) : MapFeature
|
||||
|
||||
class MapCircle(
|
||||
val center: GeodeticMapCoordinates,
|
||||
)
|
Loading…
Reference in New Issue
Block a user