Multi-listener drag

This commit is contained in:
Alexander Nozik 2022-12-26 17:44:01 +03:00
parent 572adf041f
commit 7e7cb0a260
14 changed files with 165 additions and 125 deletions

View File

@ -64,35 +64,13 @@ fun App() {
image(pointOne, Icons.Filled.Home) image(pointOne, Icons.Filled.Home)
var drag1 = Gmc.ofDegrees(55.744, 37.614) val marker1 = rectangle(55.744 to 37.614, size = DpSize(10.dp, 10.dp), color = Color.Magenta)
val marker2 = rectangle(55.8 to 37.5, size = DpSize(10.dp, 10.dp), color = Color.Magenta)
val marker3 = rectangle(56.0 to 37.5, size = DpSize(10.dp, 10.dp), color = Color.Magenta)
var drag2 = Gmc.ofDegrees(55.8, 37.5) draggableLine(marker1, marker2)
draggableLine(marker2, marker3)
var drag3 = Gmc.ofDegrees(56.0, 37.5) draggableLine(marker3, marker1)
fun updateLine() {
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 ->
drag1 = end
updateLine()
}
rectangle(drag2, size = DpSize(10.dp, 10.dp)).draggable { _, end ->
drag2 = end
updateLine()
}
rectangle(drag3, size = DpSize(10.dp, 10.dp)).draggable { _, end ->
drag3 = end
updateLine()
}
updateLine()
points( points(
points = listOf( points = listOf(

View File

@ -8,7 +8,7 @@ import androidx.compose.ui.graphics.Color
import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.painterResource
import androidx.compose.ui.window.Window import androidx.compose.ui.window.Window
import androidx.compose.ui.window.application import androidx.compose.ui.window.application
import center.sciprog.maps.features.FeaturesState import center.sciprog.maps.features.FeatureCollection
import center.sciprog.maps.features.ViewConfig import center.sciprog.maps.features.ViewConfig
import center.sciprog.maps.features.ViewPoint import center.sciprog.maps.features.ViewPoint
import center.sciprog.maps.features.computeBoundingBox import center.sciprog.maps.features.computeBoundingBox
@ -29,7 +29,7 @@ fun App() {
MaterialTheme { MaterialTheme {
val scope = rememberCoroutineScope() val scope = rememberCoroutineScope()
val schemeFeaturesState = FeaturesState.remember(XYCoordinateSpace) { val schemeFeaturesState = FeatureCollection.remember(XYCoordinateSpace) {
background(1600f, 1200f) { painterResource("middle-earth.jpg") } background(1600f, 1200f) { painterResource("middle-earth.jpg") }
circle(410.52737 to 868.7676, color = Color.Blue) circle(410.52737 to 868.7676, color = Color.Blue)
text(410.52737 to 868.7676, "Shire", color = Color.Blue) text(410.52737 to 868.7676, "Shire", color = Color.Blue)

View File

@ -18,7 +18,7 @@ import kotlin.math.min
public expect fun MapView( public expect fun MapView(
mapTileProvider: MapTileProvider, mapTileProvider: MapTileProvider,
initialViewPoint: MapViewPoint, initialViewPoint: MapViewPoint,
featuresState: FeaturesState<Gmc>, featuresState: FeatureCollection<Gmc>,
config: ViewConfig<Gmc> = ViewConfig(), config: ViewConfig<Gmc> = ViewConfig(),
modifier: Modifier = Modifier.fillMaxSize(), modifier: Modifier = Modifier.fillMaxSize(),
) )
@ -51,7 +51,7 @@ public fun MapView(
modifier: Modifier = Modifier.fillMaxSize(), modifier: Modifier = Modifier.fillMaxSize(),
) { ) {
val featuresState = key(featureMap) { val featuresState = key(featureMap) {
FeaturesState.build(GmcCoordinateSpace) { FeatureCollection.build(GmcCoordinateSpace) {
featureMap.forEach { feature(it.key.id, it.value) } featureMap.forEach { feature(it.key.id, it.value) }
} }
} }
@ -80,9 +80,9 @@ public fun MapView(
initialRectangle: Rectangle<Gmc>? = null, initialRectangle: Rectangle<Gmc>? = null,
config: ViewConfig<Gmc> = ViewConfig(), config: ViewConfig<Gmc> = ViewConfig(),
modifier: Modifier = Modifier.fillMaxSize(), modifier: Modifier = Modifier.fillMaxSize(),
buildFeatures: FeaturesState<Gmc>.() -> Unit = {}, buildFeatures: FeatureCollection<Gmc>.() -> Unit = {},
) { ) {
val featureState = FeaturesState.remember(GmcCoordinateSpace, buildFeatures) val featureState = FeatureCollection.remember(GmcCoordinateSpace, buildFeatures)
val viewPointOverride: MapViewPoint = remember(initialViewPoint, initialRectangle) { val viewPointOverride: MapViewPoint = remember(initialViewPoint, initialRectangle) {
initialViewPoint initialViewPoint

View File

@ -12,12 +12,12 @@ import center.sciprog.maps.features.*
import center.sciprog.maps.features.Feature.Companion.defaultZoomRange import center.sciprog.maps.features.Feature.Companion.defaultZoomRange
internal fun FeaturesState<Gmc>.coordinatesOf(pair: Pair<Number, Number>) = internal fun FeatureCollection<Gmc>.coordinatesOf(pair: Pair<Number, Number>) =
GeodeticMapCoordinates.ofDegrees(pair.first.toDouble(), pair.second.toDouble()) GeodeticMapCoordinates.ofDegrees(pair.first.toDouble(), pair.second.toDouble())
public typealias MapFeature = Feature<Gmc> public typealias MapFeature = Feature<Gmc>
public fun FeaturesState<Gmc>.circle( public fun FeatureCollection<Gmc>.circle(
centerCoordinates: Pair<Number, Number>, centerCoordinates: Pair<Number, Number>,
zoomRange: DoubleRange = defaultZoomRange, zoomRange: DoubleRange = defaultZoomRange,
size: Dp = 5.dp, size: Dp = 5.dp,
@ -27,7 +27,7 @@ public fun FeaturesState<Gmc>.circle(
id, CircleFeature(coordinateSpace, coordinatesOf(centerCoordinates), zoomRange, size, color) id, CircleFeature(coordinateSpace, coordinatesOf(centerCoordinates), zoomRange, size, color)
) )
public fun FeaturesState<Gmc>.rectangle( public fun FeatureCollection<Gmc>.rectangle(
centerCoordinates: Pair<Number, Number>, centerCoordinates: Pair<Number, Number>,
zoomRange: DoubleRange = defaultZoomRange, zoomRange: DoubleRange = defaultZoomRange,
size: DpSize = DpSize(5.dp, 5.dp), size: DpSize = DpSize(5.dp, 5.dp),
@ -38,7 +38,7 @@ public fun FeaturesState<Gmc>.rectangle(
) )
public fun FeaturesState<Gmc>.draw( public fun FeatureCollection<Gmc>.draw(
position: Pair<Number, Number>, position: Pair<Number, Number>,
zoomRange: DoubleRange = defaultZoomRange, zoomRange: DoubleRange = defaultZoomRange,
id: String? = null, id: String? = null,
@ -49,7 +49,7 @@ public fun FeaturesState<Gmc>.draw(
) )
public fun FeaturesState<Gmc>.line( public fun FeatureCollection<Gmc>.line(
curve: GmcCurve, curve: GmcCurve,
zoomRange: DoubleRange = defaultZoomRange, zoomRange: DoubleRange = defaultZoomRange,
color: Color = Color.Red, color: Color = Color.Red,
@ -60,7 +60,7 @@ public fun FeaturesState<Gmc>.line(
) )
public fun FeaturesState<Gmc>.line( public fun FeatureCollection<Gmc>.line(
aCoordinates: Pair<Double, Double>, aCoordinates: Pair<Double, Double>,
bCoordinates: Pair<Double, Double>, bCoordinates: Pair<Double, Double>,
zoomRange: DoubleRange = defaultZoomRange, zoomRange: DoubleRange = defaultZoomRange,
@ -72,7 +72,7 @@ public fun FeaturesState<Gmc>.line(
) )
public fun FeaturesState<Gmc>.arc( public fun FeatureCollection<Gmc>.arc(
center: Pair<Double, Double>, center: Pair<Double, Double>,
radius: Distance, radius: Distance,
startAngle: Angle, startAngle: Angle,
@ -92,7 +92,7 @@ public fun FeaturesState<Gmc>.arc(
) )
) )
public fun FeaturesState<Gmc>.points( public fun FeatureCollection<Gmc>.points(
points: List<Pair<Double, Double>>, points: List<Pair<Double, Double>>,
zoomRange: DoubleRange = defaultZoomRange, zoomRange: DoubleRange = defaultZoomRange,
stroke: Float = 2f, stroke: Float = 2f,
@ -102,7 +102,7 @@ public fun FeaturesState<Gmc>.points(
): FeatureId<PointsFeature<Gmc>> = ): FeatureId<PointsFeature<Gmc>> =
feature(id, PointsFeature(coordinateSpace, points.map(::coordinatesOf), zoomRange, stroke, color, pointMode)) feature(id, PointsFeature(coordinateSpace, points.map(::coordinatesOf), zoomRange, stroke, color, pointMode))
public fun FeaturesState<Gmc>.image( public fun FeatureCollection<Gmc>.image(
position: Pair<Double, Double>, position: Pair<Double, Double>,
image: ImageVector, image: ImageVector,
size: DpSize = DpSize(20.dp, 20.dp), size: DpSize = DpSize(20.dp, 20.dp),
@ -119,7 +119,7 @@ public fun FeaturesState<Gmc>.image(
) )
) )
public fun FeaturesState<Gmc>.text( public fun FeatureCollection<Gmc>.text(
position: Pair<Double, Double>, position: Pair<Double, Double>,
text: String, text: String,
zoomRange: DoubleRange = defaultZoomRange, zoomRange: DoubleRange = defaultZoomRange,

View File

@ -42,7 +42,7 @@ private val logger = KotlinLogging.logger("MapView")
public actual fun MapView( public actual fun MapView(
mapTileProvider: MapTileProvider, mapTileProvider: MapTileProvider,
initialViewPoint: MapViewPoint, initialViewPoint: MapViewPoint,
featuresState: FeaturesState<Gmc>, featuresState: FeatureCollection<Gmc>,
config: ViewConfig<Gmc>, config: ViewConfig<Gmc>,
modifier: Modifier, modifier: Modifier,
): Unit = key(initialViewPoint) { ): Unit = key(initialViewPoint) {

View File

@ -5,8 +5,6 @@ import androidx.compose.ui.graphics.Color
public object ZAttribute : Feature.Attribute<Float> public object ZAttribute : Feature.Attribute<Float>
public object DraggableAttribute : Feature.Attribute<DragHandle<Any>>
public object SelectableAttribute : Feature.Attribute<(FeatureId<*>, SelectableFeature<*>) -> Unit> public object SelectableAttribute : Feature.Attribute<(FeatureId<*>, SelectableFeature<*>) -> Unit>
public object VisibleAttribute : Feature.Attribute<Boolean> public object VisibleAttribute : Feature.Attribute<Boolean>

View File

@ -0,0 +1,6 @@
package center.sciprog.maps.features
public object DraggableAttribute : Feature.Attribute<DragHandle<Any>>
public object DragListenerAttribute : Feature.Attribute<Set<(begin: Any, end: Any) -> Unit>>

View File

@ -49,6 +49,13 @@ public interface DraggableFeature<T : Any> : SelectableFeature<T> {
public fun withCoordinates(newCoordinates: T): Feature<T> public fun withCoordinates(newCoordinates: T): Feature<T>
} }
/**
* A draggable marker feature. Other features could be bound to this one.
*/
public interface MarkerFeature<T: Any>: DraggableFeature<T>{
public val center: T
}
public fun <T : Any> Iterable<Feature<T>>.computeBoundingBox( public fun <T : Any> Iterable<Feature<T>>.computeBoundingBox(
space: CoordinateSpace<T>, space: CoordinateSpace<T>,
zoom: Float, zoom: Float,
@ -115,12 +122,12 @@ public class PointsFeature<T : Any>(
public data class CircleFeature<T : Any>( public data class CircleFeature<T : Any>(
override val space: CoordinateSpace<T>, override val space: CoordinateSpace<T>,
public val center: T, override val center: T,
override val zoomRange: FloatRange = defaultZoomRange, override val zoomRange: FloatRange = defaultZoomRange,
public val size: Dp = 5.dp, public val size: Dp = 5.dp,
public val color: Color = Color.Red, public val color: Color = Color.Red,
override var attributes: AttributeMap = AttributeMap(), override var attributes: AttributeMap = AttributeMap(),
) : DraggableFeature<T> { ) : MarkerFeature<T> {
override fun getBoundingBox(zoom: Float): Rectangle<T> = override fun getBoundingBox(zoom: Float): Rectangle<T> =
space.Rectangle(center, zoom, DpSize(size, size)) space.Rectangle(center, zoom, DpSize(size, size))
@ -130,12 +137,12 @@ public data class CircleFeature<T : Any>(
public class RectangleFeature<T : Any>( public class RectangleFeature<T : Any>(
override val space: CoordinateSpace<T>, override val space: CoordinateSpace<T>,
public val center: T, override val center: T,
override val zoomRange: FloatRange = defaultZoomRange, override val zoomRange: FloatRange = 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 var attributes: AttributeMap = AttributeMap(), override var attributes: AttributeMap = AttributeMap(),
) : DraggableFeature<T> { ) : MarkerFeature<T> {
override fun getBoundingBox(zoom: Float): Rectangle<T> = override fun getBoundingBox(zoom: Float): Rectangle<T> =
space.Rectangle(center, zoom, size) space.Rectangle(center, zoom, size)
@ -193,35 +200,35 @@ public data class DrawFeature<T : Any>(
public data class BitmapImageFeature<T : Any>( public data class BitmapImageFeature<T : Any>(
override val space: CoordinateSpace<T>, override val space: CoordinateSpace<T>,
public val position: T, override val center: T,
public val size: DpSize, public val size: DpSize,
public val image: ImageBitmap, public val image: ImageBitmap,
override val zoomRange: FloatRange = defaultZoomRange, override val zoomRange: FloatRange = defaultZoomRange,
override var attributes: AttributeMap = AttributeMap(), override var attributes: AttributeMap = AttributeMap(),
) : DraggableFeature<T> { ) : MarkerFeature<T> {
override fun getBoundingBox(zoom: Float): Rectangle<T> = space.Rectangle(position, zoom, size) override fun getBoundingBox(zoom: Float): Rectangle<T> = space.Rectangle(center, zoom, size)
override fun withCoordinates(newCoordinates: T): Feature<T> = copy(position = newCoordinates) override fun withCoordinates(newCoordinates: T): Feature<T> = copy(center = newCoordinates)
} }
public data class VectorImageFeature<T : Any>( public data class VectorImageFeature<T : Any>(
override val space: CoordinateSpace<T>, override val space: CoordinateSpace<T>,
public val position: T, override val center: T,
public val size: DpSize, public val size: DpSize,
public val image: ImageVector, public val image: ImageVector,
override val zoomRange: FloatRange = defaultZoomRange, override val zoomRange: FloatRange = defaultZoomRange,
override var attributes: AttributeMap = AttributeMap(), override var attributes: AttributeMap = AttributeMap(),
) : DraggableFeature<T>, PainterFeature<T> { ) : MarkerFeature<T>, PainterFeature<T> {
override fun getBoundingBox(zoom: Float): Rectangle<T> = space.Rectangle(position, zoom, size) override fun getBoundingBox(zoom: Float): Rectangle<T> = space.Rectangle(center, zoom, size)
override fun withCoordinates(newCoordinates: T): Feature<T> = copy(position = newCoordinates) override fun withCoordinates(newCoordinates: T): Feature<T> = copy(center = newCoordinates)
@Composable @Composable
override fun painter(): VectorPainter = rememberVectorPainter(image) override fun painter(): VectorPainter = rememberVectorPainter(image)
} }
/** /**
* A background image that is bound to coordinates and is scaled together with them * An image that is bound to coordinates and is scaled together with them
* *
* @param rectangle the size of background in scheme size units. The screen units to scheme units ratio equals scale. * @param rectangle the size of background in scheme size units. The screen units to scheme units ratio equals scale.
*/ */

View File

@ -18,10 +18,23 @@ import kotlinx.coroutines.isActive
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
@JvmInline @JvmInline
public value class FeatureId<out MapFeature>(public val id: String) public value class FeatureId<out F : Feature<*>>(public val id: String)
public class FeaturesState<T : Any>(public val coordinateSpace: CoordinateSpace<T>) : public interface FeatureBuilder<T : Any> {
CoordinateSpace<T> by coordinateSpace {
public val coordinateSpace: CoordinateSpace<T>
public fun <F : Feature<T>> feature(id: String?, feature: F): FeatureId<F>
public fun <F : Feature<T>, V> setAttribute(id: FeatureId<F>, key: Feature.Attribute<V>, value: V?)
}
public fun <T : Any, F : Feature<T>> FeatureBuilder<T>.feature(id: FeatureId<F>, feature: F): FeatureId<F> =
feature(id.id, feature)
public class FeatureCollection<T : Any>(
override val coordinateSpace: CoordinateSpace<T>,
) : CoordinateSpace<T> by coordinateSpace, FeatureBuilder<T> {
@PublishedApi @PublishedApi
internal val featureMap: MutableMap<String, Feature<T>> = mutableStateMapOf() internal val featureMap: MutableMap<String, Feature<T>> = mutableStateMapOf()
@ -30,21 +43,21 @@ public class FeaturesState<T : Any>(public val coordinateSpace: CoordinateSpace<
get() = featureMap.mapKeys { FeatureId<Feature<T>>(it.key) } get() = featureMap.mapKeys { FeatureId<Feature<T>>(it.key) }
@Suppress("UNCHECKED_CAST") @Suppress("UNCHECKED_CAST")
public fun <F : Feature<T>> getFeature(id: FeatureId<F>): F = featureMap[id.id] as F public operator fun <F : Feature<T>> get(id: FeatureId<F>): F = featureMap[id.id] as F
private fun generateID(feature: Feature<T>): String = "@feature[${feature.hashCode().toUInt()}]" private fun generateID(feature: Feature<T>): String = "@feature[${feature.hashCode().toUInt()}]"
public fun <F : Feature<T>> feature(id: String?, feature: F): FeatureId<F> { override fun <F : Feature<T>> feature(id: String?, feature: F): FeatureId<F> {
val safeId = id ?: generateID(feature) val safeId = id ?: generateID(feature)
featureMap[safeId] = feature featureMap[safeId] = feature
return FeatureId(safeId) return FeatureId(safeId)
} }
public fun <F : Feature<T>> feature(id: FeatureId<F>?, feature: F): FeatureId<F> = feature(id?.id, feature) public fun <F : Feature<T>> feature(id: FeatureId<F>, feature: F): FeatureId<F> = feature(id.id, feature)
public fun <F : Feature<T>, V> setAttribute(id: FeatureId<F>, key: Feature.Attribute<V>, value: V?) { override fun <F : Feature<T>, V> setAttribute(id: FeatureId<F>, key: Feature.Attribute<V>, value: V?) {
getFeature(id).attributes[key] = value get(id).attributes[key] = value
} }
/** /**
@ -54,26 +67,38 @@ public class FeaturesState<T : Any>(public val coordinateSpace: CoordinateSpace<
* *
* TODO use context receiver for that * TODO use context receiver for that
*/ */
@Suppress("UNCHECKED_CAST")
public fun FeatureId<DraggableFeature<T>>.draggable( public fun FeatureId<DraggableFeature<T>>.draggable(
constraint: ((T) -> T)? = null, constraint: ((T) -> T)? = null,
callback: ((start: T, end: T) -> Unit) = { _, _ -> }, callback: ((start: T, end: T) -> Unit)? = null,
) { ) {
@Suppress("UNCHECKED_CAST") if (getAttribute(this, DraggableAttribute) == null) {
val handle = DragHandle.withPrimaryButton<Any> { _, start, end -> val handle = DragHandle.withPrimaryButton<Any> { _, start, end ->
val startPosition = start.focus as T val feature = featureMap[id] as? DraggableFeature ?: return@withPrimaryButton DragResult(end)
val endPosition = end.focus as T val startPosition = start.focus as T
val feature = featureMap[id] as? DraggableFeature ?: return@withPrimaryButton DragResult(end) val endPosition = end.focus as T
val boundingBox = feature.getBoundingBox(start.zoom) ?: return@withPrimaryButton DragResult(end) val boundingBox = feature.getBoundingBox(start.zoom) ?: return@withPrimaryButton DragResult(end)
if (startPosition in boundingBox) { if (startPosition in boundingBox) {
val finalPosition = constraint?.invoke(endPosition) ?: endPosition val finalPosition = constraint?.invoke(endPosition) ?: endPosition
feature(id, feature.withCoordinates(finalPosition)) feature(id, feature.withCoordinates(finalPosition))
callback(startPosition, finalPosition) feature.attributes[DragListenerAttribute]?.forEach {
DragResult(ViewPoint(finalPosition, end.zoom), false) it.invoke(startPosition, endPosition)
} else { }
DragResult(end, true) DragResult(ViewPoint(finalPosition, end.zoom), false)
} else {
DragResult(end, true)
}
} }
setAttribute(this, DraggableAttribute, handle)
}
//Apply callback
if (callback != null) {
setAttribute(
this, DragListenerAttribute,
((getAttribute(this, DragListenerAttribute) ?: emptySet()) + callback) as Set<(Any, Any) -> Unit>
)
} }
setAttribute(this, DraggableAttribute, handle)
} }
/** /**
@ -84,7 +109,7 @@ public class FeaturesState<T : Any>(public val coordinateSpace: CoordinateSpace<
update: suspend (F) -> F, update: suspend (F) -> F,
): Job = scope.launch { ): Job = scope.launch {
while (isActive) { while (isActive) {
feature(this@updated, update(getFeature(this@updated))) feature(this@updated, update(get(this@updated)))
} }
} }
@ -98,15 +123,7 @@ public class FeaturesState<T : Any>(public val coordinateSpace: CoordinateSpace<
@Suppress("UNCHECKED_CAST") @Suppress("UNCHECKED_CAST")
public fun <A> getAttribute(id: FeatureId<Feature<T>>, key: Feature.Attribute<A>): A? = public fun <A> getAttribute(id: FeatureId<Feature<T>>, key: Feature.Attribute<A>): A? =
getFeature(id).attributes[key] get(id).attributes[key]
// @Suppress("UNCHECKED_CAST")
// public fun <T> findAllWithAttribute(key: Attribute<T>, condition: (T) -> Boolean): Set<FeatureId> {
// return attributes.filterValues {
// condition(it[key] as T)
// }.keys
// }
/** /**
* Process all features with a given attribute from the one with highest [z] to lowest * Process all features with a given attribute from the one with highest [z] to lowest
@ -129,8 +146,8 @@ public class FeaturesState<T : Any>(public val coordinateSpace: CoordinateSpace<
*/ */
public fun <T : Any> build( public fun <T : Any> build(
coordinateSpace: CoordinateSpace<T>, coordinateSpace: CoordinateSpace<T>,
builder: FeaturesState<T>.() -> Unit = {}, builder: FeatureCollection<T>.() -> Unit = {},
): FeaturesState<T> = FeaturesState(coordinateSpace).apply(builder) ): FeatureCollection<T> = FeatureCollection(coordinateSpace).apply(builder)
/** /**
* Build and remember map feature state * Build and remember map feature state
@ -138,15 +155,15 @@ public class FeaturesState<T : Any>(public val coordinateSpace: CoordinateSpace<
@Composable @Composable
public fun <T : Any> remember( public fun <T : Any> remember(
coordinateSpace: CoordinateSpace<T>, coordinateSpace: CoordinateSpace<T>,
builder: FeaturesState<T>.() -> Unit = {}, builder: FeatureCollection<T>.() -> Unit = {},
): FeaturesState<T> = remember(builder) { ): FeatureCollection<T> = remember(builder) {
build(coordinateSpace, builder) build(coordinateSpace, builder)
} }
} }
} }
public fun <T : Any> FeaturesState<T>.circle( public fun <T : Any> FeatureBuilder<T>.circle(
center: T, center: T,
zoomRange: FloatRange = defaultZoomRange, zoomRange: FloatRange = defaultZoomRange,
size: Dp = 5.dp, size: Dp = 5.dp,
@ -156,7 +173,7 @@ public fun <T : Any> FeaturesState<T>.circle(
id, CircleFeature(coordinateSpace, center, zoomRange, size, color) id, CircleFeature(coordinateSpace, center, zoomRange, size, color)
) )
public fun <T : Any> FeaturesState<T>.rectangle( public fun <T : Any> FeatureBuilder<T>.rectangle(
centerCoordinates: T, centerCoordinates: T,
zoomRange: FloatRange = defaultZoomRange, zoomRange: FloatRange = defaultZoomRange,
size: DpSize = DpSize(5.dp, 5.dp), size: DpSize = DpSize(5.dp, 5.dp),
@ -166,7 +183,7 @@ public fun <T : Any> FeaturesState<T>.rectangle(
id, RectangleFeature(coordinateSpace, centerCoordinates, zoomRange, size, color) id, RectangleFeature(coordinateSpace, centerCoordinates, zoomRange, size, color)
) )
public fun <T : Any> FeaturesState<T>.draw( public fun <T : Any> FeatureBuilder<T>.draw(
position: T, position: T,
zoomRange: FloatRange = defaultZoomRange, zoomRange: FloatRange = defaultZoomRange,
id: String? = null, id: String? = null,
@ -176,7 +193,7 @@ public fun <T : Any> FeaturesState<T>.draw(
DrawFeature(coordinateSpace, position, zoomRange, drawFeature = draw) DrawFeature(coordinateSpace, position, zoomRange, drawFeature = draw)
) )
public fun <T : Any> FeaturesState<T>.line( public fun <T : Any> FeatureBuilder<T>.line(
aCoordinates: T, aCoordinates: T,
bCoordinates: T, bCoordinates: T,
zoomRange: FloatRange = defaultZoomRange, zoomRange: FloatRange = defaultZoomRange,
@ -187,7 +204,7 @@ public fun <T : Any> FeaturesState<T>.line(
LineFeature(coordinateSpace, aCoordinates, bCoordinates, zoomRange, color) LineFeature(coordinateSpace, aCoordinates, bCoordinates, zoomRange, color)
) )
public fun <T : Any> FeaturesState<T>.arc( public fun <T : Any> FeatureBuilder<T>.arc(
oval: Rectangle<T>, oval: Rectangle<T>,
startAngle: Float, startAngle: Float,
arcLength: Float, arcLength: Float,
@ -199,7 +216,7 @@ public fun <T : Any> FeaturesState<T>.arc(
ArcFeature(coordinateSpace, oval, startAngle, arcLength, zoomRange, color) ArcFeature(coordinateSpace, oval, startAngle, arcLength, zoomRange, color)
) )
public fun <T : Any> FeaturesState<T>.points( public fun <T : Any> FeatureBuilder<T>.points(
points: List<T>, points: List<T>,
zoomRange: FloatRange = defaultZoomRange, zoomRange: FloatRange = defaultZoomRange,
stroke: Float = 2f, stroke: Float = 2f,
@ -209,7 +226,7 @@ public fun <T : Any> FeaturesState<T>.points(
): FeatureId<PointsFeature<T>> = ): FeatureId<PointsFeature<T>> =
feature(id, PointsFeature(coordinateSpace, points, zoomRange, stroke, color, pointMode)) feature(id, PointsFeature(coordinateSpace, points, zoomRange, stroke, color, pointMode))
public fun <T : Any> FeaturesState<T>.image( public fun <T : Any> FeatureBuilder<T>.image(
position: T, position: T,
image: ImageVector, image: ImageVector,
zoomRange: FloatRange = defaultZoomRange, zoomRange: FloatRange = defaultZoomRange,
@ -227,17 +244,17 @@ public fun <T : Any> FeaturesState<T>.image(
) )
) )
public fun <T : Any> FeaturesState<T>.group( public fun <T : Any> FeatureBuilder<T>.group(
zoomRange: FloatRange = defaultZoomRange, zoomRange: FloatRange = defaultZoomRange,
id: String? = null, id: String? = null,
builder: FeaturesState<T>.() -> Unit, builder: FeatureCollection<T>.() -> Unit,
): FeatureId<FeatureGroup<T>> { ): FeatureId<FeatureGroup<T>> {
val map = FeaturesState(coordinateSpace).apply(builder).features val map = FeatureCollection(coordinateSpace).apply(builder).features
val feature = FeatureGroup(coordinateSpace, map, zoomRange) val feature = FeatureGroup(coordinateSpace, map, zoomRange)
return feature(id, feature) return feature(id, feature)
} }
public fun <T : Any> FeaturesState<T>.scalableImage( public fun <T : Any> FeatureBuilder<T>.scalableImage(
box: Rectangle<T>, box: Rectangle<T>,
zoomRange: FloatRange = defaultZoomRange, zoomRange: FloatRange = defaultZoomRange,
id: String? = null, id: String? = null,
@ -247,7 +264,7 @@ public fun <T : Any> FeaturesState<T>.scalableImage(
ScalableImageFeature<T>(coordinateSpace, box, zoomRange, painter = painter) ScalableImageFeature<T>(coordinateSpace, box, zoomRange, painter = painter)
) )
public fun <T : Any> FeaturesState<T>.text( public fun <T : Any> FeatureBuilder<T>.text(
position: T, position: T,
text: String, text: String,
zoomRange: FloatRange = defaultZoomRange, zoomRange: FloatRange = defaultZoomRange,

View File

@ -0,0 +1,34 @@
package center.sciprog.maps.features
import androidx.compose.ui.graphics.Color
public fun <T : Any> FeatureCollection<T>.draggableLine(
aId: FeatureId<MarkerFeature<T>>,
bId: FeatureId<MarkerFeature<T>>,
zoomRange: FloatRange = Feature.defaultZoomRange,
color: Color = Color.Red,
id: String? = null,
): FeatureId<LineFeature<T>> {
var lineId: FeatureId<LineFeature<T>>? = null
fun drawLine(): FeatureId<LineFeature<T>> = line(
get(aId).center,
get(bId).center,
zoomRange,
color,
lineId?.id ?: id
).also {
lineId = it
}
aId.draggable { _, _ ->
drawLine()
}
bId.draggable { _, _ ->
drawLine()
}
return drawLine()
}

View File

@ -62,10 +62,10 @@ public fun <T : Any> DrawScope.drawFeature(
} }
is BitmapImageFeature -> drawImage(feature.image, feature.position.toOffset()) is BitmapImageFeature -> drawImage(feature.image, feature.center.toOffset())
is VectorImageFeature -> { is VectorImageFeature -> {
val offset = feature.position.toOffset() val offset = feature.center.toOffset()
val size = feature.size.toSize() val size = feature.size.toSize()
translate(offset.x - size.width / 2, offset.y - size.height / 2) { translate(offset.x - size.width / 2, offset.y - size.height / 2) {
with(painterCache[feature]!!) { with(painterCache[feature]!!) {

View File

@ -12,7 +12,7 @@ import center.sciprog.maps.features.*
internal fun Pair<Number, Number>.toCoordinates(): XY = XY(first.toFloat(), second.toFloat()) internal fun Pair<Number, Number>.toCoordinates(): XY = XY(first.toFloat(), second.toFloat())
fun FeaturesState<XY>.background( fun FeatureCollection<XY>.background(
width: Float, width: Float,
height: Float, height: Float,
offset: XY = XY(0f, 0f), offset: XY = XY(0f, 0f),
@ -31,7 +31,7 @@ fun FeaturesState<XY>.background(
) )
} }
fun FeaturesState<XY>.circle( fun FeatureCollection<XY>.circle(
centerCoordinates: Pair<Number, Number>, centerCoordinates: Pair<Number, Number>,
zoomRange: FloatRange = Feature.defaultZoomRange, zoomRange: FloatRange = Feature.defaultZoomRange,
size: Dp = 5.dp, size: Dp = 5.dp,
@ -39,14 +39,14 @@ fun FeaturesState<XY>.circle(
id: String? = null, id: String? = null,
): FeatureId<CircleFeature<XY>> = circle(centerCoordinates.toCoordinates(), zoomRange, size, color, id = id) ): FeatureId<CircleFeature<XY>> = circle(centerCoordinates.toCoordinates(), zoomRange, size, color, id = id)
fun FeaturesState<XY>.draw( fun FeatureCollection<XY>.draw(
position: Pair<Number, Number>, position: Pair<Number, Number>,
zoomRange: FloatRange = Feature.defaultZoomRange, zoomRange: FloatRange = Feature.defaultZoomRange,
id: String? = null, id: String? = null,
draw: DrawScope.() -> Unit, draw: DrawScope.() -> Unit,
): FeatureId<DrawFeature<XY>> = draw(position.toCoordinates(), zoomRange = zoomRange, id = id, draw = draw) ): FeatureId<DrawFeature<XY>> = draw(position.toCoordinates(), zoomRange = zoomRange, id = id, draw = draw)
fun FeaturesState<XY>.line( fun FeatureCollection<XY>.line(
aCoordinates: Pair<Number, Number>, aCoordinates: Pair<Number, Number>,
bCoordinates: Pair<Number, Number>, bCoordinates: Pair<Number, Number>,
scaleRange: FloatRange = Feature.defaultZoomRange, scaleRange: FloatRange = Feature.defaultZoomRange,
@ -55,7 +55,7 @@ fun FeaturesState<XY>.line(
): FeatureId<LineFeature<XY>> = line(aCoordinates.toCoordinates(), bCoordinates.toCoordinates(), scaleRange, color, id) ): FeatureId<LineFeature<XY>> = line(aCoordinates.toCoordinates(), bCoordinates.toCoordinates(), scaleRange, color, id)
public fun FeaturesState<XY>.arc( public fun FeatureCollection<XY>.arc(
center: Pair<Double, Double>, center: Pair<Double, Double>,
radius: Float, radius: Float,
startAngle: Float, startAngle: Float,
@ -71,7 +71,7 @@ public fun FeaturesState<XY>.arc(
color = color color = color
) )
fun FeaturesState<XY>.image( fun FeatureCollection<XY>.image(
position: Pair<Number, Number>, position: Pair<Number, Number>,
image: ImageVector, image: ImageVector,
size: DpSize = DpSize(image.defaultWidth, image.defaultHeight), size: DpSize = DpSize(image.defaultWidth, image.defaultHeight),
@ -80,7 +80,7 @@ fun FeaturesState<XY>.image(
): FeatureId<VectorImageFeature<XY>> = ): FeatureId<VectorImageFeature<XY>> =
image(position.toCoordinates(), image, size = size, zoomRange = zoomRange, id = id) image(position.toCoordinates(), image, size = size, zoomRange = zoomRange, id = id)
fun FeaturesState<XY>.text( fun FeatureCollection<XY>.text(
position: Pair<Number, Number>, position: Pair<Number, Number>,
text: String, text: String,
zoomRange: FloatRange = Feature.defaultZoomRange, zoomRange: FloatRange = Feature.defaultZoomRange,

View File

@ -23,7 +23,7 @@ private val logger = KotlinLogging.logger("SchemeView")
@Composable @Composable
public fun SchemeView( public fun SchemeView(
initialViewPoint: ViewPoint<XY>, initialViewPoint: ViewPoint<XY>,
featuresState: FeaturesState<XY>, featuresState: FeatureCollection<XY>,
config: ViewConfig<XY>, config: ViewConfig<XY>,
modifier: Modifier = Modifier.fillMaxSize(), modifier: Modifier = Modifier.fillMaxSize(),
) = key(initialViewPoint) { ) = key(initialViewPoint) {
@ -96,7 +96,7 @@ public fun SchemeView(
modifier: Modifier = Modifier.fillMaxSize(), modifier: Modifier = Modifier.fillMaxSize(),
) { ) {
val featuresState = key(featureMap) { val featuresState = key(featureMap) {
FeaturesState.build(XYCoordinateSpace) { FeatureCollection.build(XYCoordinateSpace) {
featureMap.forEach { feature(it.key.id, it.value) } featureMap.forEach { feature(it.key.id, it.value) }
} }
} }
@ -124,9 +124,9 @@ public fun SchemeView(
initialRectangle: Rectangle<XY>? = null, initialRectangle: Rectangle<XY>? = null,
config: ViewConfig<XY> = ViewConfig(), config: ViewConfig<XY> = ViewConfig(),
modifier: Modifier = Modifier.fillMaxSize(), modifier: Modifier = Modifier.fillMaxSize(),
buildFeatures: FeaturesState<XY>.() -> Unit = {}, buildFeatures: FeatureCollection<XY>.() -> Unit = {},
) { ) {
val featureState = FeaturesState.remember(XYCoordinateSpace, buildFeatures) val featureState = FeatureCollection.remember(XYCoordinateSpace, buildFeatures)
val viewPointOverride: ViewPoint<XY> = remember(initialViewPoint, initialRectangle) { val viewPointOverride: ViewPoint<XY> = remember(initialViewPoint, initialRectangle) {
initialViewPoint initialViewPoint

View File

@ -22,7 +22,7 @@ class FeatureStateSnapshot<T : Any>(
) )
@Composable @Composable
fun <T: Any> FeaturesState<T>.snapshot(): FeatureStateSnapshot<T> = FeatureStateSnapshot( fun <T: Any> FeatureCollection<T>.snapshot(): FeatureStateSnapshot<T> = FeatureStateSnapshot(
features, features,
features.values.filterIsInstance<PainterFeature<T>>().associateWith { it.painter() } features.values.filterIsInstance<PainterFeature<T>>().associateWith { it.painter() }
) )
@ -83,10 +83,10 @@ fun FeatureStateSnapshot<XY>.generateSvg(
) )
} }
is BitmapImageFeature -> drawImage(feature.image, feature.position.toOffset()) is BitmapImageFeature -> drawImage(feature.image, feature.center.toOffset())
is VectorImageFeature -> { is VectorImageFeature -> {
val offset = feature.position.toOffset() val offset = feature.center.toOffset()
val imageSize = feature.size.toSize() val imageSize = feature.size.toSize()
translate(offset.x - imageSize.width / 2, offset.y - imageSize.height / 2) { translate(offset.x - imageSize.width / 2, offset.y - imageSize.height / 2) {
with(painterCache[feature]!!) { with(painterCache[feature]!!) {