From edeb422335f407c9fecde5877d25acb7105e8790 Mon Sep 17 00:00:00 2001 From: Alexander Nozik Date: Fri, 9 Dec 2022 22:21:24 +0300 Subject: [PATCH] Typed features for Map --- demo/maps/src/jvmMain/kotlin/Main.kt | 37 ++-- .../center/sciprog/maps/compose/MapFeature.kt | 6 +- .../sciprog/maps/compose/MapFeaturesState.kt | 159 +++++++++++------- .../center/sciprog/maps/compose/MapView.kt | 6 +- .../sciprog/maps/compose/MapTextFeature.kt | 0 5 files changed, 123 insertions(+), 85 deletions(-) delete mode 100644 maps-kt-compose/src/jvmMain/kotlin/center/sciprog/maps/compose/MapTextFeature.kt diff --git a/demo/maps/src/jvmMain/kotlin/Main.kt b/demo/maps/src/jvmMain/kotlin/Main.kt index 3b4c032..1f19eeb 100644 --- a/demo/maps/src/jvmMain/kotlin/Main.kt +++ b/demo/maps/src/jvmMain/kotlin/Main.kt @@ -15,8 +15,6 @@ import center.sciprog.maps.coordinates.* import io.ktor.client.HttpClient import io.ktor.client.engine.cio.CIO import kotlinx.coroutines.delay -import kotlinx.coroutines.isActive -import kotlinx.coroutines.launch import java.nio.file.Path import kotlin.math.PI import kotlin.random.Random @@ -45,6 +43,8 @@ fun App() { val pointTwo = 55.929444 to 37.518434 val pointThree = 60.929444 to 37.518434 + val dragPoint = 55.744 to 37.614 + MapView( mapTileProvider = mapTileProvider, // initialViewPoint = MapViewPoint( @@ -67,8 +67,12 @@ fun App() { var drag2 = Gmc.ofDegrees(55.8, 37.5) + var drag3 = Gmc.ofDegrees(56.0, 37.5) + fun updateLine() { - line(drag1, drag2, id = "connection", color = Color.Magenta) + line(drag1, drag2, id = "connection1", color = Color.Magenta) + line(drag2, drag3, id = "connection2", color = Color.Magenta) + line(drag3, drag1, id = "connection3", color = Color.Magenta) } rectangle(drag1, size = DpSize(10.dp, 10.dp)).draggable { _, _, end -> @@ -83,6 +87,13 @@ fun App() { true } + rectangle(drag3, size = DpSize(10.dp, 10.dp)).draggable { _, _, end -> + drag3 = end.focus + updateLine() + true + } + + updateLine() points( @@ -98,9 +109,13 @@ fun App() { ) //remember feature ID - val circleId: FeatureId = circle( + circle( centerCoordinates = pointTwo, - ) + ).updates(scope) { + delay(200) + //Overwrite a feature with new color + 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) @@ -118,18 +133,6 @@ fun App() { text(position = it, it.toShortString(), id = "text", color = Color.Blue) } } - - scope.launch { - while (isActive) { - delay(200) - //Overwrite a feature with new color - circle( - pointTwo, - id = circleId, - color = Color(Random.nextFloat(), Random.nextFloat(), Random.nextFloat()) - ) - } - } } } } diff --git a/maps-kt-compose/src/commonMain/kotlin/center/sciprog/maps/compose/MapFeature.kt b/maps-kt-compose/src/commonMain/kotlin/center/sciprog/maps/compose/MapFeature.kt index 6e09bc8..b78d7d9 100644 --- a/maps-kt-compose/src/commonMain/kotlin/center/sciprog/maps/compose/MapFeature.kt +++ b/maps-kt-compose/src/commonMain/kotlin/center/sciprog/maps/compose/MapFeature.kt @@ -33,7 +33,7 @@ public interface DraggableMapFeature : SelectableMapFeature { public fun Iterable.computeBoundingBox(zoom: Double): GmcRectangle? = mapNotNull { it.getBoundingBox(zoom) }.wrapAll() -internal fun Pair.toCoordinates() = +public fun Pair.toCoordinates() = GeodeticMapCoordinates.ofDegrees(first.toDouble(), second.toDouble()) internal val defaultZoomRange = 1..18 @@ -88,7 +88,7 @@ public class MapPointsFeature( override fun getBoundingBox(zoom: Double): GmcRectangle = GmcRectangle(points.first(), points.last()) } -public class MapCircleFeature( +public data class MapCircleFeature( public val center: GeodeticMapCoordinates, override val zoomRange: IntRange = defaultZoomRange, public val size: Float = 5f, @@ -179,7 +179,7 @@ public class MapVectorImageFeature( * A group of other features */ public class MapFeatureGroup( - public val children: Map, + public val children: Map, MapFeature>, override val zoomRange: IntRange = defaultZoomRange, ) : MapFeature { override fun getBoundingBox(zoom: Double): GmcRectangle? = diff --git a/maps-kt-compose/src/commonMain/kotlin/center/sciprog/maps/compose/MapFeaturesState.kt b/maps-kt-compose/src/commonMain/kotlin/center/sciprog/maps/compose/MapFeaturesState.kt index 49c26d9..fc3dcc4 100644 --- a/maps-kt-compose/src/commonMain/kotlin/center/sciprog/maps/compose/MapFeaturesState.kt +++ b/maps-kt-compose/src/commonMain/kotlin/center/sciprog/maps/compose/MapFeaturesState.kt @@ -11,27 +11,33 @@ import androidx.compose.ui.graphics.vector.ImageVector import androidx.compose.ui.unit.DpSize import androidx.compose.ui.unit.dp import center.sciprog.maps.coordinates.* +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Job +import kotlinx.coroutines.isActive +import kotlinx.coroutines.launch -public typealias FeatureId = String +@JvmInline +public value class FeatureId(public val id: String) public object DraggableAttribute : MapFeaturesState.Attribute +public object SelectableAttribute : MapFeaturesState.Attribute<(FeatureId<*>, SelectableMapFeature) -> Unit> public class MapFeaturesState internal constructor( - private val features: MutableMap, - @PublishedApi internal val attributes: MutableMap, in Any?>>, + private val featureMap: MutableMap, + @PublishedApi internal val attributeMap: MutableMap, in Any?>>, ) { public interface Attribute //TODO use context receiver for that - public fun FeatureId.draggable( + public fun FeatureId.draggable( //TODO add constraints callback: DragHandle = DragHandle.BYPASS, ) { val handle = DragHandle.withPrimaryButton { event, start, end -> - val feature = features[this] as? DraggableMapFeature ?: return@withPrimaryButton true + val feature = featureMap[id] as? DraggableMapFeature ?: return@withPrimaryButton true val boundingBox = feature.getBoundingBox(start.zoom) ?: return@withPrimaryButton true if (start.focus in boundingBox) { - addFeature(this, feature.withCoordinates(end.focus)) + feature(id, feature.withCoordinates(end.focus)) callback.handle(event, start, end) false } else { @@ -41,29 +47,53 @@ public class MapFeaturesState internal constructor( setAttribute(this, DraggableAttribute, handle) } - - public fun features(): Map = features - - - private fun generateID(feature: MapFeature): FeatureId = "@feature[${feature.hashCode().toUInt()}]" - - public fun addFeature(id: FeatureId?, feature: MapFeature): FeatureId { - val safeId = id ?: generateID(feature) - features[id ?: generateID(feature)] = feature - return safeId - } - - public fun setAttribute(id: FeatureId, key: Attribute, value: T) { - attributes.getOrPut(id) { mutableStateMapOf() }[key] = value - } - - public fun removeAttribute(id: FeatureId, key: Attribute<*>) { - attributes[id]?.remove(key) + /** + * Cyclic update of a feature. Called infinitely until canceled. + */ + public fun FeatureId.updates( + scope: CoroutineScope, + update: suspend (T) -> T, + ): Job = scope.launch { + while (isActive) { + feature(this@updates, update(getFeature(this@updates))) + } } @Suppress("UNCHECKED_CAST") - public fun getAttribute(id: FeatureId, key: Attribute): T? = - attributes[id]?.get(key)?.let { it as T } + public fun FeatureId.selectable( + onSelect: (FeatureId, T) -> Unit, + ) { + setAttribute(this, SelectableAttribute) { id, feature -> onSelect(id as FeatureId, feature as T) } + } + + + public fun features(): Map, MapFeature> = featureMap.mapKeys { FeatureId(it.key) } + + @Suppress("UNCHECKED_CAST") + public fun getFeature(id: FeatureId): T = featureMap[id.id] as T + + + private fun generateID(feature: MapFeature): String = "@feature[${feature.hashCode().toUInt()}]" + + public fun feature(id: String?, feature: T): FeatureId { + val safeId = id ?: generateID(feature) + featureMap[safeId] = feature + return FeatureId(safeId) + } + + public fun feature(id: FeatureId?, feature: T): FeatureId = feature(id?.id, feature) + + public fun setAttribute(id: FeatureId<*>, key: Attribute, value: T) { + attributeMap.getOrPut(id.id) { mutableStateMapOf() }[key] = value + } + + public fun removeAttribute(id: FeatureId<*>, key: Attribute<*>) { + attributeMap[id.id]?.remove(key) + } + + @Suppress("UNCHECKED_CAST") + public fun getAttribute(id: FeatureId<*>, key: Attribute): T? = + attributeMap[id.id]?.get(key)?.let { it as T } // @Suppress("UNCHECKED_CAST") // public fun findAllWithAttribute(key: Attribute, condition: (T) -> Boolean): Set { @@ -72,11 +102,14 @@ public class MapFeaturesState internal constructor( // }.keys // } - public inline fun forEachWithAttribute(key: Attribute, block: (id: FeatureId, attributeValue: T) -> Unit) { - attributes.forEach { (id, attributeMap) -> + public inline fun forEachWithAttribute( + key: Attribute, + block: (id: FeatureId<*>, attributeValue: T) -> Unit, + ) { + attributeMap.forEach { (id, attributeMap) -> attributeMap[key]?.let { @Suppress("UNCHECKED_CAST") - block(id, it as T) + block(FeatureId(id), it as T) } } } @@ -111,8 +144,8 @@ public fun MapFeaturesState.circle( zoomRange: IntRange = defaultZoomRange, size: Float = 5f, color: Color = Color.Red, - id: FeatureId? = null, -): FeatureId = addFeature( + id: String? = null, +): FeatureId = feature( id, MapCircleFeature(center, zoomRange, size, color) ) @@ -121,8 +154,8 @@ public fun MapFeaturesState.circle( zoomRange: IntRange = defaultZoomRange, size: Float = 5f, color: Color = Color.Red, - id: FeatureId? = null, -): FeatureId = addFeature( + id: String? = null, +): FeatureId = feature( id, MapCircleFeature(centerCoordinates.toCoordinates(), zoomRange, size, color) ) @@ -131,8 +164,8 @@ public fun MapFeaturesState.rectangle( zoomRange: IntRange = defaultZoomRange, size: DpSize = DpSize(5.dp, 5.dp), color: Color = Color.Red, - id: FeatureId? = null, -): FeatureId = addFeature( + id: String? = null, +): FeatureId = feature( id, MapRectangleFeature(centerCoordinates, zoomRange, size, color) ) @@ -141,25 +174,25 @@ public fun MapFeaturesState.rectangle( zoomRange: IntRange = defaultZoomRange, size: DpSize = DpSize(5.dp, 5.dp), color: Color = Color.Red, - id: FeatureId? = null, -): FeatureId = addFeature( + id: String? = null, +): FeatureId = feature( id, MapRectangleFeature(centerCoordinates.toCoordinates(), zoomRange, size, color) ) public fun MapFeaturesState.draw( position: Pair, zoomRange: IntRange = defaultZoomRange, - id: FeatureId? = null, + id: String? = null, drawFeature: DrawScope.() -> Unit, -): FeatureId = addFeature(id, MapDrawFeature(position.toCoordinates(), zoomRange, drawFeature)) +): FeatureId = feature(id, MapDrawFeature(position.toCoordinates(), zoomRange, drawFeature)) public fun MapFeaturesState.line( aCoordinates: Gmc, bCoordinates: Gmc, zoomRange: IntRange = defaultZoomRange, color: Color = Color.Red, - id: FeatureId? = null, -): FeatureId = addFeature( + id: String? = null, +): FeatureId = feature( id, MapLineFeature(aCoordinates, bCoordinates, zoomRange, color) ) @@ -168,8 +201,8 @@ public fun MapFeaturesState.line( curve: GmcCurve, zoomRange: IntRange = defaultZoomRange, color: Color = Color.Red, - id: FeatureId? = null, -): FeatureId = addFeature( + id: String? = null, +): FeatureId = feature( id, MapLineFeature(curve.forward.coordinates, curve.backward.coordinates, zoomRange, color) ) @@ -179,8 +212,8 @@ public fun MapFeaturesState.line( bCoordinates: Pair, zoomRange: IntRange = defaultZoomRange, color: Color = Color.Red, - id: FeatureId? = null, -): FeatureId = addFeature( + id: String? = null, +): FeatureId = feature( id, MapLineFeature(aCoordinates.toCoordinates(), bCoordinates.toCoordinates(), zoomRange, color) ) @@ -191,8 +224,8 @@ public fun MapFeaturesState.arc( arcLength: Angle, zoomRange: IntRange = defaultZoomRange, color: Color = Color.Red, - id: FeatureId? = null, -): FeatureId = addFeature( + id: String? = null, +): FeatureId = feature( id, MapArcFeature(oval, startAngle, arcLength, zoomRange, color) ) @@ -204,8 +237,8 @@ public fun MapFeaturesState.arc( arcLength: Angle, zoomRange: IntRange = defaultZoomRange, color: Color = Color.Red, - id: FeatureId? = null, -): FeatureId = addFeature( + id: String? = null, +): FeatureId = feature( id, MapArcFeature( oval = GmcRectangle.square(center.toCoordinates(), radius, radius), @@ -222,8 +255,8 @@ public fun MapFeaturesState.points( stroke: Float = 2f, color: Color = Color.Red, pointMode: PointMode = PointMode.Points, - id: FeatureId? = null, -): FeatureId = addFeature(id, MapPointsFeature(points, zoomRange, stroke, color, pointMode)) + id: String? = null, +): FeatureId = feature(id, MapPointsFeature(points, zoomRange, stroke, color, pointMode)) @JvmName("pointsFromPairs") public fun MapFeaturesState.points( @@ -232,28 +265,30 @@ public fun MapFeaturesState.points( stroke: Float = 2f, color: Color = Color.Red, pointMode: PointMode = PointMode.Points, - id: FeatureId? = null, -): FeatureId = addFeature(id, MapPointsFeature(points.map { it.toCoordinates() }, zoomRange, stroke, color, pointMode)) + id: String? = null, +): FeatureId = + feature(id, MapPointsFeature(points.map { it.toCoordinates() }, zoomRange, stroke, color, pointMode)) public fun MapFeaturesState.image( position: Pair, image: ImageVector, size: DpSize = DpSize(20.dp, 20.dp), zoomRange: IntRange = defaultZoomRange, - id: FeatureId? = null, -): FeatureId = addFeature(id, MapVectorImageFeature(position.toCoordinates(), image, size, zoomRange)) + id: String? = null, +): FeatureId = + feature(id, MapVectorImageFeature(position.toCoordinates(), image, size, zoomRange)) public fun MapFeaturesState.group( zoomRange: IntRange = defaultZoomRange, - id: FeatureId? = null, + id: String? = null, builder: MapFeaturesState.() -> Unit, -): FeatureId { +): FeatureId { val map = MapFeaturesState( mutableStateMapOf(), mutableStateMapOf() ).apply(builder).features() val feature = MapFeatureGroup(map, zoomRange) - return addFeature(id, feature) + return feature(id, feature) } public fun MapFeaturesState.text( @@ -262,8 +297,8 @@ public fun MapFeaturesState.text( zoomRange: IntRange = defaultZoomRange, color: Color = Color.Red, font: MapTextFeatureFont.() -> Unit = { size = 16f }, - id: FeatureId? = null, -): FeatureId = addFeature(id, MapTextFeature(position, text, zoomRange, color, font)) + id: String? = null, +): FeatureId = feature(id, MapTextFeature(position, text, zoomRange, color, font)) public fun MapFeaturesState.text( position: Pair, @@ -271,5 +306,5 @@ public fun MapFeaturesState.text( zoomRange: IntRange = defaultZoomRange, color: Color = Color.Red, font: MapTextFeatureFont.() -> Unit = { size = 16f }, - id: FeatureId? = null, -): FeatureId = addFeature(id, MapTextFeature(position.toCoordinates(), text, zoomRange, color, font)) + id: String? = null, +): FeatureId = feature(id, MapTextFeature(position.toCoordinates(), text, zoomRange, color, font)) 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 a4b1d25..6b62d54 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 @@ -98,13 +98,13 @@ public fun MapView( mapTileProvider: MapTileProvider, initialViewPoint: MapViewPoint? = null, initialRectangle: GmcRectangle? = null, - featureMap: Map, + featureMap: Map, MapFeature>, config: MapViewConfig = MapViewConfig(), modifier: Modifier = Modifier.fillMaxSize(), ) { val featuresState = key(featureMap) { MapFeaturesState.build { - featureMap.forEach(::addFeature) + featureMap.forEach { feature(it.key.id, it.value) } } } @@ -147,7 +147,7 @@ public fun MapView( val featureDrag: DragHandle = DragHandle.withPrimaryButton { event, start, end -> featureState.forEachWithAttribute(DraggableAttribute) { _, handle -> - if(!handle.handle(event, start, end)) return@withPrimaryButton false + if (!handle.handle(event, start, end)) return@withPrimaryButton false } true } diff --git a/maps-kt-compose/src/jvmMain/kotlin/center/sciprog/maps/compose/MapTextFeature.kt b/maps-kt-compose/src/jvmMain/kotlin/center/sciprog/maps/compose/MapTextFeature.kt deleted file mode 100644 index e69de29..0000000