Full refacor of map state and arguments
This commit is contained in:
parent
56ccd66db5
commit
9e3eec9533
@ -2,8 +2,9 @@ import androidx.compose.desktop.ui.tooling.preview.Preview
|
||||
import androidx.compose.material.MaterialTheme
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.Home
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.ui.geometry.Offset
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.rememberCoroutineScope
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.graphics.PointMode
|
||||
import androidx.compose.ui.unit.DpSize
|
||||
@ -17,6 +18,10 @@ import center.sciprog.maps.geojson.geoJson
|
||||
import io.ktor.client.HttpClient
|
||||
import io.ktor.client.engine.cio.CIO
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.filterNotNull
|
||||
import kotlinx.coroutines.flow.launchIn
|
||||
import kotlinx.coroutines.flow.onEach
|
||||
import java.net.URL
|
||||
import java.nio.file.Path
|
||||
import kotlin.math.PI
|
||||
@ -32,6 +37,7 @@ fun App() {
|
||||
MaterialTheme {
|
||||
|
||||
val scope = rememberCoroutineScope()
|
||||
|
||||
val mapTileProvider = remember {
|
||||
OpenStreetMapTileProvider(
|
||||
client = HttpClient(CIO),
|
||||
@ -39,7 +45,7 @@ fun App() {
|
||||
)
|
||||
}
|
||||
|
||||
var centerCoordinates by remember { mutableStateOf<Gmc?>(null) }
|
||||
val centerCoordinates = MutableStateFlow<Gmc?>(null)
|
||||
|
||||
|
||||
val pointOne = 55.568548 to 37.568604
|
||||
@ -48,17 +54,9 @@ fun App() {
|
||||
|
||||
MapView(
|
||||
mapTileProvider = mapTileProvider,
|
||||
// initialViewPoint = MapViewPoint(
|
||||
// GeodeticMapCoordinates.ofDegrees(55.7558, 37.6173),
|
||||
// 8.0
|
||||
// ),
|
||||
// initialRectangle = GmcRectangle.square(
|
||||
// GeodeticMapCoordinates.ofDegrees(55.7558, 37.6173),
|
||||
// 50.kilometers,
|
||||
// 50.kilometers
|
||||
// ),
|
||||
config = ViewConfig(
|
||||
onViewChange = { centerCoordinates = focus },
|
||||
onViewChange = { centerCoordinates.value = focus },
|
||||
onClick = { _, viewPoint -> println(viewPoint) }
|
||||
)
|
||||
) {
|
||||
|
||||
@ -95,22 +93,22 @@ fun App() {
|
||||
it.copy(color = Color(Random.nextFloat(), Random.nextFloat(), Random.nextFloat()))
|
||||
}
|
||||
|
||||
draw(position = pointThree) {
|
||||
drawLine(start = Offset(-10f, -10f), end = Offset(10f, 10f), color = Color.Red)
|
||||
drawLine(start = Offset(-10f, 10f), end = Offset(10f, -10f), color = Color.Red)
|
||||
}
|
||||
// draw(position = pointThree) {
|
||||
// drawLine(start = Offset(-10f, -10f), end = Offset(10f, 10f), color = Color.Red)
|
||||
// drawLine(start = Offset(-10f, 10f), end = Offset(10f, -10f), color = Color.Red)
|
||||
// }
|
||||
|
||||
arc(pointOne, 10.0.kilometers, (PI / 4).radians, -Angle.pi / 2)
|
||||
|
||||
line(pointOne, pointTwo, id = "line")
|
||||
text(pointOne, "Home", font = { size = 32f })
|
||||
|
||||
centerCoordinates?.let {
|
||||
centerCoordinates.filterNotNull().onEach {
|
||||
group(id = "center") {
|
||||
circle(center = it, color = Color.Blue, id = "circle", size = 1.dp)
|
||||
text(position = it, it.toShortString(), id = "text", color = Color.Blue)
|
||||
}
|
||||
}
|
||||
}.launchIn(scope)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -79,15 +79,20 @@ fun App() {
|
||||
)
|
||||
}
|
||||
) {
|
||||
SchemeView(
|
||||
initialViewPoint = initialViewPoint,
|
||||
featuresState = schemeFeaturesState,
|
||||
config = ViewConfig(
|
||||
onClick = {
|
||||
println("${focus.x}, ${focus.y}")
|
||||
val mapState: XYViewScope = rememberMapState(
|
||||
ViewConfig(
|
||||
onClick = {_, click ->
|
||||
println("${click.focus.x}, ${click.focus.y}")
|
||||
},
|
||||
onViewChange = { viewPoint = this }
|
||||
),
|
||||
schemeFeaturesState.features.values,
|
||||
initialViewPoint = initialViewPoint,
|
||||
)
|
||||
|
||||
SchemeView(
|
||||
mapState,
|
||||
schemeFeaturesState,
|
||||
)
|
||||
}
|
||||
|
||||
|
@ -21,9 +21,9 @@ public interface MapTileProvider {
|
||||
|
||||
public val tileSize: Int get() = DEFAULT_TILE_SIZE
|
||||
|
||||
public fun toIndex(d: Double): Int = floor(d / tileSize).toInt()
|
||||
public fun toIndex(d: Float): Int = floor(d / tileSize).toInt()
|
||||
|
||||
public fun toCoordinate(i: Int): Double = (i * tileSize).toDouble()
|
||||
public fun toCoordinate(i: Int): Float = (i * tileSize).toFloat()
|
||||
|
||||
public companion object {
|
||||
public const val DEFAULT_TILE_SIZE: Int = 256
|
||||
|
@ -2,42 +2,19 @@ package center.sciprog.maps.compose
|
||||
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.key
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.unit.DpSize
|
||||
import androidx.compose.ui.unit.dp
|
||||
import center.sciprog.maps.coordinates.Gmc
|
||||
import center.sciprog.maps.features.*
|
||||
import kotlin.math.PI
|
||||
import kotlin.math.log2
|
||||
import kotlin.math.min
|
||||
|
||||
|
||||
@Composable
|
||||
public expect fun MapView(
|
||||
mapTileProvider: MapTileProvider,
|
||||
initialViewPoint: MapViewPoint,
|
||||
mapState: MapViewScope,
|
||||
featuresState: FeatureCollection<Gmc>,
|
||||
config: ViewConfig<Gmc> = ViewConfig(),
|
||||
modifier: Modifier = Modifier.fillMaxSize(),
|
||||
)
|
||||
|
||||
internal val defaultCanvasSize = DpSize(512.dp, 512.dp)
|
||||
|
||||
public fun Rectangle<Gmc>.computeViewPoint(
|
||||
mapTileProvider: MapTileProvider,
|
||||
canvasSize: DpSize = defaultCanvasSize,
|
||||
): MapViewPoint {
|
||||
val zoom = log2(
|
||||
min(
|
||||
canvasSize.width.value / longitudeDelta.radians.value,
|
||||
canvasSize.height.value / latitudeDelta.radians.value
|
||||
) * PI / mapTileProvider.tileSize
|
||||
)
|
||||
return MapViewPoint(center, zoom.toFloat())
|
||||
}
|
||||
|
||||
/**
|
||||
* A builder for a Map with static features.
|
||||
*/
|
||||
@ -50,20 +27,22 @@ public fun MapView(
|
||||
config: ViewConfig<Gmc> = ViewConfig(),
|
||||
modifier: Modifier = Modifier.fillMaxSize(),
|
||||
) {
|
||||
val featuresState = key(featureMap) {
|
||||
FeatureCollection.build(GmcCoordinateSpace) {
|
||||
|
||||
val featuresState = remember(featureMap) {
|
||||
FeatureCollection.build(WebMercatorSpace) {
|
||||
featureMap.forEach { feature(it.key.id, it.value) }
|
||||
}
|
||||
}
|
||||
|
||||
val viewPointOverride: MapViewPoint = remember(initialViewPoint, initialRectangle) {
|
||||
initialViewPoint
|
||||
?: initialRectangle?.computeViewPoint(mapTileProvider)
|
||||
?: featureMap.values.computeBoundingBox(GmcCoordinateSpace, 1f)?.computeViewPoint(mapTileProvider)
|
||||
?: MapViewPoint.globe
|
||||
}
|
||||
val mapState: MapViewScope = rememberMapState(
|
||||
mapTileProvider,
|
||||
config,
|
||||
featuresState.features.values,
|
||||
initialViewPoint = initialViewPoint,
|
||||
initialRectangle = initialRectangle,
|
||||
)
|
||||
|
||||
MapView(mapTileProvider, viewPointOverride, featuresState, config, modifier)
|
||||
MapView(mapState, featuresState, modifier)
|
||||
}
|
||||
|
||||
/**
|
||||
@ -82,15 +61,15 @@ public fun MapView(
|
||||
modifier: Modifier = Modifier.fillMaxSize(),
|
||||
buildFeatures: FeatureCollection<Gmc>.() -> Unit = {},
|
||||
) {
|
||||
val featureState = FeatureCollection.remember(GmcCoordinateSpace, buildFeatures)
|
||||
|
||||
val viewPointOverride: MapViewPoint = remember(initialViewPoint, initialRectangle) {
|
||||
initialViewPoint
|
||||
?: initialRectangle?.computeViewPoint(mapTileProvider)
|
||||
?: featureState.features.values.computeBoundingBox(GmcCoordinateSpace, 1f)
|
||||
?.computeViewPoint(mapTileProvider)
|
||||
?: MapViewPoint.globe
|
||||
}
|
||||
val featureState = FeatureCollection.remember(WebMercatorSpace, buildFeatures)
|
||||
val mapState: MapViewScope = rememberMapState(
|
||||
mapTileProvider,
|
||||
config,
|
||||
featureState.features.values,
|
||||
initialViewPoint = initialViewPoint,
|
||||
initialRectangle = initialRectangle,
|
||||
)
|
||||
|
||||
val featureDrag: DragHandle<Gmc> = DragHandle.withPrimaryButton { event, start, end ->
|
||||
featureState.forEachWithAttribute(DraggableAttribute) { _, handle ->
|
||||
@ -107,16 +86,18 @@ public fun MapView(
|
||||
DragResult(end)
|
||||
}
|
||||
|
||||
val featureClick: ClickHandle<Gmc> = ClickHandle.withPrimaryButton { event, click ->
|
||||
featureState.forEachWithAttribute(SelectableAttribute) { _, handle ->
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
(handle as ClickHandle<Gmc>).handle(event, click)
|
||||
config.onClick?.handle(event, click)
|
||||
}
|
||||
}
|
||||
|
||||
val newConfig = config.copy(
|
||||
dragHandle = DragHandle.combine(featureDrag, config.dragHandle)
|
||||
dragHandle = config.dragHandle?.let { DragHandle.combine(featureDrag, it) } ?: featureDrag,
|
||||
onClick = featureClick
|
||||
)
|
||||
|
||||
MapView(
|
||||
mapTileProvider = mapTileProvider,
|
||||
initialViewPoint = viewPointOverride,
|
||||
featuresState = featureState,
|
||||
config = newConfig,
|
||||
modifier = modifier,
|
||||
)
|
||||
MapView(mapState, featureState, modifier)
|
||||
}
|
@ -11,7 +11,7 @@ public data class MapViewPoint(
|
||||
override val focus: GeodeticMapCoordinates,
|
||||
override val zoom: Float,
|
||||
) : ViewPoint<Gmc>{
|
||||
val scaleFactor: Double by lazy { WebMercatorProjection.scaleFactor(zoom) }
|
||||
val scaleFactor: Float by lazy { WebMercatorProjection.scaleFactor(zoom) }
|
||||
|
||||
public companion object{
|
||||
public val globe: MapViewPoint = MapViewPoint(GeodeticMapCoordinates(0.0.radians, 0.0.radians), 1f)
|
||||
|
@ -2,29 +2,30 @@ package center.sciprog.maps.compose
|
||||
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.unit.*
|
||||
import androidx.compose.ui.unit.Dp
|
||||
import androidx.compose.ui.unit.DpOffset
|
||||
import androidx.compose.ui.unit.DpRect
|
||||
import androidx.compose.ui.unit.dp
|
||||
import center.sciprog.maps.coordinates.*
|
||||
import center.sciprog.maps.features.*
|
||||
import kotlin.math.*
|
||||
|
||||
internal class MapViewState internal constructor(
|
||||
public class MapViewScope internal constructor(
|
||||
public val mapTileProvider: MapTileProvider,
|
||||
config: ViewConfig<Gmc>,
|
||||
canvasSize: DpSize,
|
||||
viewPoint: ViewPoint<Gmc>,
|
||||
val tileSize: Int,
|
||||
) : CoordinateViewState<Gmc>(config, canvasSize, viewPoint) {
|
||||
override val space: CoordinateSpace<Gmc> get() = GmcCoordinateSpace
|
||||
) : CoordinateViewScope<Gmc>(config) {
|
||||
override val space: CoordinateSpace<Gmc> get() = WebMercatorSpace
|
||||
|
||||
val scaleFactor: Double
|
||||
get() = WebMercatorProjection.scaleFactor(viewPoint.zoom)
|
||||
public val scaleFactor: Float
|
||||
get() = WebMercatorProjection.scaleFactor(zoom)
|
||||
|
||||
val intZoom: Int get() = floor(zoom).toInt()
|
||||
public val intZoom: Int get() = floor(zoom).toInt()
|
||||
|
||||
val centerCoordinates: WebMercatorCoordinates
|
||||
public val centerCoordinates: WebMercatorCoordinates
|
||||
get() = WebMercatorProjection.toMercator(viewPoint.focus, intZoom)
|
||||
|
||||
val tileScale: Float
|
||||
get() = 2f.pow(viewPoint.zoom - floor(viewPoint.zoom))
|
||||
public val tileScale: Float
|
||||
get() = 2f.pow(zoom - floor(zoom))
|
||||
|
||||
/*
|
||||
* Convert screen independent offset to GMC, adjusting for fractional zoom
|
||||
@ -40,9 +41,9 @@ internal class MapViewState internal constructor(
|
||||
|
||||
override fun Gmc.toDpOffset(): DpOffset {
|
||||
val mercator = WebMercatorProjection.toMercator(this, intZoom)
|
||||
return DpOffset(
|
||||
(canvasSize.width / 2 + (mercator.x.dp - centerCoordinates.x.dp) * tileScale.toFloat()),
|
||||
(canvasSize.height / 2 + (mercator.y.dp - centerCoordinates.y.dp) * tileScale.toFloat())
|
||||
return DpOffset(
|
||||
(canvasSize.width / 2 + (mercator.x.dp - centerCoordinates.x.dp) * tileScale),
|
||||
(canvasSize.height / 2 + (mercator.y.dp - centerCoordinates.y.dp) * tileScale)
|
||||
)
|
||||
}
|
||||
|
||||
@ -52,12 +53,12 @@ internal class MapViewState internal constructor(
|
||||
return DpRect(topLeft.x, topLeft.y, bottomRight.x, bottomRight.y)
|
||||
}
|
||||
|
||||
override fun viewPointFor(rectangle: Rectangle<Gmc>): ViewPoint<Gmc> {
|
||||
override fun computeViewPoint(rectangle: Rectangle<Gmc>): ViewPoint<Gmc> {
|
||||
val zoom = log2(
|
||||
min(
|
||||
canvasSize.width.value / rectangle.longitudeDelta.radians.value,
|
||||
canvasSize.height.value / rectangle.latitudeDelta.radians.value
|
||||
) * PI / tileSize
|
||||
) * PI / mapTileProvider.tileSize
|
||||
)
|
||||
return MapViewPoint(rectangle.center, zoom.toFloat())
|
||||
}
|
||||
@ -78,10 +79,21 @@ internal class MapViewState internal constructor(
|
||||
|
||||
@Composable
|
||||
internal fun rememberMapState(
|
||||
mapTileProvider: MapTileProvider,
|
||||
config: ViewConfig<Gmc>,
|
||||
canvasSize: DpSize,
|
||||
viewPoint: ViewPoint<Gmc>,
|
||||
tileSize: Int,
|
||||
): MapViewState = remember {
|
||||
MapViewState(config, canvasSize, viewPoint, tileSize)
|
||||
features: Collection<Feature<Gmc>> = emptyList(),
|
||||
initialViewPoint: MapViewPoint? = null,
|
||||
initialRectangle: Rectangle<Gmc>? = null,
|
||||
): MapViewScope = remember {
|
||||
MapViewScope(mapTileProvider, config).also { mapState->
|
||||
if (initialViewPoint != null) {
|
||||
mapState.viewPoint = initialViewPoint
|
||||
} else if (initialRectangle != null) {
|
||||
mapState.viewPoint = mapState.computeViewPoint(initialRectangle)
|
||||
} else {
|
||||
features.computeBoundingBox(WebMercatorSpace, 1f)?.let {
|
||||
mapState.viewPoint = mapState.computeViewPoint(it)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -1,13 +1,21 @@
|
||||
package center.sciprog.maps.compose
|
||||
|
||||
import androidx.compose.ui.unit.DpOffset
|
||||
import androidx.compose.ui.unit.DpSize
|
||||
import androidx.compose.ui.unit.dp
|
||||
import center.sciprog.maps.coordinates.*
|
||||
import center.sciprog.maps.features.CoordinateSpace
|
||||
import center.sciprog.maps.features.Rectangle
|
||||
import center.sciprog.maps.features.ViewPoint
|
||||
import kotlin.math.floor
|
||||
import kotlin.math.pow
|
||||
|
||||
public object GmcCoordinateSpace : CoordinateSpace<Gmc> {
|
||||
public object WebMercatorSpace : CoordinateSpace<Gmc> {
|
||||
|
||||
private fun intZoom(zoom: Float): Int = floor(zoom).toInt()
|
||||
private fun tileScale(zoom: Float): Float = 2f.pow(zoom - floor(zoom))
|
||||
|
||||
|
||||
override fun Rectangle(first: Gmc, second: Gmc): Rectangle<Gmc> = GmcRectangle(first, second)
|
||||
|
||||
override fun Rectangle(center: Gmc, zoom: Float, size: DpSize): Rectangle<Gmc> {
|
||||
@ -15,6 +23,8 @@ public object GmcCoordinateSpace : CoordinateSpace<Gmc> {
|
||||
return Rectangle(center, (size.width.value / scale).radians, (size.height.value / scale).radians)
|
||||
}
|
||||
|
||||
override val defaultViewPoint: ViewPoint<Gmc> = MapViewPoint.globe
|
||||
|
||||
override fun ViewPoint(center: Gmc, zoom: Float): ViewPoint<Gmc> = MapViewPoint(center, zoom)
|
||||
|
||||
override fun ViewPoint<Gmc>.moveBy(delta: Gmc): ViewPoint<Gmc> {
|
||||
@ -29,7 +39,7 @@ public object GmcCoordinateSpace : CoordinateSpace<Gmc> {
|
||||
}
|
||||
|
||||
override fun ViewPoint<Gmc>.zoomBy(zoomDelta: Float, invariant: Gmc): ViewPoint<Gmc> = if (invariant == focus) {
|
||||
ViewPoint(focus, (zoom + zoomDelta).coerceIn(2f, 18f) )
|
||||
ViewPoint(focus, (zoom + zoomDelta).coerceIn(2f, 18f))
|
||||
} else {
|
||||
val difScale = (1 - 2f.pow(-zoomDelta))
|
||||
val newCenter = GeodeticMapCoordinates(
|
||||
@ -61,6 +71,17 @@ public object GmcCoordinateSpace : CoordinateSpace<Gmc> {
|
||||
val maxLong = maxOf { it.longitude }
|
||||
return GmcRectangle(GeodeticMapCoordinates(minLat, minLong), GeodeticMapCoordinates(maxLat, maxLong))
|
||||
}
|
||||
|
||||
override fun Gmc.offsetTo(b: Gmc, zoom: Float): DpOffset {
|
||||
val intZoom = intZoom(zoom)
|
||||
val mercatorA = WebMercatorProjection.toMercator(this, intZoom)
|
||||
val mercatorB = WebMercatorProjection.toMercator(b, intZoom)
|
||||
val tileScale = tileScale(zoom)
|
||||
return DpOffset(
|
||||
(mercatorA.x - mercatorB.x).dp * tileScale,
|
||||
(mercatorA.y - mercatorB.y).dp * tileScale
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
@ -15,7 +15,10 @@ import androidx.compose.ui.unit.IntOffset
|
||||
import androidx.compose.ui.unit.IntSize
|
||||
import androidx.compose.ui.unit.dp
|
||||
import center.sciprog.maps.coordinates.Gmc
|
||||
import center.sciprog.maps.features.*
|
||||
import center.sciprog.maps.features.FeatureCollection
|
||||
import center.sciprog.maps.features.PainterFeature
|
||||
import center.sciprog.maps.features.drawFeature
|
||||
import center.sciprog.maps.features.z
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.supervisorScope
|
||||
import mu.KotlinLogging
|
||||
@ -40,111 +43,101 @@ private val logger = KotlinLogging.logger("MapView")
|
||||
*/
|
||||
@Composable
|
||||
public actual fun MapView(
|
||||
mapTileProvider: MapTileProvider,
|
||||
initialViewPoint: MapViewPoint,
|
||||
mapState: MapViewScope,
|
||||
featuresState: FeatureCollection<Gmc>,
|
||||
config: ViewConfig<Gmc>,
|
||||
modifier: Modifier,
|
||||
): Unit = key(initialViewPoint) {
|
||||
): Unit = with(mapState) {
|
||||
|
||||
val state = rememberMapState(
|
||||
config,
|
||||
defaultCanvasSize,
|
||||
initialViewPoint,
|
||||
mapTileProvider.tileSize
|
||||
)
|
||||
val mapTiles = remember(mapTileProvider) { mutableStateListOf<MapTile>() }
|
||||
|
||||
with(state) {
|
||||
// Load tiles asynchronously
|
||||
LaunchedEffect(viewPoint, canvasSize) {
|
||||
with(mapTileProvider) {
|
||||
val indexRange = 0 until 2.0.pow(intZoom).toInt()
|
||||
|
||||
val mapTiles = remember(mapTileProvider) { mutableStateListOf<MapTile>() }
|
||||
val left = centerCoordinates.x - canvasSize.width.value / 2 / tileScale
|
||||
val right = centerCoordinates.x + canvasSize.width.value / 2 / tileScale
|
||||
val horizontalIndices: IntRange = (toIndex(left)..toIndex(right)).intersect(indexRange)
|
||||
|
||||
// Load tiles asynchronously
|
||||
LaunchedEffect(viewPoint, canvasSize) {
|
||||
with(mapTileProvider) {
|
||||
val indexRange = 0 until 2.0.pow(intZoom).toInt()
|
||||
val top = (centerCoordinates.y + canvasSize.height.value / 2 / tileScale)
|
||||
val bottom = (centerCoordinates.y - canvasSize.height.value / 2 / tileScale)
|
||||
val verticalIndices: IntRange = (toIndex(bottom)..toIndex(top)).intersect(indexRange)
|
||||
|
||||
val left = centerCoordinates.x - canvasSize.width.value / 2 / tileScale
|
||||
val right = centerCoordinates.x + canvasSize.width.value / 2 / tileScale
|
||||
val horizontalIndices: IntRange = (toIndex(left)..toIndex(right)).intersect(indexRange)
|
||||
mapTiles.clear()
|
||||
|
||||
val top = (centerCoordinates.y + canvasSize.height.value / 2 / tileScale)
|
||||
val bottom = (centerCoordinates.y - canvasSize.height.value / 2 / tileScale)
|
||||
val verticalIndices: IntRange = (toIndex(bottom)..toIndex(top)).intersect(indexRange)
|
||||
|
||||
mapTiles.clear()
|
||||
|
||||
for (j in verticalIndices) {
|
||||
for (i in horizontalIndices) {
|
||||
val id = TileId(intZoom, i, j)
|
||||
//ensure that failed tiles do not fail the application
|
||||
supervisorScope {
|
||||
//start all
|
||||
val deferred = loadTileAsync(id)
|
||||
//wait asynchronously for it to finish
|
||||
launch {
|
||||
try {
|
||||
mapTiles += deferred.await()
|
||||
} catch (ex: Exception) {
|
||||
//displaying the error is maps responsibility
|
||||
logger.error(ex) { "Failed to load tile with id=$id" }
|
||||
}
|
||||
for (j in verticalIndices) {
|
||||
for (i in horizontalIndices) {
|
||||
val id = TileId(intZoom, i, j)
|
||||
//ensure that failed tiles do not fail the application
|
||||
supervisorScope {
|
||||
//start all
|
||||
val deferred = loadTileAsync(id)
|
||||
//wait asynchronously for it to finish
|
||||
launch {
|
||||
try {
|
||||
mapTiles += deferred.await()
|
||||
} catch (ex: Exception) {
|
||||
//displaying the error is maps responsibility
|
||||
logger.error(ex) { "Failed to load tile with id=$id" }
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
val painterCache: Map<PainterFeature<Gmc>, Painter> = key(featuresState) {
|
||||
featuresState.features.values.filterIsInstance<PainterFeature<Gmc>>().associateWith { it.getPainter() }
|
||||
}
|
||||
|
||||
Canvas(modifier = modifier.mapControls(state).fillMaxSize()) {
|
||||
|
||||
if (canvasSize != size.toDpSize()) {
|
||||
logger.debug { "Recalculate canvas. Size: $size" }
|
||||
config.onCanvasSizeChange(canvasSize)
|
||||
canvasSize = size.toDpSize()
|
||||
}
|
||||
|
||||
clipRect {
|
||||
val tileSize = IntSize(
|
||||
ceil((mapTileProvider.tileSize.dp * tileScale).toPx()).toInt(),
|
||||
ceil((mapTileProvider.tileSize.dp * tileScale).toPx()).toInt()
|
||||
)
|
||||
mapTiles.forEach { (id, image) ->
|
||||
//converting back from tile index to screen offset
|
||||
val offset = IntOffset(
|
||||
(canvasSize.width / 2 + (mapTileProvider.toCoordinate(id.i).dp - centerCoordinates.x.dp) * tileScale).roundToPx(),
|
||||
(canvasSize.height / 2 + (mapTileProvider.toCoordinate(id.j).dp - centerCoordinates.y.dp) * tileScale).roundToPx()
|
||||
)
|
||||
drawImage(
|
||||
image = image.toComposeImageBitmap(),
|
||||
dstOffset = offset,
|
||||
dstSize = tileSize
|
||||
)
|
||||
}
|
||||
|
||||
featuresState.features.values.filter { viewPoint.zoom in it.zoomRange }.sortedBy { it.z }.forEach { feature ->
|
||||
drawFeature(state, painterCache, feature)
|
||||
}
|
||||
}
|
||||
|
||||
selectRect?.let { dpRect ->
|
||||
val rect = dpRect.toRect()
|
||||
drawRect(
|
||||
color = Color.Blue,
|
||||
topLeft = rect.topLeft,
|
||||
size = rect.size,
|
||||
alpha = 0.5f,
|
||||
style = Stroke(
|
||||
width = 2f,
|
||||
pathEffect = PathEffect.dashPathEffect(floatArrayOf(10f, 10f), 0f)
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
val painterCache: Map<PainterFeature<Gmc>, Painter> = key(featuresState) {
|
||||
featuresState.features.values.filterIsInstance<PainterFeature<Gmc>>().associateWith { it.getPainter() }
|
||||
}
|
||||
|
||||
Canvas(modifier = modifier.mapControls(mapState).fillMaxSize()) {
|
||||
|
||||
if (canvasSize != size.toDpSize()) {
|
||||
logger.debug { "Recalculate canvas. Size: $size" }
|
||||
config.onCanvasSizeChange(canvasSize)
|
||||
canvasSize = size.toDpSize()
|
||||
}
|
||||
|
||||
clipRect {
|
||||
val tileSize = IntSize(
|
||||
ceil((mapTileProvider.tileSize.dp * tileScale).toPx()).toInt(),
|
||||
ceil((mapTileProvider.tileSize.dp * tileScale).toPx()).toInt()
|
||||
)
|
||||
mapTiles.forEach { (id, image) ->
|
||||
//converting back from tile index to screen offset
|
||||
val offset = IntOffset(
|
||||
(canvasSize.width / 2 + (mapTileProvider.toCoordinate(id.i).dp - centerCoordinates.x.dp) * tileScale).roundToPx(),
|
||||
(canvasSize.height / 2 + (mapTileProvider.toCoordinate(id.j).dp - centerCoordinates.y.dp) * tileScale).roundToPx()
|
||||
)
|
||||
drawImage(
|
||||
image = image.toComposeImageBitmap(),
|
||||
dstOffset = offset,
|
||||
dstSize = tileSize
|
||||
)
|
||||
}
|
||||
|
||||
featuresState.features.values.filter { viewPoint.zoom in it.zoomRange }.sortedBy { it.z }
|
||||
.forEach { feature ->
|
||||
drawFeature(mapState, painterCache, feature)
|
||||
}
|
||||
}
|
||||
|
||||
selectRect?.let { dpRect ->
|
||||
val rect = dpRect.toRect()
|
||||
drawRect(
|
||||
color = Color.Blue,
|
||||
topLeft = rect.topLeft,
|
||||
size = rect.size,
|
||||
alpha = 0.5f,
|
||||
style = Stroke(
|
||||
width = 2f,
|
||||
pathEffect = PathEffect.dashPathEffect(floatArrayOf(10f, 10f), 0f)
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -7,14 +7,14 @@ package center.sciprog.maps.coordinates
|
||||
|
||||
import kotlin.math.*
|
||||
|
||||
public data class WebMercatorCoordinates(val zoom: Int, val x: Double, val y: Double)
|
||||
public data class WebMercatorCoordinates(val zoom: Int, val x: Float, val y: Float)
|
||||
|
||||
public object WebMercatorProjection {
|
||||
|
||||
/**
|
||||
* Compute radians to projection coordinates ratio for given [zoom] factor
|
||||
*/
|
||||
public fun scaleFactor(zoom: Float): Double = 256.0 / 2 / PI * 2f.pow(zoom)
|
||||
public fun scaleFactor(zoom: Float): Float = (256.0 / 2 / PI * 2f.pow(zoom)).toFloat()
|
||||
|
||||
public fun toGeodetic(mercator: WebMercatorCoordinates): GeodeticMapCoordinates {
|
||||
val scaleFactor = scaleFactor(mercator.zoom.toFloat())
|
||||
@ -32,8 +32,8 @@ public object WebMercatorProjection {
|
||||
val scaleFactor = scaleFactor(zoom.toFloat())
|
||||
return WebMercatorCoordinates(
|
||||
zoom = zoom,
|
||||
x = scaleFactor * (gmc.longitude.radians.value + PI),
|
||||
y = scaleFactor * (PI - ln(tan(PI / 4 + gmc.latitude.radians.value / 2)))
|
||||
x = scaleFactor * (gmc.longitude.radians.value + PI).toFloat(),
|
||||
y = scaleFactor * (PI - ln(tan(PI / 4 + gmc.latitude.radians.value / 2))).toFloat()
|
||||
)
|
||||
}
|
||||
|
||||
|
@ -5,7 +5,11 @@ import androidx.compose.ui.graphics.Color
|
||||
|
||||
public object ZAttribute : Feature.Attribute<Float>
|
||||
|
||||
public object SelectableAttribute : Feature.Attribute<(FeatureId<*>, SelectableFeature<*>) -> Unit>
|
||||
public object DraggableAttribute : Feature.Attribute<DragHandle<Any>>
|
||||
|
||||
public object DragListenerAttribute : Feature.Attribute<Set<(begin: Any, end: Any) -> Unit>>
|
||||
|
||||
public object SelectableAttribute : Feature.Attribute<ClickHandle<Any>>
|
||||
|
||||
public object VisibleAttribute : Feature.Attribute<Boolean>
|
||||
|
||||
|
@ -1,6 +1,11 @@
|
||||
package center.sciprog.maps.features
|
||||
|
||||
import androidx.compose.ui.unit.Dp
|
||||
import androidx.compose.ui.unit.DpOffset
|
||||
import androidx.compose.ui.unit.DpSize
|
||||
import androidx.compose.ui.unit.dp
|
||||
import kotlin.math.abs
|
||||
import kotlin.math.sqrt
|
||||
|
||||
|
||||
public interface Area<T : Any> {
|
||||
@ -31,6 +36,11 @@ public interface CoordinateSpace<T : Any> {
|
||||
*/
|
||||
public fun Rectangle(center: T, zoom: Float, size: DpSize): Rectangle<T>
|
||||
|
||||
/**
|
||||
* A view point used by default
|
||||
*/
|
||||
public val defaultViewPoint: ViewPoint<T>
|
||||
|
||||
/**
|
||||
* Create a [ViewPoint] associated with this coordinate space.
|
||||
*/
|
||||
@ -52,6 +62,20 @@ public interface CoordinateSpace<T : Any> {
|
||||
|
||||
public fun Collection<T>.wrapPoints(): Rectangle<T>?
|
||||
|
||||
public fun T.offsetTo(b: T, zoom: Float): DpOffset
|
||||
|
||||
public fun T.distanceTo(b: T, zoom: Float): Dp {
|
||||
val offset = offsetTo(b, zoom)
|
||||
return sqrt(offset.x.value * offset.x.value + offset.y.value * offset.y.value).dp
|
||||
}
|
||||
|
||||
public fun T.distanceToLine(a: T, b: T, zoom: Float): Dp {
|
||||
val d12 = a.offsetTo(b, zoom)
|
||||
val d01 = offsetTo(a, zoom)
|
||||
val distanceVale = abs(d12.x.value * d01.y.value - d12.y.value * d01.x.value) / a.distanceTo(b, zoom).value
|
||||
|
||||
return distanceVale.dp
|
||||
}
|
||||
}
|
||||
|
||||
public fun <T : Any> CoordinateSpace<T>.Rectangle(viewPoint: ViewPoint<T>, size: DpSize): Rectangle<T> =
|
||||
|
@ -6,32 +6,43 @@ import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.geometry.Offset
|
||||
import androidx.compose.ui.unit.*
|
||||
import kotlin.math.pow
|
||||
import kotlin.math.sqrt
|
||||
|
||||
public abstract class CoordinateViewState<T : Any>(
|
||||
private fun distanceBetween(a: DpOffset, b: DpOffset): Dp = sqrt((b.x - a.x).value.pow(2) + (b.y - a.y).value.pow(2)).dp
|
||||
|
||||
public abstract class CoordinateViewScope<T : Any>(
|
||||
public val config: ViewConfig<T>,
|
||||
canvasSize: DpSize,
|
||||
viewPoint: ViewPoint<T>,
|
||||
) {
|
||||
|
||||
public abstract val space: CoordinateSpace<T>
|
||||
|
||||
public var canvasSize: DpSize by mutableStateOf(canvasSize)
|
||||
protected var viewPointState: MutableState<ViewPoint<T>> = mutableStateOf(viewPoint)
|
||||
protected var canvasSizeState: MutableState<DpSize?> = mutableStateOf(null)
|
||||
protected var viewPointState: MutableState<ViewPoint<T>?> = mutableStateOf(null)
|
||||
|
||||
public var canvasSize: DpSize
|
||||
get() = canvasSizeState.value ?: DpSize(512.dp, 512.dp)
|
||||
set(value) {
|
||||
canvasSizeState.value = value
|
||||
}
|
||||
|
||||
public var viewPoint: ViewPoint<T>
|
||||
get() = viewPointState.value
|
||||
get() = viewPointState.value ?: space.defaultViewPoint
|
||||
set(value) {
|
||||
config.onViewChange(value)
|
||||
viewPointState.value = value
|
||||
}
|
||||
|
||||
public val zoom: Float get() = viewPoint.zoom
|
||||
|
||||
|
||||
// Selection rectangle. If null - no selection
|
||||
public var selectRect: DpRect? by mutableStateOf(null)
|
||||
|
||||
public abstract fun DpOffset.toCoordinates(): T
|
||||
|
||||
public abstract fun T.toDpOffset(): DpOffset
|
||||
|
||||
public fun T.toOffset(density: Density): Offset = with(density){
|
||||
public fun T.toOffset(density: Density): Offset = with(density) {
|
||||
val dpOffset = this@toOffset.toDpOffset()
|
||||
Offset(dpOffset.x.toPx(), dpOffset.y.toPx())
|
||||
}
|
||||
@ -40,10 +51,7 @@ public abstract class CoordinateViewState<T : Any>(
|
||||
|
||||
public abstract fun ViewPoint<T>.moveBy(x: Dp, y: Dp): ViewPoint<T>
|
||||
|
||||
public abstract fun viewPointFor(rectangle: Rectangle<T>): ViewPoint<T>
|
||||
|
||||
// Selection rectangle. If null - no selection
|
||||
public var selectRect: DpRect? by mutableStateOf(null)
|
||||
public abstract fun computeViewPoint(rectangle: Rectangle<T>): ViewPoint<T>
|
||||
}
|
||||
|
||||
|
@ -1,6 +0,0 @@
|
||||
package center.sciprog.maps.features
|
||||
|
||||
|
||||
public object DraggableAttribute : Feature.Attribute<DragHandle<Any>>
|
||||
|
||||
public object DragListenerAttribute : Feature.Attribute<Set<(begin: Any, end: Any) -> Unit>>
|
@ -29,14 +29,14 @@ public interface Feature<T : Any> {
|
||||
public fun getBoundingBox(zoom: Float): Rectangle<T>?
|
||||
}
|
||||
|
||||
public interface PainterFeature<T:Any>: Feature<T> {
|
||||
public interface PainterFeature<T : Any> : Feature<T> {
|
||||
@Composable
|
||||
public fun getPainter(): Painter
|
||||
}
|
||||
|
||||
public interface SelectableFeature<T : Any> : Feature<T> {
|
||||
public operator fun contains(point: ViewPoint<T>): Boolean = getBoundingBox(point.zoom)?.let {
|
||||
point.focus in it
|
||||
public fun contains(point: T, zoom: Float): Boolean = getBoundingBox(zoom)?.let {
|
||||
point in it
|
||||
} ?: false
|
||||
}
|
||||
|
||||
@ -47,7 +47,7 @@ public interface DraggableFeature<T : Any> : SelectableFeature<T> {
|
||||
/**
|
||||
* A draggable marker feature. Other features could be bound to this one.
|
||||
*/
|
||||
public interface MarkerFeature<T: Any>: DraggableFeature<T>{
|
||||
public interface MarkerFeature<T : Any> : DraggableFeature<T> {
|
||||
public val center: T
|
||||
}
|
||||
|
||||
@ -72,7 +72,6 @@ public class FeatureSelector<T : Any>(
|
||||
public val selector: (zoom: Float) -> Feature<T>,
|
||||
) : Feature<T> {
|
||||
|
||||
|
||||
override fun getBoundingBox(zoom: Float): Rectangle<T>? = selector(zoom).getBoundingBox(zoom)
|
||||
}
|
||||
|
||||
@ -157,8 +156,8 @@ public class LineFeature<T : Any>(
|
||||
override fun getBoundingBox(zoom: Float): Rectangle<T> =
|
||||
space.Rectangle(a, b)
|
||||
|
||||
override fun contains(point: ViewPoint<T>): Boolean {
|
||||
return super.contains(point)
|
||||
override fun contains(point: T, zoom: Float): Boolean = with(space) {
|
||||
point in space.Rectangle(a, b) && point.distanceToLine(a, b, zoom).value < 5f
|
||||
}
|
||||
}
|
||||
|
||||
@ -228,17 +227,17 @@ public data class VectorImageFeature<T : Any>(
|
||||
*
|
||||
* @param rectangle the size of background in scheme size units. The screen units to scheme units ratio equals scale.
|
||||
*/
|
||||
public class ScalableImageFeature<T: Any>(
|
||||
public class ScalableImageFeature<T : Any>(
|
||||
override val space: CoordinateSpace<T>,
|
||||
public val rectangle: Rectangle<T>,
|
||||
override val zoomRange: FloatRange,
|
||||
override val attributes: AttributeMap = AttributeMap(),
|
||||
public val painter: @Composable () -> Painter,
|
||||
) : Feature<T>, PainterFeature<T>{
|
||||
) : Feature<T>, PainterFeature<T> {
|
||||
@Composable
|
||||
override fun getPainter(): Painter = painter.invoke()
|
||||
override fun getPainter(): Painter = painter.invoke()
|
||||
|
||||
override fun getBoundingBox(zoom: Float): Rectangle<T> =rectangle
|
||||
override fun getBoundingBox(zoom: Float): Rectangle<T> = rectangle
|
||||
}
|
||||
|
||||
|
||||
|
@ -119,9 +119,14 @@ public class FeatureCollection<T : Any>(
|
||||
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
public fun <F : SelectableFeature<T>> FeatureId<F>.selectable(
|
||||
onSelect: (FeatureId<F>, F) -> Unit,
|
||||
onSelect: () -> Unit,
|
||||
) {
|
||||
setAttribute(this, SelectableAttribute) { id, feature -> onSelect(id as FeatureId<F>, feature as F) }
|
||||
// val handle = ClickHandle<Any> { event, click ->
|
||||
// val feature: F = get(this@selectable)
|
||||
// if (feature.contains(this, click.focus))
|
||||
// }
|
||||
//
|
||||
// setAttribute(this, SelectableAttribute, handle)
|
||||
}
|
||||
|
||||
|
||||
|
@ -1,12 +1,27 @@
|
||||
package center.sciprog.maps.features
|
||||
|
||||
import androidx.compose.ui.input.pointer.PointerEvent
|
||||
import androidx.compose.ui.input.pointer.isPrimaryPressed
|
||||
import androidx.compose.ui.unit.DpSize
|
||||
|
||||
public fun interface ClickHandle<T : Any> {
|
||||
public fun handle(event: PointerEvent, click: ViewPoint<T>): Unit
|
||||
|
||||
public companion object {
|
||||
public fun <T : Any> withPrimaryButton(
|
||||
block: (event: PointerEvent, click: ViewPoint<T>) -> Unit,
|
||||
): ClickHandle<T> = ClickHandle { event, click ->
|
||||
if (event.buttons.isPrimaryPressed) {
|
||||
block(event, click)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public data class ViewConfig<T : Any>(
|
||||
val zoomSpeed: Float = 1f / 3f,
|
||||
val onClick: ViewPoint<T>.(PointerEvent) -> Unit = {},
|
||||
val dragHandle: DragHandle<T> = DragHandle.bypass(),
|
||||
val onClick: ClickHandle<T>? = null,
|
||||
val dragHandle: DragHandle<T>? = null,
|
||||
val onViewChange: ViewPoint<T>.() -> Unit = {},
|
||||
val onSelect: (Rectangle<T>) -> Unit = {},
|
||||
val zoomOnSelect: Boolean = true,
|
||||
|
@ -1,14 +1,14 @@
|
||||
package center.sciprog.maps.compose
|
||||
|
||||
import androidx.compose.foundation.gestures.drag
|
||||
import androidx.compose.foundation.gestures.forEachGesture
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.ui.ExperimentalComposeUiApi
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.geometry.Offset
|
||||
import androidx.compose.ui.input.pointer.*
|
||||
import androidx.compose.ui.unit.*
|
||||
import center.sciprog.maps.features.CoordinateViewState
|
||||
import androidx.compose.ui.unit.DpOffset
|
||||
import androidx.compose.ui.unit.DpRect
|
||||
import androidx.compose.ui.unit.dp
|
||||
import center.sciprog.maps.features.CoordinateViewScope
|
||||
import center.sciprog.maps.features.bottomRight
|
||||
import center.sciprog.maps.features.topLeft
|
||||
import kotlin.math.max
|
||||
@ -17,16 +17,37 @@ import kotlin.math.min
|
||||
|
||||
@OptIn(ExperimentalComposeUiApi::class)
|
||||
public fun <T : Any> Modifier.mapControls(
|
||||
state: CoordinateViewState<T>,
|
||||
state: CoordinateViewScope<T>,
|
||||
): Modifier = with(state) {
|
||||
pointerInput(Unit) {
|
||||
forEachGesture {
|
||||
awaitPointerEventScope {
|
||||
fun Offset.toDpOffset() = DpOffset(x.toDp(), y.toDp())
|
||||
|
||||
fun Offset.toDpOffset() = DpOffset(x.toDp(), y.toDp())
|
||||
awaitPointerEventScope {
|
||||
while (true) {
|
||||
val event = awaitPointerEvent()
|
||||
if (event.type == PointerEventType.Release ) {
|
||||
config.onClick?.handle(
|
||||
event,
|
||||
space.ViewPoint(event.changes.first().position.toDpOffset().toCoordinates(), zoom)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}.pointerInput(Unit) {
|
||||
fun Offset.toDpOffset() = DpOffset(x.toDp(), y.toDp())
|
||||
awaitPointerEventScope {
|
||||
while (true) {
|
||||
val event: PointerEvent = awaitPointerEvent()
|
||||
|
||||
event.changes.forEach { change ->
|
||||
|
||||
if (event.type == PointerEventType.Scroll) {
|
||||
val (xPos, yPos) = change.position
|
||||
//compute invariant point of translation
|
||||
val invariant = DpOffset(xPos.toDp(), yPos.toDp()).toCoordinates()
|
||||
viewPoint = with(space) {
|
||||
viewPoint.zoomBy(-change.scrollDelta.y * config.zoomSpeed, invariant)
|
||||
}
|
||||
change.consume()
|
||||
}
|
||||
//val dragStart = change.position
|
||||
//val dpPos = DpOffset(dragStart.x.toDp(), dragStart.y.toDp())
|
||||
|
||||
@ -45,12 +66,12 @@ public fun <T : Any> Modifier.mapControls(
|
||||
|
||||
//apply drag handle and check if it prohibits the drag even propagation
|
||||
if (selectionStart == null) {
|
||||
val dragResult = config.dragHandle.handle(
|
||||
val dragResult = config.dragHandle?.handle(
|
||||
event,
|
||||
space.ViewPoint(dpStart.toCoordinates(), viewPoint.zoom),
|
||||
space.ViewPoint(dpEnd.toCoordinates(), viewPoint.zoom)
|
||||
space.ViewPoint(dpStart.toCoordinates(), zoom),
|
||||
space.ViewPoint(dpEnd.toCoordinates(), zoom)
|
||||
)
|
||||
if(!dragResult.handleNext) return@drag
|
||||
if (dragResult?.handleNext == false) return@drag
|
||||
}
|
||||
|
||||
if (event.buttons.isPrimaryPressed) {
|
||||
@ -85,20 +106,12 @@ public fun <T : Any> Modifier.mapControls(
|
||||
)
|
||||
config.onSelect(coordinateRect)
|
||||
if (config.zoomOnSelect) {
|
||||
viewPoint = viewPointFor(coordinateRect)
|
||||
viewPoint = computeViewPoint(coordinateRect)
|
||||
}
|
||||
selectRect = null
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}.onPointerEvent(PointerEventType.Scroll) {
|
||||
val change = it.changes.first()
|
||||
val (xPos, yPos) = change.position
|
||||
//compute invariant point of translation
|
||||
val invariant = DpOffset(xPos.toDp(), yPos.toDp()).toCoordinates()
|
||||
viewPoint = with(space) {
|
||||
viewPoint.zoomBy(-change.scrollDelta.y * config.zoomSpeed, invariant)
|
||||
}
|
||||
}
|
||||
}
|
@ -21,7 +21,7 @@ internal fun Color.toPaint(): Paint = Paint().apply {
|
||||
}
|
||||
|
||||
public fun <T : Any> DrawScope.drawFeature(
|
||||
state: CoordinateViewState<T>,
|
||||
state: CoordinateViewScope<T>,
|
||||
painterCache: Map<PainterFeature<T>, Painter>,
|
||||
feature: Feature<T>,
|
||||
): Unit = with(state) {
|
||||
|
@ -1,6 +1,8 @@
|
||||
package center.sciprog.maps.scheme
|
||||
|
||||
import androidx.compose.ui.unit.DpOffset
|
||||
import androidx.compose.ui.unit.DpSize
|
||||
import androidx.compose.ui.unit.dp
|
||||
import center.sciprog.maps.features.CoordinateSpace
|
||||
import center.sciprog.maps.features.Rectangle
|
||||
import center.sciprog.maps.features.ViewPoint
|
||||
@ -61,4 +63,11 @@ object XYCoordinateSpace : CoordinateSpace<XY> {
|
||||
XY(maxX, maxY)
|
||||
)
|
||||
}
|
||||
|
||||
override val defaultViewPoint: ViewPoint<XY> = XYViewPoint(XY(0f, 0f), 1f)
|
||||
|
||||
override fun XY.offsetTo(b: XY, zoom: Float): DpOffset = DpOffset(
|
||||
(b.x - x).dp * zoom,
|
||||
(b.y - y).dp * zoom
|
||||
)
|
||||
}
|
@ -1,14 +1,17 @@
|
||||
package center.sciprog.maps.scheme
|
||||
|
||||
import androidx.compose.ui.unit.*
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.unit.Dp
|
||||
import androidx.compose.ui.unit.DpOffset
|
||||
import androidx.compose.ui.unit.DpRect
|
||||
import androidx.compose.ui.unit.dp
|
||||
import center.sciprog.maps.features.*
|
||||
import kotlin.math.min
|
||||
|
||||
class XYViewState(
|
||||
class XYViewScope(
|
||||
config: ViewConfig<XY>,
|
||||
canvasSize: DpSize,
|
||||
viewPoint: ViewPoint<XY>,
|
||||
) : CoordinateViewState<XY>(config, canvasSize, viewPoint) {
|
||||
) : CoordinateViewScope<XY>(config) {
|
||||
override val space: CoordinateSpace<XY>
|
||||
get() = XYCoordinateSpace
|
||||
|
||||
@ -21,8 +24,7 @@ class XYViewState(
|
||||
(canvasSize.width / 2 + (x.dp - viewPoint.focus.x.dp) * viewPoint.zoom),
|
||||
(canvasSize.height / 2 + (viewPoint.focus.y.dp - y.dp) * viewPoint.zoom)
|
||||
)
|
||||
|
||||
override fun viewPointFor(rectangle: Rectangle<XY>): ViewPoint<XY> {
|
||||
override fun computeViewPoint(rectangle: Rectangle<XY>): ViewPoint<XY> {
|
||||
val scale = min(
|
||||
canvasSize.width.value / rectangle.width,
|
||||
canvasSize.height.value / rectangle.height
|
||||
@ -41,5 +43,24 @@ class XYViewState(
|
||||
val bottomRight = rightBottom.toDpOffset()
|
||||
return DpRect(topLeft.x, topLeft.y, bottomRight.x, bottomRight.y)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@Composable
|
||||
public fun rememberMapState(
|
||||
config: ViewConfig<XY>,
|
||||
features: Collection<Feature<XY>> = emptyList(),
|
||||
initialViewPoint: ViewPoint<XY>? = null,
|
||||
initialRectangle: Rectangle<XY>? = null,
|
||||
): XYViewScope = remember {
|
||||
XYViewScope(config).also { mapState->
|
||||
if (initialViewPoint != null) {
|
||||
mapState.viewPoint = initialViewPoint
|
||||
} else if (initialRectangle != null) {
|
||||
mapState.viewPoint = mapState.computeViewPoint(initialRectangle)
|
||||
} else {
|
||||
features.computeBoundingBox(XYCoordinateSpace, 1f)?.let {
|
||||
mapState.viewPoint = mapState.computeViewPoint(it)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -4,7 +4,6 @@ import androidx.compose.foundation.Canvas
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.key
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.graphics.PathEffect
|
||||
@ -22,19 +21,10 @@ private val logger = KotlinLogging.logger("SchemeView")
|
||||
|
||||
@Composable
|
||||
public fun SchemeView(
|
||||
initialViewPoint: ViewPoint<XY>,
|
||||
state: XYViewScope,
|
||||
featuresState: FeatureCollection<XY>,
|
||||
config: ViewConfig<XY>,
|
||||
modifier: Modifier = Modifier.fillMaxSize(),
|
||||
) = key(initialViewPoint) {
|
||||
|
||||
val state = remember {
|
||||
XYViewState(
|
||||
config,
|
||||
defaultCanvasSize,
|
||||
initialViewPoint,
|
||||
)
|
||||
}
|
||||
) {
|
||||
with(state) {
|
||||
val painterCache: Map<PainterFeature<XY>, Painter> = key(featuresState) {
|
||||
featuresState.features.values.filterIsInstance<PainterFeature<XY>>().associateWith { it.getPainter() }
|
||||
@ -95,20 +85,22 @@ public fun SchemeView(
|
||||
config: ViewConfig<XY> = ViewConfig(),
|
||||
modifier: Modifier = Modifier.fillMaxSize(),
|
||||
) {
|
||||
|
||||
|
||||
val featuresState = key(featureMap) {
|
||||
FeatureCollection.build(XYCoordinateSpace) {
|
||||
featureMap.forEach { feature(it.key.id, it.value) }
|
||||
}
|
||||
}
|
||||
|
||||
val viewPointOverride: ViewPoint<XY> = remember(initialViewPoint, initialRectangle) {
|
||||
initialViewPoint
|
||||
?: initialRectangle?.computeViewPoint()
|
||||
?: featureMap.values.computeBoundingBox(XYCoordinateSpace, 1f)?.computeViewPoint()
|
||||
?: XYViewPoint(XY(0f, 0f), 1f)
|
||||
}
|
||||
val state = rememberMapState(
|
||||
config,
|
||||
featuresState.features.values,
|
||||
initialViewPoint = initialViewPoint,
|
||||
initialRectangle = initialRectangle,
|
||||
)
|
||||
|
||||
SchemeView(viewPointOverride, featuresState, config, modifier)
|
||||
SchemeView(state, featuresState, modifier)
|
||||
}
|
||||
|
||||
/**
|
||||
@ -127,37 +119,42 @@ public fun SchemeView(
|
||||
buildFeatures: FeatureCollection<XY>.() -> Unit = {},
|
||||
) {
|
||||
val featureState = FeatureCollection.remember(XYCoordinateSpace, buildFeatures)
|
||||
val mapState: XYViewScope = rememberMapState(
|
||||
config,
|
||||
featureState.features.values,
|
||||
initialViewPoint = initialViewPoint,
|
||||
initialRectangle = initialRectangle,
|
||||
)
|
||||
|
||||
val viewPointOverride: ViewPoint<XY> = remember(initialViewPoint, initialRectangle) {
|
||||
initialViewPoint
|
||||
?: initialRectangle?.computeViewPoint()
|
||||
?: featureState.features.values.computeBoundingBox(XYCoordinateSpace, 1f)?.computeViewPoint()
|
||||
?: XYViewPoint(XY(0f, 0f), 1f)
|
||||
val featureDrag: DragHandle<XY> = DragHandle.withPrimaryButton { event, start, end ->
|
||||
featureState.forEachWithAttribute(DraggableAttribute) { _, handle ->
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
(handle as DragHandle<XY>)
|
||||
.handle(event, start, end)
|
||||
.takeIf { !it.handleNext }
|
||||
?.let {
|
||||
//we expect it already have no bypass
|
||||
return@withPrimaryButton it
|
||||
}
|
||||
}
|
||||
//bypass
|
||||
DragResult(end)
|
||||
}
|
||||
|
||||
val featureDrag: DragHandle<XY> =
|
||||
DragHandle.withPrimaryButton { event, start: ViewPoint<XY>, end: ViewPoint<XY> ->
|
||||
featureState.forEachWithAttribute(DraggableAttribute) { _, handle ->
|
||||
//TODO add safety
|
||||
(handle as DragHandle<XY>)
|
||||
.handle(event, start, end)
|
||||
.takeIf { !it.handleNext }
|
||||
?.let { return@withPrimaryButton it }
|
||||
}
|
||||
DragResult(end)
|
||||
val featureClick: ClickHandle<XY> = ClickHandle.withPrimaryButton { event, click ->
|
||||
featureState.forEachWithAttribute(SelectableAttribute) { _, handle ->
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
(handle as ClickHandle<XY>).handle(event, click)
|
||||
config.onClick?.handle(event, click)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
val newConfig = config.copy(
|
||||
dragHandle = DragHandle.combine(featureDrag, config.dragHandle)
|
||||
dragHandle = config.dragHandle?.let { DragHandle.combine(featureDrag, it) } ?: featureDrag,
|
||||
onClick = featureClick
|
||||
)
|
||||
|
||||
SchemeView(
|
||||
initialViewPoint = viewPointOverride,
|
||||
featuresState = featureState,
|
||||
config = newConfig,
|
||||
modifier = modifier,
|
||||
)
|
||||
SchemeView(mapState, featureState, modifier)
|
||||
}
|
||||
|
||||
///**
|
||||
|
Loading…
Reference in New Issue
Block a user