Merge TAVRIDA-MR-4: feature/boundbox

This commit is contained in:
Alexander Nozik 2022-07-15 06:26:40 +00:00 committed by Space
commit 5984de70b4
7 changed files with 149 additions and 31 deletions

View File

@ -43,6 +43,8 @@ public class GeodeticMapCoordinates private constructor(public val latitude: Dou
} }
} }
internal typealias Gmc = GeodeticMapCoordinates
//public interface GeoToScreenConversion { //public interface GeoToScreenConversion {
// public fun getScreenX(gmc: GeodeticMapCoordinates): Double // public fun getScreenX(gmc: GeodeticMapCoordinates): Double

View File

@ -0,0 +1,41 @@
package centre.sciprog.maps
import androidx.compose.ui.unit.DpSize
import centre.sciprog.maps.compose.MapFeature
import kotlin.math.*
class GmcBox(val a: GeodeticMapCoordinates, val b: GeodeticMapCoordinates)
fun GmcBox(latitudes: ClosedFloatingPointRange<Double>, longitudes: ClosedFloatingPointRange<Double>) = GmcBox(
Gmc.ofRadians(latitudes.start, longitudes.start),
Gmc.ofRadians(latitudes.endInclusive, longitudes.endInclusive)
)
val GmcBox.center
get() = GeodeticMapCoordinates.ofRadians(
(a.latitude + b.latitude) / 2,
(a.longitude + b.longitude) / 2
)
val GmcBox.left get() = min(a.longitude, b.longitude)
val GmcBox.right get() = max(a.longitude, b.longitude)
val GmcBox.top get() = max(a.latitude, b.latitude)
val GmcBox.bottom get() = min(a.latitude, b.latitude)
//TODO take curvature into account
val GmcBox.width get() = abs(a.longitude - b.longitude)
val GmcBox.height get() = abs(a.latitude - b.latitude)
/**
* Compute a minimal bounding box including all given boxes. Return null if collection is empty
*/
fun Collection<GmcBox>.wrapAll(): GmcBox? {
if (isEmpty()) return null
//TODO optimize computation
val minLat = minOf { it.bottom }
val maxLat = maxOf { it.top }
val minLong = minOf { it.left }
val maxLong = maxOf { it.right }
return GmcBox(minLat..maxLat, minLong..maxLong)
}

View File

@ -15,7 +15,11 @@ interface FeatureBuilder {
fun build(): SnapshotStateMap<FeatureId, MapFeature> fun build(): SnapshotStateMap<FeatureId, MapFeature>
} }
internal class MapFeatureBuilder(private val content: SnapshotStateMap<FeatureId, MapFeature> = mutableStateMapOf()) : FeatureBuilder { internal class MapFeatureBuilder(initialFeatures: Map<FeatureId,MapFeature>) : FeatureBuilder {
private val content: SnapshotStateMap<FeatureId, MapFeature> = mutableStateMapOf<FeatureId, MapFeature>().apply {
putAll(initialFeatures)
}
private fun generateID(feature: MapFeature): FeatureId = "@feature[${feature.hashCode().toUInt()}]" private fun generateID(feature: MapFeature): FeatureId = "@feature[${feature.hashCode().toUInt()}]"
override fun addFeature(id: FeatureId?, feature: MapFeature): FeatureId { override fun addFeature(id: FeatureId?, feature: MapFeature): FeatureId {

View File

@ -9,9 +9,15 @@ import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.graphics.vector.rememberVectorPainter import androidx.compose.ui.graphics.vector.rememberVectorPainter
import androidx.compose.ui.unit.IntSize import androidx.compose.ui.unit.IntSize
import centre.sciprog.maps.GeodeticMapCoordinates import centre.sciprog.maps.GeodeticMapCoordinates
import centre.sciprog.maps.GmcBox
import centre.sciprog.maps.wrapAll
//TODO replace zoom range with zoom-based representation change //TODO replace zoom range with zoom-based representation change
sealed class MapFeature(val zoomRange: IntRange) sealed class MapFeature(val zoomRange: IntRange) {
abstract fun getBoundingBox(zoom: Int): GmcBox
}
fun Iterable<MapFeature>.computeBoundingBox(zoom: Int): GmcBox? = map { it.getBoundingBox(zoom) }.wrapAll()
internal fun Pair<Double, Double>.toCoordinates() = GeodeticMapCoordinates.ofDegrees(first, second) internal fun Pair<Double, Double>.toCoordinates() = GeodeticMapCoordinates.ofDegrees(first, second)
@ -20,35 +26,46 @@ internal val defaultZoomRange = 1..18
/** /**
* A feature that decides what to show depending on the zoom value (it could change size of shape) * A feature that decides what to show depending on the zoom value (it could change size of shape)
*/ */
class MapFeatureSelector(val selector: (zoom: Int) -> MapFeature) : MapFeature(defaultZoomRange) class MapFeatureSelector(val selector: (zoom: Int) -> MapFeature) : MapFeature(defaultZoomRange) {
override fun getBoundingBox(zoom: Int): GmcBox = selector(zoom).getBoundingBox(zoom)
}
class MapCircleFeature( class MapCircleFeature(
val center: GeodeticMapCoordinates, val center: GeodeticMapCoordinates,
zoomRange: IntRange = defaultZoomRange, zoomRange: IntRange = defaultZoomRange,
val size: Float = 5f, val size: Float = 5f,
val color: Color = Color.Red, val color: Color = Color.Red,
) : MapFeature(zoomRange) ) : MapFeature(zoomRange) {
override fun getBoundingBox(zoom: Int): GmcBox = GmcBox(center, center)
}
class MapLineFeature( class MapLineFeature(
val a: GeodeticMapCoordinates, val a: GeodeticMapCoordinates,
val b: GeodeticMapCoordinates, val b: GeodeticMapCoordinates,
zoomRange: IntRange = defaultZoomRange, zoomRange: IntRange = defaultZoomRange,
val color: Color = Color.Red, val color: Color = Color.Red,
) : MapFeature(zoomRange) ) : MapFeature(zoomRange) {
override fun getBoundingBox(zoom: Int): GmcBox = GmcBox(a, b)
}
class MapTextFeature( class MapTextFeature(
val position: GeodeticMapCoordinates, val position: GeodeticMapCoordinates,
val text: String, val text: String,
zoomRange: IntRange = defaultZoomRange, zoomRange: IntRange = defaultZoomRange,
val color: Color = Color.Red, val color: Color = Color.Red,
) : MapFeature(zoomRange) ) : MapFeature(zoomRange) {
override fun getBoundingBox(zoom: Int): GmcBox = GmcBox(position, position)
}
class MapBitmapImageFeature( class MapBitmapImageFeature(
val position: GeodeticMapCoordinates, val position: GeodeticMapCoordinates,
val image: ImageBitmap, val image: ImageBitmap,
val size: IntSize = IntSize(15, 15), val size: IntSize = IntSize(15, 15),
zoomRange: IntRange = defaultZoomRange, zoomRange: IntRange = defaultZoomRange,
) : MapFeature(zoomRange) ) : MapFeature(zoomRange) {
override fun getBoundingBox(zoom: Int): GmcBox = GmcBox(position, position)
}
class MapVectorImageFeature ( class MapVectorImageFeature (
@ -56,7 +73,9 @@ class MapVectorImageFeature (
val painter: Painter, val painter: Painter,
val size: Size, val size: Size,
zoomRange: IntRange = defaultZoomRange, zoomRange: IntRange = defaultZoomRange,
) : MapFeature(zoomRange) ) : MapFeature(zoomRange) {
override fun getBoundingBox(zoom: Int): GmcBox = GmcBox(position, position)
}
@Composable @Composable
fun MapVectorImageFeature( fun MapVectorImageFeature(

View File

@ -3,18 +3,22 @@ package centre.sciprog.maps.compose
import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import centre.sciprog.maps.GeodeticMapCoordinates import androidx.compose.ui.unit.DpSize
import centre.sciprog.maps.MapViewPoint import centre.sciprog.maps.*
import kotlin.math.PI
import kotlin.math.log2
import kotlin.math.min
data class MapViewConfig( data class MapViewConfig(
val zoomSpeed: Double = 1.0 / 3.0, val zoomSpeed: Double = 1.0 / 3.0,
val inferViewBoxFromFeatures: Boolean = false
) )
@Composable @Composable
expect fun MapView( expect fun MapView(
initialViewPoint: MapViewPoint,
mapTileProvider: MapTileProvider, mapTileProvider: MapTileProvider,
computeViewPoint: (canvasSize: DpSize) -> MapViewPoint,
features: Map<FeatureId, MapFeature>, features: Map<FeatureId, MapFeature>,
onClick: (GeodeticMapCoordinates) -> Unit = {}, onClick: (GeodeticMapCoordinates) -> Unit = {},
//TODO consider replacing by modifier //TODO consider replacing by modifier
@ -24,14 +28,39 @@ expect fun MapView(
@Composable @Composable
fun MapView( fun MapView(
initialViewPoint: MapViewPoint,
mapTileProvider: MapTileProvider, mapTileProvider: MapTileProvider,
initialViewPoint: MapViewPoint,
features: Map<FeatureId, MapFeature> = emptyMap(),
onClick: (GeodeticMapCoordinates) -> Unit = {}, onClick: (GeodeticMapCoordinates) -> Unit = {},
config: MapViewConfig = MapViewConfig(), config: MapViewConfig = MapViewConfig(),
modifier: Modifier = Modifier.fillMaxSize(), modifier: Modifier = Modifier.fillMaxSize(),
addFeatures: @Composable() (FeatureBuilder.() -> Unit) = {}, buildFeatures: @Composable (FeatureBuilder.() -> Unit) = {},
) { ) {
val featuresBuilder = MapFeatureBuilder() val featuresBuilder = MapFeatureBuilder(features)
featuresBuilder.addFeatures() featuresBuilder.buildFeatures()
MapView(initialViewPoint, mapTileProvider, featuresBuilder.build(), onClick, config, modifier) MapView(mapTileProvider, { initialViewPoint }, featuresBuilder.build(), onClick, config, modifier)
}
@Composable
fun MapView(
mapTileProvider: MapTileProvider,
box: GmcBox,
features: Map<FeatureId, MapFeature> = emptyMap(),
onClick: (GeodeticMapCoordinates) -> Unit = {},
config: MapViewConfig = MapViewConfig(),
modifier: Modifier = Modifier.fillMaxSize(),
buildFeatures: @Composable (FeatureBuilder.() -> Unit) = {},
) {
val featuresBuilder = MapFeatureBuilder(features)
featuresBuilder.buildFeatures()
val computeViewPoint: (canvasSize: DpSize) -> MapViewPoint = { canvasSize ->
val zoom = log2(
min(
canvasSize.width.value / box.width,
canvasSize.height.value / box.height
) * PI / mapTileProvider.tileSize
)
MapViewPoint(box.center, zoom)
}
MapView(mapTileProvider, computeViewPoint, featuresBuilder.build(), onClick, config, modifier)
} }

View File

@ -40,7 +40,12 @@ fun App() {
Column { Column {
//display click coordinates //display click coordinates
Text(coordinates?.toString() ?: "") Text(coordinates?.toString() ?: "")
MapView(viewPoint, mapTileProvider, onClick = { gmc: GeodeticMapCoordinates -> coordinates = gmc }) { MapView(
mapTileProvider,
viewPoint,
onClick = { gmc -> coordinates = gmc },
config = MapViewConfig(inferViewBoxFromFeatures = true)
) {
val pointOne = 55.568548 to 37.568604 val pointOne = 55.568548 to 37.568604
val pointTwo = 55.929444 to 37.518434 val pointTwo = 55.929444 to 37.518434
@ -53,10 +58,14 @@ fun App() {
text(pointOne, "Home") text(pointOne, "Home")
scope.launch { scope.launch {
while (isActive){ while (isActive) {
delay(200) delay(200)
//Overwrite a feature with new color //Overwrite a feature with new color
circle(pointTwo, id = circleId, color = Color(Random.nextFloat(), Random.nextFloat(), Random.nextFloat())) circle(
pointTwo,
id = circleId,
color = Color(Random.nextFloat(), Random.nextFloat(), Random.nextFloat())
)
} }
} }
} }

View File

@ -21,10 +21,7 @@ import kotlinx.coroutines.launch
import mu.KotlinLogging import mu.KotlinLogging
import org.jetbrains.skia.Font import org.jetbrains.skia.Font
import org.jetbrains.skia.Paint import org.jetbrains.skia.Paint
import kotlin.math.ceil import kotlin.math.*
import kotlin.math.floor
import kotlin.math.log2
import kotlin.math.pow
private fun Color.toPaint(): Paint = Paint().apply { private fun Color.toPaint(): Paint = Paint().apply {
@ -40,15 +37,32 @@ private val logger = KotlinLogging.logger("MapView")
@OptIn(ExperimentalComposeUiApi::class) @OptIn(ExperimentalComposeUiApi::class)
@Composable @Composable
actual fun MapView( actual fun MapView(
initialViewPoint: MapViewPoint,
mapTileProvider: MapTileProvider, mapTileProvider: MapTileProvider,
computeViewPoint: (canvasSize: DpSize) -> MapViewPoint,
features: Map<FeatureId, MapFeature>, features: Map<FeatureId, MapFeature>,
onClick: (GeodeticMapCoordinates) -> Unit, onClick: (GeodeticMapCoordinates) -> Unit,
config: MapViewConfig, config: MapViewConfig,
modifier: Modifier, modifier: Modifier,
) { ) {
var canvasSize by remember { mutableStateOf(DpSize(512.dp, 512.dp)) }
var viewPoint by remember { mutableStateOf(initialViewPoint) } var viewPointOverride by remember { mutableStateOf<MapViewPoint?>(
if(config.inferViewBoxFromFeatures){
features.values.computeBoundingBox(1)?.let { box ->
val zoom = log2(
min(
canvasSize.width.value / box.width,
canvasSize.height.value / box.height
) * PI / mapTileProvider.tileSize
)
MapViewPoint(box.center, zoom)
}
} else {
null
}
) }
val viewPoint by derivedStateOf { viewPointOverride ?: computeViewPoint(canvasSize) }
val zoom: Int by derivedStateOf { floor(viewPoint.zoom).toInt() } val zoom: Int by derivedStateOf { floor(viewPoint.zoom).toInt() }
@ -56,9 +70,6 @@ actual fun MapView(
val mapTiles = remember { mutableStateListOf<MapTile>() } val mapTiles = remember { mutableStateListOf<MapTile>() }
//var mapRectangle by remember { mutableStateOf(initialRectangle) }
var canvasSize by remember { mutableStateOf(DpSize(512.dp, 512.dp)) }
val centerCoordinates by derivedStateOf { WebMercatorProjection.toMercator(viewPoint.focus, zoom) } val centerCoordinates by derivedStateOf { WebMercatorProjection.toMercator(viewPoint.focus, zoom) }
fun DpOffset.toMercator(): WebMercatorCoordinates = WebMercatorCoordinates( fun DpOffset.toMercator(): WebMercatorCoordinates = WebMercatorCoordinates(
@ -103,7 +114,10 @@ actual fun MapView(
val verticalZoom: Float = log2(canvasSize.height.toPx() / rect.height) val verticalZoom: Float = log2(canvasSize.height.toPx() / rect.height)
viewPoint = MapViewPoint(centerGmc, viewPoint.zoom + kotlin.math.min(verticalZoom, horizontalZoom)) viewPointOverride = MapViewPoint(
centerGmc,
viewPoint.zoom + kotlin.math.min(verticalZoom, horizontalZoom)
)
selectRect = null selectRect = null
} }
} else { } else {
@ -112,7 +126,7 @@ actual fun MapView(
onClick(dpPos.toGeodetic()) onClick(dpPos.toGeodetic())
drag(change.id) { dragChange -> drag(change.id) { dragChange ->
val dragAmount = dragChange.position - dragChange.previousPosition val dragAmount = dragChange.position - dragChange.previousPosition
viewPoint = viewPoint.move( viewPointOverride = viewPoint.move(
-dragAmount.x.toDp().value / tileScale, -dragAmount.x.toDp().value / tileScale,
+dragAmount.y.toDp().value / tileScale +dragAmount.y.toDp().value / tileScale
) )
@ -127,7 +141,7 @@ actual fun MapView(
val (xPos, yPos) = change.position val (xPos, yPos) = change.position
//compute invariant point of translation //compute invariant point of translation
val invariant = DpOffset(xPos.toDp(), yPos.toDp()).toGeodetic() val invariant = DpOffset(xPos.toDp(), yPos.toDp()).toGeodetic()
viewPoint = viewPoint.zoom(-change.scrollDelta.y.toDouble() * config.zoomSpeed, invariant) viewPointOverride = viewPoint.zoom(-change.scrollDelta.y.toDouble() * config.zoomSpeed, invariant)
}.fillMaxSize() }.fillMaxSize()