diff --git a/demo/maps/src/jvmMain/kotlin/Main.kt b/demo/maps/src/jvmMain/kotlin/Main.kt index ebd4f8d..f293b59 100644 --- a/demo/maps/src/jvmMain/kotlin/Main.kt +++ b/demo/maps/src/jvmMain/kotlin/Main.kt @@ -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(null) } + val centerCoordinates = MutableStateFlow(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) } } } diff --git a/demo/scheme/src/jvmMain/kotlin/Main.kt b/demo/scheme/src/jvmMain/kotlin/Main.kt index f0621bd..5bd5171 100644 --- a/demo/scheme/src/jvmMain/kotlin/Main.kt +++ b/demo/scheme/src/jvmMain/kotlin/Main.kt @@ -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, ) } diff --git a/maps-kt-compose/src/commonMain/kotlin/center/sciprog/maps/compose/MapTileProvider.kt b/maps-kt-compose/src/commonMain/kotlin/center/sciprog/maps/compose/MapTileProvider.kt index 8d99571..3aeb0c8 100644 --- a/maps-kt-compose/src/commonMain/kotlin/center/sciprog/maps/compose/MapTileProvider.kt +++ b/maps-kt-compose/src/commonMain/kotlin/center/sciprog/maps/compose/MapTileProvider.kt @@ -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 diff --git a/maps-kt-compose/src/commonMain/kotlin/center/sciprog/maps/compose/MapView.kt b/maps-kt-compose/src/commonMain/kotlin/center/sciprog/maps/compose/MapView.kt index c035942..4bf50d2 100644 --- a/maps-kt-compose/src/commonMain/kotlin/center/sciprog/maps/compose/MapView.kt +++ b/maps-kt-compose/src/commonMain/kotlin/center/sciprog/maps/compose/MapView.kt @@ -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, - config: ViewConfig = ViewConfig(), modifier: Modifier = Modifier.fillMaxSize(), ) -internal val defaultCanvasSize = DpSize(512.dp, 512.dp) - -public fun Rectangle.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 = 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.() -> 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 = DragHandle.withPrimaryButton { event, start, end -> featureState.forEachWithAttribute(DraggableAttribute) { _, handle -> @@ -107,16 +86,18 @@ public fun MapView( DragResult(end) } + val featureClick: ClickHandle = ClickHandle.withPrimaryButton { event, click -> + featureState.forEachWithAttribute(SelectableAttribute) { _, handle -> + @Suppress("UNCHECKED_CAST") + (handle as ClickHandle).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) } \ No newline at end of file diff --git a/maps-kt-compose/src/commonMain/kotlin/center/sciprog/maps/compose/MapViewPoint.kt b/maps-kt-compose/src/commonMain/kotlin/center/sciprog/maps/compose/MapViewPoint.kt index 93840de..014fe50 100644 --- a/maps-kt-compose/src/commonMain/kotlin/center/sciprog/maps/compose/MapViewPoint.kt +++ b/maps-kt-compose/src/commonMain/kotlin/center/sciprog/maps/compose/MapViewPoint.kt @@ -11,7 +11,7 @@ public data class MapViewPoint( override val focus: GeodeticMapCoordinates, override val zoom: Float, ) : ViewPoint{ - 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) diff --git a/maps-kt-compose/src/commonMain/kotlin/center/sciprog/maps/compose/MapViewState.kt b/maps-kt-compose/src/commonMain/kotlin/center/sciprog/maps/compose/MapViewScope.kt similarity index 58% rename from maps-kt-compose/src/commonMain/kotlin/center/sciprog/maps/compose/MapViewState.kt rename to maps-kt-compose/src/commonMain/kotlin/center/sciprog/maps/compose/MapViewScope.kt index 846fbe4..3ed3a06 100644 --- a/maps-kt-compose/src/commonMain/kotlin/center/sciprog/maps/compose/MapViewState.kt +++ b/maps-kt-compose/src/commonMain/kotlin/center/sciprog/maps/compose/MapViewScope.kt @@ -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, - canvasSize: DpSize, - viewPoint: ViewPoint, - val tileSize: Int, -) : CoordinateViewState(config, canvasSize, viewPoint) { - override val space: CoordinateSpace get() = GmcCoordinateSpace +) : CoordinateViewScope(config) { + override val space: CoordinateSpace 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): ViewPoint { + override fun computeViewPoint(rectangle: Rectangle): ViewPoint { 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, - canvasSize: DpSize, - viewPoint: ViewPoint, - tileSize: Int, -): MapViewState = remember { - MapViewState(config, canvasSize, viewPoint, tileSize) + features: Collection> = emptyList(), + initialViewPoint: MapViewPoint? = null, + initialRectangle: Rectangle? = 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) + } + } + } } \ No newline at end of file diff --git a/maps-kt-compose/src/commonMain/kotlin/center/sciprog/maps/compose/GmcCoordinateSpace.kt b/maps-kt-compose/src/commonMain/kotlin/center/sciprog/maps/compose/WebMercatorSpace.kt similarity index 80% rename from maps-kt-compose/src/commonMain/kotlin/center/sciprog/maps/compose/GmcCoordinateSpace.kt rename to maps-kt-compose/src/commonMain/kotlin/center/sciprog/maps/compose/WebMercatorSpace.kt index 17f1326..5a7af9d 100644 --- a/maps-kt-compose/src/commonMain/kotlin/center/sciprog/maps/compose/GmcCoordinateSpace.kt +++ b/maps-kt-compose/src/commonMain/kotlin/center/sciprog/maps/compose/WebMercatorSpace.kt @@ -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 { +public object WebMercatorSpace : CoordinateSpace { + + 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 = GmcRectangle(first, second) override fun Rectangle(center: Gmc, zoom: Float, size: DpSize): Rectangle { @@ -15,6 +23,8 @@ public object GmcCoordinateSpace : CoordinateSpace { return Rectangle(center, (size.width.value / scale).radians, (size.height.value / scale).radians) } + override val defaultViewPoint: ViewPoint = MapViewPoint.globe + override fun ViewPoint(center: Gmc, zoom: Float): ViewPoint = MapViewPoint(center, zoom) override fun ViewPoint.moveBy(delta: Gmc): ViewPoint { @@ -29,7 +39,7 @@ public object GmcCoordinateSpace : CoordinateSpace { } override fun ViewPoint.zoomBy(zoomDelta: Float, invariant: Gmc): ViewPoint = 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 { 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 + ) + } } /** diff --git a/maps-kt-compose/src/jvmMain/kotlin/center/sciprog/maps/compose/MapViewJvm.kt b/maps-kt-compose/src/jvmMain/kotlin/center/sciprog/maps/compose/MapViewJvm.kt index 95af93e..0a47584 100644 --- a/maps-kt-compose/src/jvmMain/kotlin/center/sciprog/maps/compose/MapViewJvm.kt +++ b/maps-kt-compose/src/jvmMain/kotlin/center/sciprog/maps/compose/MapViewJvm.kt @@ -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, - config: ViewConfig, modifier: Modifier, -): Unit = key(initialViewPoint) { +): Unit = with(mapState) { - val state = rememberMapState( - config, - defaultCanvasSize, - initialViewPoint, - mapTileProvider.tileSize - ) + val mapTiles = remember(mapTileProvider) { mutableStateListOf() } - with(state) { + // Load tiles asynchronously + LaunchedEffect(viewPoint, canvasSize) { + with(mapTileProvider) { + val indexRange = 0 until 2.0.pow(intZoom).toInt() - val mapTiles = remember(mapTileProvider) { mutableStateListOf() } + 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, Painter> = key(featuresState) { - featuresState.features.values.filterIsInstance>().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, Painter> = key(featuresState) { + featuresState.features.values.filterIsInstance>().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) + ) + ) + } + } } + diff --git a/maps-kt-core/src/commonMain/kotlin/center/sciprog/maps/coordinates/WebMercatorProjection.kt b/maps-kt-core/src/commonMain/kotlin/center/sciprog/maps/coordinates/WebMercatorProjection.kt index 1e4151c..c7c6c1b 100644 --- a/maps-kt-core/src/commonMain/kotlin/center/sciprog/maps/coordinates/WebMercatorProjection.kt +++ b/maps-kt-core/src/commonMain/kotlin/center/sciprog/maps/coordinates/WebMercatorProjection.kt @@ -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() ) } diff --git a/maps-kt-features/src/commonMain/kotlin/center/sciprog/maps/features/AttributeMap.kt b/maps-kt-features/src/commonMain/kotlin/center/sciprog/maps/features/AttributeMap.kt index e5d2908..6368626 100644 --- a/maps-kt-features/src/commonMain/kotlin/center/sciprog/maps/features/AttributeMap.kt +++ b/maps-kt-features/src/commonMain/kotlin/center/sciprog/maps/features/AttributeMap.kt @@ -5,7 +5,11 @@ import androidx.compose.ui.graphics.Color public object ZAttribute : Feature.Attribute -public object SelectableAttribute : Feature.Attribute<(FeatureId<*>, SelectableFeature<*>) -> Unit> +public object DraggableAttribute : Feature.Attribute> + +public object DragListenerAttribute : Feature.Attribute Unit>> + +public object SelectableAttribute : Feature.Attribute> public object VisibleAttribute : Feature.Attribute diff --git a/maps-kt-features/src/commonMain/kotlin/center/sciprog/maps/features/CoordinateSpace.kt b/maps-kt-features/src/commonMain/kotlin/center/sciprog/maps/features/CoordinateSpace.kt index c3c5d67..92f689c 100644 --- a/maps-kt-features/src/commonMain/kotlin/center/sciprog/maps/features/CoordinateSpace.kt +++ b/maps-kt-features/src/commonMain/kotlin/center/sciprog/maps/features/CoordinateSpace.kt @@ -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 { @@ -31,6 +36,11 @@ public interface CoordinateSpace { */ public fun Rectangle(center: T, zoom: Float, size: DpSize): Rectangle + /** + * A view point used by default + */ + public val defaultViewPoint: ViewPoint + /** * Create a [ViewPoint] associated with this coordinate space. */ @@ -52,6 +62,20 @@ public interface CoordinateSpace { public fun Collection.wrapPoints(): Rectangle? + 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 CoordinateSpace.Rectangle(viewPoint: ViewPoint, size: DpSize): Rectangle = diff --git a/maps-kt-features/src/commonMain/kotlin/center/sciprog/maps/features/CoordinateViewState.kt b/maps-kt-features/src/commonMain/kotlin/center/sciprog/maps/features/CoordinateViewScope.kt similarity index 62% rename from maps-kt-features/src/commonMain/kotlin/center/sciprog/maps/features/CoordinateViewState.kt rename to maps-kt-features/src/commonMain/kotlin/center/sciprog/maps/features/CoordinateViewScope.kt index 32c0dca..2d872e2 100644 --- a/maps-kt-features/src/commonMain/kotlin/center/sciprog/maps/features/CoordinateViewState.kt +++ b/maps-kt-features/src/commonMain/kotlin/center/sciprog/maps/features/CoordinateViewScope.kt @@ -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( +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( public val config: ViewConfig, - canvasSize: DpSize, - viewPoint: ViewPoint, ) { public abstract val space: CoordinateSpace - public var canvasSize: DpSize by mutableStateOf(canvasSize) - protected var viewPointState: MutableState> = mutableStateOf(viewPoint) + protected var canvasSizeState: MutableState = mutableStateOf(null) + protected var viewPointState: MutableState?> = mutableStateOf(null) + + public var canvasSize: DpSize + get() = canvasSizeState.value ?: DpSize(512.dp, 512.dp) + set(value) { + canvasSizeState.value = value + } public var viewPoint: ViewPoint - 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( public abstract fun ViewPoint.moveBy(x: Dp, y: Dp): ViewPoint - public abstract fun viewPointFor(rectangle: Rectangle): ViewPoint - - // Selection rectangle. If null - no selection - public var selectRect: DpRect? by mutableStateOf(null) + public abstract fun computeViewPoint(rectangle: Rectangle): ViewPoint } diff --git a/maps-kt-features/src/commonMain/kotlin/center/sciprog/maps/features/DraggableAttribute.kt b/maps-kt-features/src/commonMain/kotlin/center/sciprog/maps/features/DraggableAttribute.kt deleted file mode 100644 index 50d5d15..0000000 --- a/maps-kt-features/src/commonMain/kotlin/center/sciprog/maps/features/DraggableAttribute.kt +++ /dev/null @@ -1,6 +0,0 @@ -package center.sciprog.maps.features - - -public object DraggableAttribute : Feature.Attribute> - -public object DragListenerAttribute : Feature.Attribute Unit>> \ No newline at end of file diff --git a/maps-kt-features/src/commonMain/kotlin/center/sciprog/maps/features/Feature.kt b/maps-kt-features/src/commonMain/kotlin/center/sciprog/maps/features/Feature.kt index 5374cdc..fa960cb 100644 --- a/maps-kt-features/src/commonMain/kotlin/center/sciprog/maps/features/Feature.kt +++ b/maps-kt-features/src/commonMain/kotlin/center/sciprog/maps/features/Feature.kt @@ -29,14 +29,14 @@ public interface Feature { public fun getBoundingBox(zoom: Float): Rectangle? } -public interface PainterFeature: Feature { +public interface PainterFeature : Feature { @Composable public fun getPainter(): Painter } public interface SelectableFeature : Feature { - public operator fun contains(point: ViewPoint): 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 : SelectableFeature { /** * A draggable marker feature. Other features could be bound to this one. */ -public interface MarkerFeature: DraggableFeature{ +public interface MarkerFeature : DraggableFeature { public val center: T } @@ -69,10 +69,9 @@ public class FeatureSelector( override val space: CoordinateSpace, override val zoomRange: FloatRange, override val attributes: AttributeMap = AttributeMap(), - public val selector: (zoom: Float) -> Feature, + public val selector: (zoom: Float) -> Feature, ) : Feature { - override fun getBoundingBox(zoom: Float): Rectangle? = selector(zoom).getBoundingBox(zoom) } @@ -157,8 +156,8 @@ public class LineFeature( override fun getBoundingBox(zoom: Float): Rectangle = space.Rectangle(a, b) - override fun contains(point: ViewPoint): 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( * * @param rectangle the size of background in scheme size units. The screen units to scheme units ratio equals scale. */ -public class ScalableImageFeature( +public class ScalableImageFeature( override val space: CoordinateSpace, public val rectangle: Rectangle, override val zoomRange: FloatRange, override val attributes: AttributeMap = AttributeMap(), public val painter: @Composable () -> Painter, -) : Feature, PainterFeature{ +) : Feature, PainterFeature { @Composable - override fun getPainter(): Painter = painter.invoke() + override fun getPainter(): Painter = painter.invoke() - override fun getBoundingBox(zoom: Float): Rectangle =rectangle + override fun getBoundingBox(zoom: Float): Rectangle = rectangle } diff --git a/maps-kt-features/src/commonMain/kotlin/center/sciprog/maps/features/FeatureCollection.kt b/maps-kt-features/src/commonMain/kotlin/center/sciprog/maps/features/FeatureCollection.kt index 4e02413..98d593f 100644 --- a/maps-kt-features/src/commonMain/kotlin/center/sciprog/maps/features/FeatureCollection.kt +++ b/maps-kt-features/src/commonMain/kotlin/center/sciprog/maps/features/FeatureCollection.kt @@ -119,9 +119,14 @@ public class FeatureCollection( @Suppress("UNCHECKED_CAST") public fun > FeatureId.selectable( - onSelect: (FeatureId, F) -> Unit, + onSelect: () -> Unit, ) { - setAttribute(this, SelectableAttribute) { id, feature -> onSelect(id as FeatureId, feature as F) } +// val handle = ClickHandle { event, click -> +// val feature: F = get(this@selectable) +// if (feature.contains(this, click.focus)) +// } +// +// setAttribute(this, SelectableAttribute, handle) } diff --git a/maps-kt-features/src/commonMain/kotlin/center/sciprog/maps/features/ViewConfig.kt b/maps-kt-features/src/commonMain/kotlin/center/sciprog/maps/features/ViewConfig.kt index 801be70..ae2f23f 100644 --- a/maps-kt-features/src/commonMain/kotlin/center/sciprog/maps/features/ViewConfig.kt +++ b/maps-kt-features/src/commonMain/kotlin/center/sciprog/maps/features/ViewConfig.kt @@ -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 { + public fun handle(event: PointerEvent, click: ViewPoint): Unit + + public companion object { + public fun withPrimaryButton( + block: (event: PointerEvent, click: ViewPoint) -> Unit, + ): ClickHandle = ClickHandle { event, click -> + if (event.buttons.isPrimaryPressed) { + block(event, click) + } + } + } +} + public data class ViewConfig( val zoomSpeed: Float = 1f / 3f, - val onClick: ViewPoint.(PointerEvent) -> Unit = {}, - val dragHandle: DragHandle = DragHandle.bypass(), + val onClick: ClickHandle? = null, + val dragHandle: DragHandle? = null, val onViewChange: ViewPoint.() -> Unit = {}, val onSelect: (Rectangle) -> Unit = {}, val zoomOnSelect: Boolean = true, diff --git a/maps-kt-features/src/jvmMain/kotlin/center/sciprog/maps/compose/mapControls.kt b/maps-kt-features/src/jvmMain/kotlin/center/sciprog/maps/compose/mapControls.kt index 09b7ff2..f9bfbdc 100644 --- a/maps-kt-features/src/jvmMain/kotlin/center/sciprog/maps/compose/mapControls.kt +++ b/maps-kt-features/src/jvmMain/kotlin/center/sciprog/maps/compose/mapControls.kt @@ -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 Modifier.mapControls( - state: CoordinateViewState, + state: CoordinateViewScope, ): 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 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 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) - } } } \ No newline at end of file diff --git a/maps-kt-features/src/jvmMain/kotlin/center/sciprog/maps/features/drawFeature.kt b/maps-kt-features/src/jvmMain/kotlin/center/sciprog/maps/features/drawFeature.kt index 32e2d69..5460d62 100644 --- a/maps-kt-features/src/jvmMain/kotlin/center/sciprog/maps/features/drawFeature.kt +++ b/maps-kt-features/src/jvmMain/kotlin/center/sciprog/maps/features/drawFeature.kt @@ -21,7 +21,7 @@ internal fun Color.toPaint(): Paint = Paint().apply { } public fun DrawScope.drawFeature( - state: CoordinateViewState, + state: CoordinateViewScope, painterCache: Map, Painter>, feature: Feature, ): Unit = with(state) { diff --git a/maps-kt-scheme/src/commonMain/kotlin/center/sciprog/maps/scheme/XYCoordinateSpace.kt b/maps-kt-scheme/src/commonMain/kotlin/center/sciprog/maps/scheme/XYCoordinateSpace.kt index aa9c25e..ea89b4b 100644 --- a/maps-kt-scheme/src/commonMain/kotlin/center/sciprog/maps/scheme/XYCoordinateSpace.kt +++ b/maps-kt-scheme/src/commonMain/kotlin/center/sciprog/maps/scheme/XYCoordinateSpace.kt @@ -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(maxX, maxY) ) } + + override val defaultViewPoint: ViewPoint = 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 + ) } \ No newline at end of file diff --git a/maps-kt-scheme/src/commonMain/kotlin/center/sciprog/maps/scheme/XYViewState.kt b/maps-kt-scheme/src/commonMain/kotlin/center/sciprog/maps/scheme/XYViewScope.kt similarity index 55% rename from maps-kt-scheme/src/commonMain/kotlin/center/sciprog/maps/scheme/XYViewState.kt rename to maps-kt-scheme/src/commonMain/kotlin/center/sciprog/maps/scheme/XYViewScope.kt index 61fb0ab..1ff80de 100644 --- a/maps-kt-scheme/src/commonMain/kotlin/center/sciprog/maps/scheme/XYViewState.kt +++ b/maps-kt-scheme/src/commonMain/kotlin/center/sciprog/maps/scheme/XYViewScope.kt @@ -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, - canvasSize: DpSize, - viewPoint: ViewPoint, -) : CoordinateViewState(config, canvasSize, viewPoint) { +) : CoordinateViewScope(config) { override val space: CoordinateSpace 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): ViewPoint { + override fun computeViewPoint(rectangle: Rectangle): ViewPoint { 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, + features: Collection> = emptyList(), + initialViewPoint: ViewPoint? = null, + initialRectangle: Rectangle? = 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) + } + } + } } \ No newline at end of file diff --git a/maps-kt-scheme/src/jvmMain/kotlin/center/sciprog/maps/scheme/SchemeView.kt b/maps-kt-scheme/src/jvmMain/kotlin/center/sciprog/maps/scheme/SchemeView.kt index 82303d7..0551ba2 100644 --- a/maps-kt-scheme/src/jvmMain/kotlin/center/sciprog/maps/scheme/SchemeView.kt +++ b/maps-kt-scheme/src/jvmMain/kotlin/center/sciprog/maps/scheme/SchemeView.kt @@ -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, + state: XYViewScope, featuresState: FeatureCollection, - config: ViewConfig, modifier: Modifier = Modifier.fillMaxSize(), -) = key(initialViewPoint) { - - val state = remember { - XYViewState( - config, - defaultCanvasSize, - initialViewPoint, - ) - } +) { with(state) { val painterCache: Map, Painter> = key(featuresState) { featuresState.features.values.filterIsInstance>().associateWith { it.getPainter() } @@ -95,20 +85,22 @@ public fun SchemeView( config: ViewConfig = ViewConfig(), modifier: Modifier = Modifier.fillMaxSize(), ) { + + val featuresState = key(featureMap) { FeatureCollection.build(XYCoordinateSpace) { featureMap.forEach { feature(it.key.id, it.value) } } } - val viewPointOverride: ViewPoint = 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.() -> Unit = {}, ) { val featureState = FeatureCollection.remember(XYCoordinateSpace, buildFeatures) + val mapState: XYViewScope = rememberMapState( + config, + featureState.features.values, + initialViewPoint = initialViewPoint, + initialRectangle = initialRectangle, + ) - val viewPointOverride: ViewPoint = remember(initialViewPoint, initialRectangle) { - initialViewPoint - ?: initialRectangle?.computeViewPoint() - ?: featureState.features.values.computeBoundingBox(XYCoordinateSpace, 1f)?.computeViewPoint() - ?: XYViewPoint(XY(0f, 0f), 1f) + val featureDrag: DragHandle = DragHandle.withPrimaryButton { event, start, end -> + featureState.forEachWithAttribute(DraggableAttribute) { _, handle -> + @Suppress("UNCHECKED_CAST") + (handle as DragHandle) + .handle(event, start, end) + .takeIf { !it.handleNext } + ?.let { + //we expect it already have no bypass + return@withPrimaryButton it + } + } + //bypass + DragResult(end) } - val featureDrag: DragHandle = - DragHandle.withPrimaryButton { event, start: ViewPoint, end: ViewPoint -> - featureState.forEachWithAttribute(DraggableAttribute) { _, handle -> - //TODO add safety - (handle as DragHandle) - .handle(event, start, end) - .takeIf { !it.handleNext } - ?.let { return@withPrimaryButton it } - } - DragResult(end) + val featureClick: ClickHandle = ClickHandle.withPrimaryButton { event, click -> + featureState.forEachWithAttribute(SelectableAttribute) { _, handle -> + @Suppress("UNCHECKED_CAST") + (handle as ClickHandle).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) } ///**