Implementation for TAVRIDA-T-7
This commit is contained in:
parent
960c64d480
commit
b764fdd95a
@ -2,7 +2,6 @@ package centre.sciprog.maps
|
|||||||
|
|
||||||
import kotlin.math.pow
|
import kotlin.math.pow
|
||||||
import kotlin.math.roundToInt
|
import kotlin.math.roundToInt
|
||||||
import kotlin.math.sign
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Observable position on the map. Includes observation coordinate and [zoom] factor
|
* Observable position on the map. Includes observation coordinate and [zoom] factor
|
||||||
@ -14,7 +13,7 @@ data class MapViewPoint(
|
|||||||
val scaleFactor by lazy { WebMercatorProjection.scaleFactor(zoom.roundToInt()) }
|
val scaleFactor by lazy { WebMercatorProjection.scaleFactor(zoom.roundToInt()) }
|
||||||
}
|
}
|
||||||
|
|
||||||
fun MapViewPoint.move(deltaX: Float, deltaY: Float): MapViewPoint {
|
fun MapViewPoint.move(deltaX: Double, deltaY: Double): MapViewPoint {
|
||||||
val newCoordinates = GeodeticMapCoordinates.ofRadians(
|
val newCoordinates = GeodeticMapCoordinates.ofRadians(
|
||||||
(focus.latitude + deltaY / scaleFactor).coerceIn(
|
(focus.latitude + deltaY / scaleFactor).coerceIn(
|
||||||
-MercatorProjection.MAXIMUM_LATITUDE,
|
-MercatorProjection.MAXIMUM_LATITUDE,
|
||||||
@ -38,11 +37,11 @@ fun MapViewPoint.move(delta: GeodeticMapCoordinates): MapViewPoint {
|
|||||||
|
|
||||||
fun MapViewPoint.zoom(zoomDelta: Double): MapViewPoint = copy(zoom = (zoom + zoomDelta).coerceIn(2.0, 18.0))
|
fun MapViewPoint.zoom(zoomDelta: Double): MapViewPoint = copy(zoom = (zoom + zoomDelta).coerceIn(2.0, 18.0))
|
||||||
|
|
||||||
fun MapViewPoint.zoom(zoomDelta: Double, at: GeodeticMapCoordinates): MapViewPoint {
|
fun MapViewPoint.zoom(zoomDelta: Double, invariant: GeodeticMapCoordinates): MapViewPoint {
|
||||||
val difScale = 2.0.pow(-zoomDelta)
|
val difScale = 2.0.pow(-zoomDelta)
|
||||||
val newCenter = GeodeticMapCoordinates.ofRadians(
|
val newCenter = GeodeticMapCoordinates.ofRadians(
|
||||||
focus.latitude + (at.latitude - focus.latitude) * difScale,
|
focus.latitude + (invariant.latitude - focus.latitude) * difScale,
|
||||||
focus.longitude + (at.longitude - focus.longitude) * difScale
|
focus.longitude + (invariant.longitude - focus.longitude) * difScale
|
||||||
)
|
)
|
||||||
return MapViewPoint(newCenter, (zoom + zoomDelta).coerceIn(2.0, 18.0))
|
return MapViewPoint(newCenter, (zoom + zoomDelta).coerceIn(2.0, 18.0))
|
||||||
}
|
}
|
@ -6,12 +6,19 @@ import androidx.compose.ui.Modifier
|
|||||||
import centre.sciprog.maps.GeodeticMapCoordinates
|
import centre.sciprog.maps.GeodeticMapCoordinates
|
||||||
import centre.sciprog.maps.MapViewPoint
|
import centre.sciprog.maps.MapViewPoint
|
||||||
|
|
||||||
|
|
||||||
|
data class MapViewConfig(
|
||||||
|
val zoomSpeed: Double = 1.0 / 3.0,
|
||||||
|
)
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
expect fun MapView(
|
expect fun MapView(
|
||||||
initialViewPoint: MapViewPoint,
|
initialViewPoint: MapViewPoint,
|
||||||
mapTileProvider: MapTileProvider,
|
mapTileProvider: MapTileProvider,
|
||||||
features: Map<FeatureId, MapFeature>,
|
features: Map<FeatureId, MapFeature>,
|
||||||
onClick: (GeodeticMapCoordinates) -> Unit = {},
|
onClick: (GeodeticMapCoordinates) -> Unit = {},
|
||||||
|
//TODO consider replacing by modifier
|
||||||
|
config: MapViewConfig = MapViewConfig(),
|
||||||
modifier: Modifier = Modifier.fillMaxSize(),
|
modifier: Modifier = Modifier.fillMaxSize(),
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -20,10 +27,11 @@ fun MapView(
|
|||||||
initialViewPoint: MapViewPoint,
|
initialViewPoint: MapViewPoint,
|
||||||
mapTileProvider: MapTileProvider,
|
mapTileProvider: MapTileProvider,
|
||||||
onClick: (GeodeticMapCoordinates) -> Unit = {},
|
onClick: (GeodeticMapCoordinates) -> Unit = {},
|
||||||
|
config: MapViewConfig = MapViewConfig(),
|
||||||
modifier: Modifier = Modifier.fillMaxSize(),
|
modifier: Modifier = Modifier.fillMaxSize(),
|
||||||
addFeatures: @Composable() (FeatureBuilder.() -> Unit) = {},
|
addFeatures: @Composable() (FeatureBuilder.() -> Unit) = {},
|
||||||
) {
|
) {
|
||||||
val featuresBuilder = MapFeatureBuilder()
|
val featuresBuilder = MapFeatureBuilder()
|
||||||
featuresBuilder.addFeatures()
|
featuresBuilder.addFeatures()
|
||||||
MapView(initialViewPoint, mapTileProvider, featuresBuilder.build(), onClick, modifier)
|
MapView(initialViewPoint, mapTileProvider, featuresBuilder.build(), onClick, config, modifier)
|
||||||
}
|
}
|
@ -43,6 +43,7 @@ actual fun MapView(
|
|||||||
mapTileProvider: MapTileProvider,
|
mapTileProvider: MapTileProvider,
|
||||||
features: Map<FeatureId, MapFeature>,
|
features: Map<FeatureId, MapFeature>,
|
||||||
onClick: (GeodeticMapCoordinates) -> Unit,
|
onClick: (GeodeticMapCoordinates) -> Unit,
|
||||||
|
config: MapViewConfig,
|
||||||
modifier: Modifier,
|
modifier: Modifier,
|
||||||
) {
|
) {
|
||||||
|
|
||||||
@ -50,6 +51,8 @@ actual fun MapView(
|
|||||||
|
|
||||||
val zoom: Int by derivedStateOf { viewPoint.zoom.roundToInt() }
|
val zoom: Int by derivedStateOf { viewPoint.zoom.roundToInt() }
|
||||||
|
|
||||||
|
val tileScale: Double by derivedStateOf { 2.0.pow(viewPoint.zoom - zoom) }
|
||||||
|
|
||||||
val mapTiles = remember { mutableStateListOf<MapTile>() }
|
val mapTiles = remember { mutableStateListOf<MapTile>() }
|
||||||
|
|
||||||
//var mapRectangle by remember { mutableStateOf(initialRectangle) }
|
//var mapRectangle by remember { mutableStateOf(initialRectangle) }
|
||||||
@ -59,10 +62,13 @@ actual fun MapView(
|
|||||||
|
|
||||||
fun DpOffset.toMercator(): WebMercatorCoordinates = WebMercatorCoordinates(
|
fun DpOffset.toMercator(): WebMercatorCoordinates = WebMercatorCoordinates(
|
||||||
zoom,
|
zoom,
|
||||||
(x - canvasSize.width / 2).value + centerCoordinates.x,
|
(x - canvasSize.width / 2).value / tileScale + centerCoordinates.x,
|
||||||
(y - canvasSize.height / 2).value + centerCoordinates.y,
|
(y - canvasSize.height / 2).value / tileScale + centerCoordinates.y,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Convert screen independent offset to GMC, adjusting for fractional zoom
|
||||||
|
*/
|
||||||
fun DpOffset.toGeodetic() = WebMercatorProjection.toGeodetic(toMercator())
|
fun DpOffset.toGeodetic() = WebMercatorProjection.toGeodetic(toMercator())
|
||||||
|
|
||||||
@OptIn(ExperimentalComposeUiApi::class)
|
@OptIn(ExperimentalComposeUiApi::class)
|
||||||
@ -73,23 +79,23 @@ actual fun MapView(
|
|||||||
val change = it.changes.first()
|
val change = it.changes.first()
|
||||||
val (xPos, yPos) = change.position
|
val (xPos, yPos) = change.position
|
||||||
//compute invariant point of translation
|
//compute invariant point of translation
|
||||||
val at = DpOffset(xPos.toDp(), yPos.toDp()).toGeodetic()
|
val invariant = DpOffset(xPos.toDp(), yPos.toDp()).toGeodetic()
|
||||||
viewPoint = viewPoint.zoom(-change.scrollDelta.y.toDouble(), at)
|
viewPoint = viewPoint.zoom(-change.scrollDelta.y.toDouble() * config.zoomSpeed, invariant)
|
||||||
}.pointerInput(Unit) {
|
}.pointerInput(Unit) {
|
||||||
detectDragGestures { _: PointerInputChange, dragAmount: Offset ->
|
detectDragGestures { _: PointerInputChange, dragAmount: Offset ->
|
||||||
viewPoint = viewPoint.move(-dragAmount.x, +dragAmount.y)
|
viewPoint = viewPoint.move(-dragAmount.x.toDp().value / tileScale, +dragAmount.y.toDp().value / tileScale)
|
||||||
}
|
}
|
||||||
}.fillMaxSize()
|
}.fillMaxSize()
|
||||||
|
|
||||||
|
|
||||||
// Load tiles asynchronously
|
// Load tiles asynchronously
|
||||||
LaunchedEffect(viewPoint, canvasSize) {
|
LaunchedEffect(viewPoint, canvasSize) {
|
||||||
val left = centerCoordinates.x - canvasSize.width.value / 2
|
val left = centerCoordinates.x - canvasSize.width.value / 2 / tileScale
|
||||||
val right = centerCoordinates.x + canvasSize.width.value / 2
|
val right = centerCoordinates.x + canvasSize.width.value / 2 / tileScale
|
||||||
val horizontalIndices = mapTileProvider.toIndex(left)..mapTileProvider.toIndex(right)
|
val horizontalIndices = mapTileProvider.toIndex(left)..mapTileProvider.toIndex(right)
|
||||||
|
|
||||||
val top = (centerCoordinates.y + canvasSize.height.value / 2)
|
val top = (centerCoordinates.y + canvasSize.height.value / 2 / tileScale)
|
||||||
val bottom = (centerCoordinates.y - canvasSize.height.value / 2)
|
val bottom = (centerCoordinates.y - canvasSize.height.value / 2 / tileScale)
|
||||||
val verticalIndices = mapTileProvider.toIndex(bottom)..mapTileProvider.toIndex(top)
|
val verticalIndices = mapTileProvider.toIndex(bottom)..mapTileProvider.toIndex(top)
|
||||||
|
|
||||||
mapTiles.clear()
|
mapTiles.clear()
|
||||||
@ -112,14 +118,15 @@ actual fun MapView(
|
|||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// d
|
||||||
Canvas(canvasModifier) {
|
Canvas(canvasModifier) {
|
||||||
|
|
||||||
fun WebMercatorCoordinates.toOffset(): Offset = Offset(
|
fun WebMercatorCoordinates.toOffset(): Offset = Offset(
|
||||||
(canvasSize.width / 2 - centerCoordinates.x.dp + x.dp).toPx(),
|
(canvasSize.width / 2 + (x.dp - centerCoordinates.x.dp) * tileScale.toFloat()).toPx(),
|
||||||
(canvasSize.height / 2 - centerCoordinates.y.dp + y.dp).toPx()
|
(canvasSize.height / 2 + (y.dp - centerCoordinates.y.dp) * tileScale.toFloat()).toPx()
|
||||||
)
|
)
|
||||||
|
|
||||||
|
//Convert GMC to offset in pixels (not DP), adjusting for zoom
|
||||||
fun GeodeticMapCoordinates.toOffset(): Offset = WebMercatorProjection.toMercator(this, zoom).toOffset()
|
fun GeodeticMapCoordinates.toOffset(): Offset = WebMercatorProjection.toMercator(this, zoom).toOffset()
|
||||||
|
|
||||||
|
|
||||||
@ -160,12 +167,15 @@ actual fun MapView(
|
|||||||
logger.debug { "Recalculate canvas. Size: $size" }
|
logger.debug { "Recalculate canvas. Size: $size" }
|
||||||
}
|
}
|
||||||
clipRect {
|
clipRect {
|
||||||
val tileSize = IntSize(mapTileProvider.tileSize.dp.roundToPx(), mapTileProvider.tileSize.dp.roundToPx())
|
val tileSize = IntSize(
|
||||||
|
(mapTileProvider.tileSize.dp * tileScale.toFloat()).roundToPx(),
|
||||||
|
(mapTileProvider.tileSize.dp * tileScale.toFloat()).roundToPx()
|
||||||
|
)
|
||||||
mapTiles.forEach { (id, image) ->
|
mapTiles.forEach { (id, image) ->
|
||||||
//converting back from tile index to screen offset
|
//converting back from tile index to screen offset
|
||||||
val offset = IntOffset(
|
val offset = IntOffset(
|
||||||
(canvasSize.width / 2 - centerCoordinates.x.dp + mapTileProvider.toCoordinate(id.i).dp).roundToPx(),
|
(canvasSize.width / 2 + (mapTileProvider.toCoordinate(id.i).dp - centerCoordinates.x.dp) * tileScale.toFloat()).roundToPx(),
|
||||||
(canvasSize.height / 2 - centerCoordinates.y.dp + mapTileProvider.toCoordinate(id.j).dp).roundToPx()
|
(canvasSize.height / 2 + (mapTileProvider.toCoordinate(id.j).dp - centerCoordinates.y.dp) * tileScale.toFloat()).roundToPx()
|
||||||
)
|
)
|
||||||
drawImage(
|
drawImage(
|
||||||
image = image,
|
image = image,
|
||||||
|
Loading…
Reference in New Issue
Block a user