Introduce Attribute builders

This commit is contained in:
Alexander Nozik 2023-01-06 10:26:29 +03:00
parent 69e7b058d2
commit ffc77dc611
10 changed files with 76413 additions and 72 deletions

View File

@ -29,7 +29,6 @@ import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.isActive import kotlinx.coroutines.isActive
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import java.net.URL
import java.nio.file.Path import java.nio.file.Path
import kotlin.math.PI import kotlin.math.PI
import kotlin.random.Random import kotlin.random.Random
@ -69,9 +68,9 @@ fun App() {
) )
) { ) {
geoJson(URL("https://raw.githubusercontent.com/ggolikov/cities-comparison/master/src/moscow.geo.json")) geoJson(javaClass.getResource("/moscow.geo.json")!!)
.attribute(ColorAttribute, Color.Blue) .modifyAttribute(ColorAttribute, Color.Blue)
.attribute(AlphaAttribute, 0.4f) .modifyAttribute(AlphaAttribute, 0.4f)
image(pointOne, Icons.Filled.Home) image(pointOne, Icons.Filled.Home)

File diff suppressed because it is too large Load Diff

View File

@ -79,7 +79,7 @@ fun App() {
) )
} }
) { ) {
val mapState: XYViewScope = rememberMapState( val mapState: XYViewScope = XYViewScope.remember(
ViewConfig( ViewConfig(
onClick = {_, click -> onClick = {_, click ->
println("${click.focus.x}, ${click.focus.y}") println("${click.focus.x}, ${click.focus.y}")

View File

@ -19,7 +19,7 @@ public value class Attributes internal constructor(internal val map: Map<Attribu
} }
} }
public fun <T, A : Attribute<T>> Attributes.attribute( public fun <T, A : Attribute<T>> Attributes.withAttribute(
attribute: A, attribute: A,
attrValue: T?, attrValue: T?,
): Attributes = Attributes( ): Attributes = Attributes(
@ -33,7 +33,7 @@ public fun <T, A : Attribute<T>> Attributes.attribute(
/** /**
* Add an element to a [SetAttribute] * Add an element to a [SetAttribute]
*/ */
public fun <T, A : SetAttribute<T>> Attributes.addValue( public fun <T, A : SetAttribute<T>> Attributes.withAttributeElement(
attribute: A, attribute: A,
attrValue: T, attrValue: T,
): Attributes { ): Attributes {
@ -46,7 +46,7 @@ public fun <T, A : SetAttribute<T>> Attributes.addValue(
/** /**
* Remove an element from [SetAttribute] * Remove an element from [SetAttribute]
*/ */
public fun <T, A : SetAttribute<T>> Attributes.removeValue( public fun <T, A : SetAttribute<T>> Attributes.withoutAttributeElement(
attribute: A, attribute: A,
attrValue: T, attrValue: T,
): Attributes { ): Attributes {
@ -56,8 +56,6 @@ public fun <T, A : SetAttribute<T>> Attributes.removeValue(
) )
} }
public fun <T : Any, A : Attribute<T>> Attributes( public fun <T : Any, A : Attribute<T>> Attributes(
attribute: A, attribute: A,
attrValue: T, attrValue: T,

View File

@ -0,0 +1,45 @@
package center.sciprog.attributes
/**
* A safe builder for [Attributes]
*/
public class AttributesBuilder internal constructor(private val map: MutableMap<Attribute<*>, Any> = mutableMapOf()) {
@Suppress("UNCHECKED_CAST")
public operator fun <T> get(attribute: Attribute<T>): T? = map[attribute] as? T
public operator fun <V> Attribute<V>.invoke(value: V?) {
if (value == null) {
map.remove(this)
} else {
map[this] = value
}
}
public fun from(attributes: Attributes) {
map.putAll(attributes.map)
}
public fun <V> SetAttribute<V>.add(
attrValue: V,
) {
val currentSet: Set<V> = get(this) ?: emptySet()
map[this] = currentSet + attrValue
}
/**
* Remove an element from [SetAttribute]
*/
public fun <V> SetAttribute<V>.remove(
attrValue: V,
) {
val currentSet: Set<V> = get(this) ?: emptySet()
map[this] = currentSet - attrValue
}
public fun build(): Attributes = Attributes(map)
}
public fun AttributesBuilder(
attributes: Attributes,
): AttributesBuilder = AttributesBuilder(attributes.map.toMutableMap())

View File

@ -74,25 +74,28 @@ public data class FeatureGroup<T : Any>(
public fun <A> getAttribute(id: FeatureId<Feature<T>>, key: Attribute<A>): A? = public fun <A> getAttribute(id: FeatureId<Feature<T>>, key: Attribute<A>): A? =
get(id).attributes[key] get(id).attributes[key]
public fun <F : Feature<T>> FeatureId<F>.modifyAttributes(modify: Attributes.() -> Attributes): FeatureId<F> {
feature(this, get(this).withAttributes(modify)) override fun getBoundingBox(zoom: Float): Rectangle<T>? = with(space) {
featureMap.values.mapNotNull { it.getBoundingBox(zoom) }.wrapRectangles()
}
override fun withAttributes(modify: Attributes.() -> Attributes): Feature<T> = copy(attributes = modify(attributes))
public fun <F : Feature<T>> FeatureId<F>.modifyAttributes(modify: AttributesBuilder.() -> Unit): FeatureId<F> {
feature(
this,
get(this).withAttributes {
AttributesBuilder(this).apply(modify).build()
}
)
return this return this
} }
public fun <F : Feature<T>, V> FeatureId<F>.attribute(key: Attribute<V>, value: V?): FeatureId<F> { public fun <F : Feature<T>, V> FeatureId<F>.modifyAttribute(key: Attribute<V>, value: V?): FeatureId<F> {
feature(this, get(this).withAttributes { attribute(key, value) }) feature(this, get(this).withAttributes { withAttribute(key, value) })
return this return this
} }
/**
* Add multi-entry [SetAttribute] value
*/
public fun <F : Feature<T>, V> FeatureId<F>.addAttribute(key: SetAttribute<V>, value: V): FeatureId<F> {
feature(this, get(this).withAttributes { addValue(key, value) })
return this
}
/** /**
* Add drag to this feature * Add drag to this feature
* *
@ -101,10 +104,10 @@ public data class FeatureGroup<T : Any>(
* TODO use context receiver for that * TODO use context receiver for that
*/ */
@Suppress("UNCHECKED_CAST") @Suppress("UNCHECKED_CAST")
public fun FeatureId<DraggableFeature<T>>.draggable( public fun <F : DraggableFeature<T>> FeatureId<F>.draggable(
constraint: ((T) -> T)? = null, constraint: ((T) -> T)? = null,
listener: (PointerEvent.(from: ViewPoint<T>, to: ViewPoint<T>) -> Unit)? = null, listener: (PointerEvent.(from: ViewPoint<T>, to: ViewPoint<T>) -> Unit)? = null,
) { ): FeatureId<F> {
if (getAttribute(this, DraggableAttribute) == null) { if (getAttribute(this, DraggableAttribute) == null) {
val handle = DragHandle.withPrimaryButton<Any> { event, start, end -> val handle = DragHandle.withPrimaryButton<Any> { event, start, end ->
val feature = featureMap[id] as? DraggableFeature<T> ?: return@withPrimaryButton DragResult(end) val feature = featureMap[id] as? DraggableFeature<T> ?: return@withPrimaryButton DragResult(end)
@ -121,70 +124,51 @@ public data class FeatureGroup<T : Any>(
DragResult(end, true) DragResult(end, true)
} }
} }
this.attribute(DraggableAttribute, handle) modifyAttribute(DraggableAttribute, handle)
} }
//Apply callback //Apply callback
if (listener != null) { if (listener != null) {
onDrag(listener) onDrag(listener)
} }
return this
} }
@Suppress("UNCHECKED_CAST") @Suppress("UNCHECKED_CAST")
public fun FeatureId<DraggableFeature<T>>.onDrag( public fun <F : DraggableFeature<T>> FeatureId<F>.onDrag(
listener: PointerEvent.(from: ViewPoint<T>, to: ViewPoint<T>) -> Unit, listener: PointerEvent.(from: ViewPoint<T>, to: ViewPoint<T>) -> Unit,
) { ): FeatureId<F> = modifyAttributes {
addAttribute( DragListenerAttribute.add(
DragListenerAttribute,
DragListener { event, from, to -> event.listener(from as ViewPoint<T>, to as ViewPoint<T>) } DragListener { event, from, to -> event.listener(from as ViewPoint<T>, to as ViewPoint<T>) }
) )
} }
@Suppress("UNCHECKED_CAST") @Suppress("UNCHECKED_CAST")
public fun <F : DomainFeature<T>> FeatureId<F>.onClick( public fun <F : DomainFeature<T>> FeatureId<F>.onClick(
onClick: PointerEvent.(click: ViewPoint<T>) -> Unit, onClick: PointerEvent.(click: ViewPoint<T>) -> Unit,
) { ): FeatureId<F> = modifyAttributes {
addAttribute( ClickListenerAttribute.add(
ClickListenerAttribute,
MouseListener { event, point -> event.onClick(point as ViewPoint<T>) } MouseListener { event, point -> event.onClick(point as ViewPoint<T>) }
) )
} }
@Suppress("UNCHECKED_CAST") @Suppress("UNCHECKED_CAST")
public fun <F : DomainFeature<T>> FeatureId<F>.onHover( public fun <F : DomainFeature<T>> FeatureId<F>.onHover(
onClick: PointerEvent.(move: ViewPoint<T>) -> Unit, onClick: PointerEvent.(move: ViewPoint<T>) -> Unit,
) { ): FeatureId<F> = modifyAttributes {
addAttribute( HoverListenerAttribute.add(
HoverListenerAttribute,
MouseListener { event, point -> event.onClick(point as ViewPoint<T>) } MouseListener { event, point -> event.onClick(point as ViewPoint<T>) }
) )
} }
// /**
// * Cyclic update of a feature. Called infinitely until canceled.
// */
// public fun <F : Feature<T>> FeatureId<F>.updated(
// scope: CoroutineScope,
// update: suspend (F) -> F,
// ): Job = scope.launch {
// while (isActive) {
// feature(this@updated, update(get(this@updated)))
// }
// }
public fun <F : Feature<T>> FeatureId<F>.color(color: Color): FeatureId<F> = public fun <F : Feature<T>> FeatureId<F>.color(color: Color): FeatureId<F> =
attribute(ColorAttribute, color) modifyAttribute(ColorAttribute, color)
public fun <F : Feature<T>> FeatureId<F>.zoomRange(range: FloatRange): FeatureId<F> = public fun <F : Feature<T>> FeatureId<F>.zoomRange(range: FloatRange): FeatureId<F> =
attribute(ZoomRangeAttribute, range) modifyAttribute(ZoomRangeAttribute, range)
override fun getBoundingBox(zoom: Float): Rectangle<T>? = with(space) {
featureMap.values.mapNotNull { it.getBoundingBox(zoom) }.wrapRectangles()
}
override fun withAttributes(modify: Attributes.() -> Attributes): Feature<T> = copy(attributes = modify(attributes))
public companion object { public companion object {

View File

@ -2,7 +2,6 @@ package center.sciprog.maps.features
import center.sciprog.attributes.Attributes import center.sciprog.attributes.Attributes
import center.sciprog.attributes.ZAttribute import center.sciprog.attributes.ZAttribute
import center.sciprog.attributes.attribute
public fun <T : Any> FeatureGroup<T>.draggableLine( public fun <T : Any> FeatureGroup<T>.draggableLine(
@ -20,7 +19,10 @@ public fun <T : Any> FeatureGroup<T>.draggableLine(
get(bId).center, get(bId).center,
lineId?.id ?: id lineId?.id ?: id
) )
if (attributes != null) currentId.modifyAttributes { attributes.attribute(ZAttribute, -10f) } currentId.modifyAttributes {
ZAttribute(-10f)
if (attributes != null) from(attributes)
}
lineId = currentId lineId = currentId
return currentId return currentId
} }

View File

@ -0,0 +1,8 @@
package center.sciprog.maps.geojson
import center.sciprog.attributes.Attribute
import kotlinx.serialization.json.JsonObject
public object GeoJsonPropertiesAttribute : Attribute<JsonObject>
public object GeoJsonNameAttribute : Attribute<String>

View File

@ -60,7 +60,7 @@ public fun FeatureGroup<Gmc>.geoJsonFeature(
id: String? = null, id: String? = null,
): FeatureId<Feature<Gmc>> { ): FeatureId<Feature<Gmc>> {
val geometry = geoJson.geometry ?: return group{} val geometry = geoJson.geometry ?: return group{}
val idOverride = geoJson.properties?.get("id")?.jsonPrimitive?.contentOrNull ?: id val idOverride = geoJson.json["id"]?.jsonPrimitive?.contentOrNull ?: geoJson.properties?.get("id")?.jsonPrimitive?.contentOrNull ?: id
val colorOverride = geoJson.properties?.get("color")?.jsonPrimitive?.intOrNull?.let { Color(it) } val colorOverride = geoJson.properties?.get("color")?.jsonPrimitive?.intOrNull?.let { Color(it) }
val jsonGeometry = geoJsonGeometry(geometry, idOverride) val jsonGeometry = geoJsonGeometry(geometry, idOverride)
return if( colorOverride!= null){ return if( colorOverride!= null){

View File

@ -43,19 +43,22 @@ class XYViewScope(
val bottomRight = rightBottom.toDpOffset() val bottomRight = rightBottom.toDpOffset()
return DpRect(topLeft.x, topLeft.y, bottomRight.x, bottomRight.y) return DpRect(topLeft.x, topLeft.y, bottomRight.x, bottomRight.y)
} }
}
@Composable companion object{
public fun rememberMapState( @Composable
config: ViewConfig<XY>, public fun remember(
initialViewPoint: ViewPoint<XY>? = null, config: ViewConfig<XY>,
initialRectangle: Rectangle<XY>? = null, initialViewPoint: ViewPoint<XY>? = null,
): XYViewScope = remember { initialRectangle: Rectangle<XY>? = null,
XYViewScope(config).also { mapState-> ): XYViewScope = remember {
if (initialViewPoint != null) { XYViewScope(config).also { mapState->
mapState.viewPoint = initialViewPoint if (initialViewPoint != null) {
} else if (initialRectangle != null) { mapState.viewPoint = initialViewPoint
mapState.viewPoint = mapState.computeViewPoint(initialRectangle) } else if (initialRectangle != null) {
mapState.viewPoint = mapState.computeViewPoint(initialRectangle)
}
}
} }
} }
} }