Working box by features (mostly)

This commit is contained in:
Alexander Nozik 2022-07-14 20:19:57 +03:00
parent 1541fb4f39
commit 9392c0f991
No known key found for this signature in database
GPG Key ID: F7FCF2DD25C71357
5 changed files with 89 additions and 21 deletions

View File

@ -1,5 +1,6 @@
package centre.sciprog.maps
import kotlin.math.abs
import kotlin.math.max
import kotlin.math.min
@ -22,6 +23,10 @@ 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
*/
@ -31,5 +36,5 @@ fun Iterable<GmcBox>.wrapAll(): GmcBox {
val maxLat = maxOf { it.top }
val minLong = minOf { it.left }
val maxLong = maxOf { it.right }
return GmcBox(maxLat..maxLat, minLong..maxLong)
return GmcBox(minLat..maxLat, minLong..maxLong)
}

View File

@ -15,7 +15,11 @@ interface FeatureBuilder {
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()}]"
override fun addFeature(id: FeatureId?, feature: MapFeature): FeatureId {

View File

@ -3,8 +3,11 @@ package centre.sciprog.maps.compose
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import centre.sciprog.maps.GeodeticMapCoordinates
import centre.sciprog.maps.MapViewPoint
import androidx.compose.ui.unit.DpSize
import centre.sciprog.maps.*
import kotlin.math.PI
import kotlin.math.log2
import kotlin.math.min
data class MapViewConfig(
@ -13,8 +16,8 @@ data class MapViewConfig(
@Composable
expect fun MapView(
initialViewPoint: MapViewPoint,
mapTileProvider: MapTileProvider,
computeViewPoint: (canvasSize: DpSize) -> MapViewPoint,
features: Map<FeatureId, MapFeature>,
onClick: (GeodeticMapCoordinates) -> Unit = {},
//TODO consider replacing by modifier
@ -24,14 +27,62 @@ expect fun MapView(
@Composable
fun MapView(
initialViewPoint: MapViewPoint,
mapTileProvider: MapTileProvider,
initialViewPoint: MapViewPoint,
features: Map<FeatureId, MapFeature> = emptyMap(),
onClick: (GeodeticMapCoordinates) -> Unit = {},
config: MapViewConfig = MapViewConfig(),
modifier: Modifier = Modifier.fillMaxSize(),
addFeatures: @Composable() (FeatureBuilder.() -> Unit) = {},
buildFeatures: @Composable (FeatureBuilder.() -> Unit) = {},
) {
val featuresBuilder = MapFeatureBuilder()
featuresBuilder.addFeatures()
MapView(initialViewPoint, mapTileProvider, featuresBuilder.build(), onClick, config, modifier)
val featuresBuilder = MapFeatureBuilder(features)
featuresBuilder.buildFeatures()
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)
}
/**
* Create a MapView with initial [MapViewPoint] inferred from features
*
* @param defaultZoom the zoom, for which the bounding box is computed
*/
@Composable
fun MapViewWithFeatures(
mapTileProvider: MapTileProvider,
features: Map<FeatureId, MapFeature> = emptyMap(),
onClick: (GeodeticMapCoordinates) -> Unit = {},
config: MapViewConfig = MapViewConfig(),
defaultZoom: Int = 1,
modifier: Modifier = Modifier.fillMaxSize(),
buildFeatures: @Composable (FeatureBuilder.() -> Unit),
) {
val featuresBuilder = MapFeatureBuilder(features)
featuresBuilder.buildFeatures()
val featureSet = featuresBuilder.build()
if(featureSet.isEmpty()) error("Can't create `MapViewWithFeatures` from empty feature set")
val box: GmcBox = featureSet.values.map { it.getBoundingBox(defaultZoom) }.wrapAll()
MapView(mapTileProvider, box, features, onClick, config, modifier, buildFeatures)
}

View File

@ -10,6 +10,7 @@ import androidx.compose.ui.graphics.Color
import androidx.compose.ui.window.Window
import androidx.compose.ui.window.application
import centre.sciprog.maps.GeodeticMapCoordinates
import centre.sciprog.maps.GmcBox
import centre.sciprog.maps.MapViewPoint
import centre.sciprog.maps.compose.*
import io.ktor.client.HttpClient
@ -40,7 +41,7 @@ fun App() {
Column {
//display click coordinates
Text(coordinates?.toString() ?: "")
MapView(viewPoint, mapTileProvider, onClick = { gmc: GeodeticMapCoordinates -> coordinates = gmc }) {
MapViewWithFeatures(mapTileProvider, onClick = { gmc: GeodeticMapCoordinates -> coordinates = gmc }) {
val pointOne = 55.568548 to 37.568604
val pointTwo = 55.929444 to 37.518434
@ -53,10 +54,14 @@ fun App() {
text(pointOne, "Home")
scope.launch {
while (isActive){
while (isActive) {
delay(200)
//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

@ -39,15 +39,18 @@ private val logger = KotlinLogging.logger("MapView")
@OptIn(ExperimentalComposeUiApi::class)
@Composable
actual fun MapView(
initialViewPoint: MapViewPoint,
mapTileProvider: MapTileProvider,
computeViewPoint: (canvasSize: DpSize) -> MapViewPoint,
features: Map<FeatureId, MapFeature>,
onClick: (GeodeticMapCoordinates) -> Unit,
config: MapViewConfig,
modifier: Modifier,
) {
var canvasSize by remember { mutableStateOf(DpSize(512.dp, 512.dp)) }
var viewPoint by remember { mutableStateOf(initialViewPoint) }
var viewPointOverride by remember { mutableStateOf<MapViewPoint?>(null) }
val viewPoint by derivedStateOf { viewPointOverride ?: computeViewPoint(canvasSize) }
val zoom: Int by derivedStateOf { floor(viewPoint.zoom).toInt() }
@ -55,9 +58,6 @@ actual fun MapView(
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) }
fun DpOffset.toMercator(): WebMercatorCoordinates = WebMercatorCoordinates(
@ -102,7 +102,10 @@ actual fun MapView(
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
}
} else {
@ -111,7 +114,7 @@ actual fun MapView(
onClick(dpPos.toGeodetic())
drag(change.id) { dragChange ->
val dragAmount = dragChange.position - dragChange.previousPosition
viewPoint = viewPoint.move(
viewPointOverride = viewPoint.move(
-dragAmount.x.toDp().value / tileScale,
+dragAmount.y.toDp().value / tileScale
)
@ -126,7 +129,7 @@ actual fun MapView(
val (xPos, yPos) = change.position
//compute invariant point of translation
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()