[WIP] attributes refactoring

This commit is contained in:
Alexander Nozik 2022-12-10 13:41:54 +03:00
parent edeb422335
commit 8451cc3aa1
No known key found for this signature in database
GPG Key ID: F7FCF2DD25C71357
4 changed files with 58 additions and 23 deletions

View File

@ -111,7 +111,7 @@ fun App() {
//remember feature ID //remember feature ID
circle( circle(
centerCoordinates = pointTwo, centerCoordinates = pointTwo,
).updates(scope) { ).updated(scope) {
delay(200) delay(200)
//Overwrite a feature with new color //Overwrite a feature with new color
it.copy(color = Color(Random.nextFloat(), Random.nextFloat(), Random.nextFloat())) it.copy(color = Color(Random.nextFloat(), Random.nextFloat(), Random.nextFloat()))

View File

@ -16,7 +16,12 @@ import center.sciprog.maps.coordinates.*
import kotlin.math.floor import kotlin.math.floor
public interface MapFeature { public interface MapFeature {
public interface Attribute<T>
public val zoomRange: IntRange public val zoomRange: IntRange
public val attributes: AttributeMap
public fun getBoundingBox(zoom: Double): GmcRectangle? public fun getBoundingBox(zoom: Double): GmcRectangle?
} }
@ -33,7 +38,7 @@ public interface DraggableMapFeature : SelectableMapFeature {
public fun Iterable<MapFeature>.computeBoundingBox(zoom: Double): GmcRectangle? = public fun Iterable<MapFeature>.computeBoundingBox(zoom: Double): GmcRectangle? =
mapNotNull { it.getBoundingBox(zoom) }.wrapAll() mapNotNull { it.getBoundingBox(zoom) }.wrapAll()
public fun Pair<Number, Number>.toCoordinates() = public fun Pair<Number, Number>.toCoordinates(): GeodeticMapCoordinates =
GeodeticMapCoordinates.ofDegrees(first.toDouble(), second.toDouble()) GeodeticMapCoordinates.ofDegrees(first.toDouble(), second.toDouble())
internal val defaultZoomRange = 1..18 internal val defaultZoomRange = 1..18
@ -42,6 +47,7 @@ internal val defaultZoomRange = 1..18
* A feature that decides what to show depending on the zoom value (it could change size of shape) * A feature that decides what to show depending on the zoom value (it could change size of shape)
*/ */
public class MapFeatureSelector( public class MapFeatureSelector(
override val attributes: AttributeMap = AttributeMap(),
public val selector: (zoom: Int) -> MapFeature, public val selector: (zoom: Int) -> MapFeature,
) : MapFeature { ) : MapFeature {
override val zoomRange: IntRange get() = defaultZoomRange override val zoomRange: IntRange get() = defaultZoomRange
@ -52,6 +58,7 @@ public class MapFeatureSelector(
public class MapDrawFeature( public class MapDrawFeature(
public val position: GeodeticMapCoordinates, public val position: GeodeticMapCoordinates,
override val zoomRange: IntRange = defaultZoomRange, override val zoomRange: IntRange = defaultZoomRange,
override val attributes: AttributeMap = AttributeMap(),
public val drawFeature: DrawScope.() -> Unit, public val drawFeature: DrawScope.() -> Unit,
) : DraggableMapFeature { ) : DraggableMapFeature {
override fun getBoundingBox(zoom: Double): GmcRectangle { override fun getBoundingBox(zoom: Double): GmcRectangle {
@ -60,7 +67,7 @@ public class MapDrawFeature(
} }
override fun withCoordinates(newCoordinates: GeodeticMapCoordinates): MapFeature = override fun withCoordinates(newCoordinates: GeodeticMapCoordinates): MapFeature =
MapDrawFeature(newCoordinates, zoomRange, drawFeature) MapDrawFeature(newCoordinates, zoomRange, attributes, drawFeature)
} }
public class MapPathFeature( public class MapPathFeature(
@ -70,6 +77,7 @@ public class MapPathFeature(
public val style: DrawStyle = Fill, public val style: DrawStyle = Fill,
public val targetRect: Rect = path.getBounds(), public val targetRect: Rect = path.getBounds(),
override val zoomRange: IntRange = defaultZoomRange, override val zoomRange: IntRange = defaultZoomRange,
override val attributes: AttributeMap = AttributeMap(),
) : DraggableMapFeature { ) : DraggableMapFeature {
override fun withCoordinates(newCoordinates: GeodeticMapCoordinates): MapFeature = override fun withCoordinates(newCoordinates: GeodeticMapCoordinates): MapFeature =
MapPathFeature(rectangle.moveTo(newCoordinates), path, brush, style, targetRect, zoomRange) MapPathFeature(rectangle.moveTo(newCoordinates), path, brush, style, targetRect, zoomRange)
@ -84,6 +92,7 @@ public class MapPointsFeature(
public val stroke: Float = 2f, public val stroke: Float = 2f,
public val color: Color = Color.Red, public val color: Color = Color.Red,
public val pointMode: PointMode = PointMode.Points, public val pointMode: PointMode = PointMode.Points,
override val attributes: AttributeMap = AttributeMap(),
) : MapFeature { ) : MapFeature {
override fun getBoundingBox(zoom: Double): GmcRectangle = GmcRectangle(points.first(), points.last()) override fun getBoundingBox(zoom: Double): GmcRectangle = GmcRectangle(points.first(), points.last())
} }
@ -93,6 +102,7 @@ public data class MapCircleFeature(
override val zoomRange: IntRange = defaultZoomRange, override val zoomRange: IntRange = defaultZoomRange,
public val size: Float = 5f, public val size: Float = 5f,
public val color: Color = Color.Red, public val color: Color = Color.Red,
override val attributes: AttributeMap = AttributeMap(),
) : DraggableMapFeature { ) : DraggableMapFeature {
override fun getBoundingBox(zoom: Double): GmcRectangle { override fun getBoundingBox(zoom: Double): GmcRectangle {
val scale = WebMercatorProjection.scaleFactor(zoom) val scale = WebMercatorProjection.scaleFactor(zoom)
@ -100,7 +110,7 @@ public data class MapCircleFeature(
} }
override fun withCoordinates(newCoordinates: GeodeticMapCoordinates): MapFeature = override fun withCoordinates(newCoordinates: GeodeticMapCoordinates): MapFeature =
MapCircleFeature(newCoordinates, zoomRange, size, color) MapCircleFeature(newCoordinates, zoomRange, size, color, attributes)
} }
public class MapRectangleFeature( public class MapRectangleFeature(
@ -108,6 +118,7 @@ public class MapRectangleFeature(
override val zoomRange: IntRange = defaultZoomRange, override val zoomRange: IntRange = defaultZoomRange,
public val size: DpSize = DpSize(5.dp, 5.dp), public val size: DpSize = DpSize(5.dp, 5.dp),
public val color: Color = Color.Red, public val color: Color = Color.Red,
override val attributes: AttributeMap = AttributeMap(),
) : DraggableMapFeature { ) : DraggableMapFeature {
override fun getBoundingBox(zoom: Double): GmcRectangle { override fun getBoundingBox(zoom: Double): GmcRectangle {
val scale = WebMercatorProjection.scaleFactor(zoom) val scale = WebMercatorProjection.scaleFactor(zoom)
@ -115,7 +126,7 @@ public class MapRectangleFeature(
} }
override fun withCoordinates(newCoordinates: GeodeticMapCoordinates): MapFeature = override fun withCoordinates(newCoordinates: GeodeticMapCoordinates): MapFeature =
MapRectangleFeature(newCoordinates, zoomRange, size, color) MapRectangleFeature(newCoordinates, zoomRange, size, color, attributes)
} }
public class MapLineFeature( public class MapLineFeature(
@ -123,6 +134,7 @@ public class MapLineFeature(
public val b: GeodeticMapCoordinates, public val b: GeodeticMapCoordinates,
override val zoomRange: IntRange = defaultZoomRange, override val zoomRange: IntRange = defaultZoomRange,
public val color: Color = Color.Red, public val color: Color = Color.Red,
override val attributes: AttributeMap = AttributeMap(),
) : SelectableMapFeature { ) : SelectableMapFeature {
override fun getBoundingBox(zoom: Double): GmcRectangle = GmcRectangle(a, b) override fun getBoundingBox(zoom: Double): GmcRectangle = GmcRectangle(a, b)
@ -141,11 +153,12 @@ public class MapArcFeature(
public val arcLength: Angle, public val arcLength: Angle,
override val zoomRange: IntRange = defaultZoomRange, override val zoomRange: IntRange = defaultZoomRange,
public val color: Color = Color.Red, public val color: Color = Color.Red,
override val attributes: AttributeMap = AttributeMap(),
) : DraggableMapFeature { ) : DraggableMapFeature {
override fun getBoundingBox(zoom: Double): GmcRectangle = oval override fun getBoundingBox(zoom: Double): GmcRectangle = oval
override fun withCoordinates(newCoordinates: GeodeticMapCoordinates): MapFeature = override fun withCoordinates(newCoordinates: GeodeticMapCoordinates): MapFeature =
MapArcFeature(oval.moveTo(newCoordinates), startAngle, arcLength, zoomRange, color) MapArcFeature(oval.moveTo(newCoordinates), startAngle, arcLength, zoomRange, color, attributes)
} }
public class MapBitmapImageFeature( public class MapBitmapImageFeature(
@ -153,11 +166,12 @@ public class MapBitmapImageFeature(
public val image: ImageBitmap, public val image: ImageBitmap,
public val size: IntSize = IntSize(15, 15), public val size: IntSize = IntSize(15, 15),
override val zoomRange: IntRange = defaultZoomRange, override val zoomRange: IntRange = defaultZoomRange,
override val attributes: AttributeMap = AttributeMap(),
) : DraggableMapFeature { ) : DraggableMapFeature {
override fun getBoundingBox(zoom: Double): GmcRectangle = GmcRectangle(position, position) override fun getBoundingBox(zoom: Double): GmcRectangle = GmcRectangle(position, position)
override fun withCoordinates(newCoordinates: GeodeticMapCoordinates): MapFeature = override fun withCoordinates(newCoordinates: GeodeticMapCoordinates): MapFeature =
MapBitmapImageFeature(newCoordinates, image, size, zoomRange) MapBitmapImageFeature(newCoordinates, image, size, zoomRange, attributes)
} }
public class MapVectorImageFeature( public class MapVectorImageFeature(
@ -165,11 +179,12 @@ public class MapVectorImageFeature(
public val image: ImageVector, public val image: ImageVector,
public val size: DpSize = DpSize(20.dp, 20.dp), public val size: DpSize = DpSize(20.dp, 20.dp),
override val zoomRange: IntRange = defaultZoomRange, override val zoomRange: IntRange = defaultZoomRange,
override val attributes: AttributeMap = AttributeMap(),
) : DraggableMapFeature { ) : DraggableMapFeature {
override fun getBoundingBox(zoom: Double): GmcRectangle = GmcRectangle(position, position) override fun getBoundingBox(zoom: Double): GmcRectangle = GmcRectangle(position, position)
override fun withCoordinates(newCoordinates: GeodeticMapCoordinates): MapFeature = override fun withCoordinates(newCoordinates: GeodeticMapCoordinates): MapFeature =
MapVectorImageFeature(newCoordinates, image, size, zoomRange) MapVectorImageFeature(newCoordinates, image, size, zoomRange, attributes)
@Composable @Composable
public fun painter(): VectorPainter = rememberVectorPainter(image) public fun painter(): VectorPainter = rememberVectorPainter(image)
@ -181,6 +196,7 @@ public class MapVectorImageFeature(
public class MapFeatureGroup( public class MapFeatureGroup(
public val children: Map<FeatureId<*>, MapFeature>, public val children: Map<FeatureId<*>, MapFeature>,
override val zoomRange: IntRange = defaultZoomRange, override val zoomRange: IntRange = defaultZoomRange,
override val attributes: AttributeMap = AttributeMap(),
) : MapFeature { ) : MapFeature {
override fun getBoundingBox(zoom: Double): GmcRectangle? = override fun getBoundingBox(zoom: Double): GmcRectangle? =
children.values.mapNotNull { it.getBoundingBox(zoom) }.wrapAll() children.values.mapNotNull { it.getBoundingBox(zoom) }.wrapAll()
@ -190,11 +206,12 @@ public class MapTextFeature(
public val position: GeodeticMapCoordinates, public val position: GeodeticMapCoordinates,
public val text: String, public val text: String,
override val zoomRange: IntRange = defaultZoomRange, override val zoomRange: IntRange = defaultZoomRange,
public val color: Color, public val color: Color = Color.Black,
override val attributes: AttributeMap = AttributeMap(),
public val fontConfig: MapTextFeatureFont.() -> Unit, public val fontConfig: MapTextFeatureFont.() -> Unit,
) : DraggableMapFeature { ) : DraggableMapFeature {
override fun getBoundingBox(zoom: Double): GmcRectangle = GmcRectangle(position, position) override fun getBoundingBox(zoom: Double): GmcRectangle = GmcRectangle(position, position)
override fun withCoordinates(newCoordinates: GeodeticMapCoordinates): MapFeature = override fun withCoordinates(newCoordinates: GeodeticMapCoordinates): MapFeature =
MapTextFeature(newCoordinates, text, zoomRange, color, fontConfig) MapTextFeature(newCoordinates, text, zoomRange, color, attributes, fontConfig)
} }

View File

@ -3,7 +3,6 @@ package center.sciprog.maps.compose
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.mutableStateMapOf import androidx.compose.runtime.mutableStateMapOf
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
import androidx.compose.runtime.snapshots.SnapshotStateMap
import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.PointMode import androidx.compose.ui.graphics.PointMode
import androidx.compose.ui.graphics.drawscope.DrawScope import androidx.compose.ui.graphics.drawscope.DrawScope
@ -19,15 +18,10 @@ import kotlinx.coroutines.launch
@JvmInline @JvmInline
public value class FeatureId<out MapFeature>(public val id: String) public value class FeatureId<out MapFeature>(public val id: String)
public object DraggableAttribute : MapFeaturesState.Attribute<DragHandle>
public object SelectableAttribute : MapFeaturesState.Attribute<(FeatureId<*>, SelectableMapFeature) -> Unit>
public class MapFeaturesState internal constructor( public class MapFeaturesState internal constructor(
private val featureMap: MutableMap<String, MapFeature>, private val featureMap: MutableMap<String, MapFeature>,
@PublishedApi internal val attributeMap: MutableMap<String, SnapshotStateMap<Attribute<out Any?>, in Any?>>, //@PublishedApi internal val attributeMap: MutableMap<String, SnapshotStateMap<Attribute<out Any?>, in Any?>>,
) { ) {
public interface Attribute<T>
//TODO use context receiver for that //TODO use context receiver for that
public fun FeatureId<DraggableMapFeature>.draggable( public fun FeatureId<DraggableMapFeature>.draggable(
//TODO add constraints //TODO add constraints
@ -50,12 +44,12 @@ public class MapFeaturesState internal constructor(
/** /**
* Cyclic update of a feature. Called infinitely until canceled. * Cyclic update of a feature. Called infinitely until canceled.
*/ */
public fun <T : MapFeature> FeatureId<T>.updates( public fun <T : MapFeature> FeatureId<T>.updated(
scope: CoroutineScope, scope: CoroutineScope,
update: suspend (T) -> T, update: suspend (T) -> T,
): Job = scope.launch { ): Job = scope.launch {
while (isActive) { while (isActive) {
feature(this@updates, update(getFeature(this@updates))) feature(this@updated, update(getFeature(this@updated)))
} }
} }
@ -83,16 +77,17 @@ public class MapFeaturesState internal constructor(
public fun <T : MapFeature> feature(id: FeatureId<T>?, feature: T): FeatureId<T> = feature(id?.id, feature) public fun <T : MapFeature> feature(id: FeatureId<T>?, feature: T): FeatureId<T> = feature(id?.id, feature)
public fun <T> setAttribute(id: FeatureId<*>, key: Attribute<T>, value: T) { public fun <T> setAttribute(id: FeatureId<*>, key: MapFeature.Attribute<T>, value: T) {
feature(id,getFeature(id).at)
attributeMap.getOrPut(id.id) { mutableStateMapOf() }[key] = value attributeMap.getOrPut(id.id) { mutableStateMapOf() }[key] = value
} }
public fun removeAttribute(id: FeatureId<*>, key: Attribute<*>) { public fun removeAttribute(id: FeatureId<*>, key: MapFeature.Attribute<*>) {
attributeMap[id.id]?.remove(key) attributeMap[id.id]?.remove(key)
} }
@Suppress("UNCHECKED_CAST") @Suppress("UNCHECKED_CAST")
public fun <T> getAttribute(id: FeatureId<*>, key: Attribute<T>): T? = public fun <T> getAttribute(id: FeatureId<*>, key: MapFeature.Attribute<T>): T? =
attributeMap[id.id]?.get(key)?.let { it as T } attributeMap[id.id]?.get(key)?.let { it as T }
// @Suppress("UNCHECKED_CAST") // @Suppress("UNCHECKED_CAST")
@ -103,7 +98,7 @@ public class MapFeaturesState internal constructor(
// } // }
public inline fun <T> forEachWithAttribute( public inline fun <T> forEachWithAttribute(
key: Attribute<T>, key: MapFeature.Attribute<T>,
block: (id: FeatureId<*>, attributeValue: T) -> Unit, block: (id: FeatureId<*>, attributeValue: T) -> Unit,
) { ) {
attributeMap.forEach { (id, attributeMap) -> attributeMap.forEach { (id, attributeMap) ->

View File

@ -0,0 +1,23 @@
package center.sciprog.maps.compose
import androidx.compose.ui.graphics.Color
public object DraggableAttribute : MapFeature.Attribute<DragHandle>
public object SelectableAttribute : MapFeature.Attribute<(FeatureId<*>, SelectableMapFeature) -> Unit>
public object VisibleAttribute : MapFeature.Attribute<Boolean>
public object ColorAttribute: MapFeature.Attribute<Color>
@JvmInline
public value class AttributeMap internal constructor(private val map: Map<MapFeature.Attribute<*>, *>) {
public fun <T, A : MapFeature.Attribute<T>> withAttribute(
attribute: A,
value: T,
): AttributeMap = AttributeMap(map + (attribute to value))
@Suppress("UNCHECKED_CAST")
public operator fun <T> get(attribute: MapFeature.Attribute<T>): T? = map[attribute] as? T
}
public fun AttributeMap(): AttributeMap = AttributeMap(emptyMap<MapFeature.Attribute<*>, Any?>())