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.isActive
import kotlinx.coroutines.launch
import java.net.URL
import java.nio.file.Path
import kotlin.math.PI
import kotlin.random.Random
@ -69,9 +68,9 @@ fun App() {
)
) {
geoJson(URL("https://raw.githubusercontent.com/ggolikov/cities-comparison/master/src/moscow.geo.json"))
.attribute(ColorAttribute, Color.Blue)
.attribute(AlphaAttribute, 0.4f)
geoJson(javaClass.getResource("/moscow.geo.json")!!)
.modifyAttribute(ColorAttribute, Color.Blue)
.modifyAttribute(AlphaAttribute, 0.4f)
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(
onClick = {_, click ->
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,
attrValue: T?,
): Attributes = Attributes(
@ -33,7 +33,7 @@ public fun <T, A : Attribute<T>> Attributes.attribute(
/**
* Add an element to a [SetAttribute]
*/
public fun <T, A : SetAttribute<T>> Attributes.addValue(
public fun <T, A : SetAttribute<T>> Attributes.withAttributeElement(
attribute: A,
attrValue: T,
): Attributes {
@ -46,7 +46,7 @@ public fun <T, A : SetAttribute<T>> Attributes.addValue(
/**
* Remove an element from [SetAttribute]
*/
public fun <T, A : SetAttribute<T>> Attributes.removeValue(
public fun <T, A : SetAttribute<T>> Attributes.withoutAttributeElement(
attribute: A,
attrValue: T,
): Attributes {
@ -56,8 +56,6 @@ public fun <T, A : SetAttribute<T>> Attributes.removeValue(
)
}
public fun <T : Any, A : Attribute<T>> Attributes(
attribute: A,
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? =
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
}
public fun <F : Feature<T>, V> FeatureId<F>.attribute(key: Attribute<V>, value: V?): FeatureId<F> {
feature(this, get(this).withAttributes { attribute(key, value) })
public fun <F : Feature<T>, V> FeatureId<F>.modifyAttribute(key: Attribute<V>, value: V?): FeatureId<F> {
feature(this, get(this).withAttributes { withAttribute(key, value) })
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
*
@ -101,10 +104,10 @@ public data class FeatureGroup<T : Any>(
* TODO use context receiver for that
*/
@Suppress("UNCHECKED_CAST")
public fun FeatureId<DraggableFeature<T>>.draggable(
public fun <F : DraggableFeature<T>> FeatureId<F>.draggable(
constraint: ((T) -> T)? = null,
listener: (PointerEvent.(from: ViewPoint<T>, to: ViewPoint<T>) -> Unit)? = null,
) {
): FeatureId<F> {
if (getAttribute(this, DraggableAttribute) == null) {
val handle = DragHandle.withPrimaryButton<Any> { event, start, 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)
}
}
this.attribute(DraggableAttribute, handle)
modifyAttribute(DraggableAttribute, handle)
}
//Apply callback
if (listener != null) {
onDrag(listener)
}
return this
}
@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,
) {
addAttribute(
DragListenerAttribute,
): FeatureId<F> = modifyAttributes {
DragListenerAttribute.add(
DragListener { event, from, to -> event.listener(from as ViewPoint<T>, to as ViewPoint<T>) }
)
}
@Suppress("UNCHECKED_CAST")
public fun <F : DomainFeature<T>> FeatureId<F>.onClick(
onClick: PointerEvent.(click: ViewPoint<T>) -> Unit,
) {
addAttribute(
ClickListenerAttribute,
): FeatureId<F> = modifyAttributes {
ClickListenerAttribute.add(
MouseListener { event, point -> event.onClick(point as ViewPoint<T>) }
)
}
@Suppress("UNCHECKED_CAST")
public fun <F : DomainFeature<T>> FeatureId<F>.onHover(
onClick: PointerEvent.(move: ViewPoint<T>) -> Unit,
) {
addAttribute(
HoverListenerAttribute,
): FeatureId<F> = modifyAttributes {
HoverListenerAttribute.add(
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> =
attribute(ColorAttribute, color)
modifyAttribute(ColorAttribute, color)
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 {

View File

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

View File

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