Change the working of groups

This commit is contained in:
Alexander Nozik 2023-01-02 14:08:20 +03:00
parent 26c3e589da
commit a8a3da7e70
17 changed files with 424 additions and 534 deletions

View File

@ -22,6 +22,8 @@ import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.filterNotNull
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
@ -64,13 +66,13 @@ fun App() {
image(pointOne, Icons.Filled.Home)
val marker1 = rectangle(55.744 to 38.614, size = DpSize(10.dp, 10.dp), color = Color.Magenta)
val marker2 = rectangle(55.8 to 38.5, size = DpSize(10.dp, 10.dp), color = Color.Magenta)
val marker3 = rectangle(56.0 to 38.5, size = DpSize(10.dp, 10.dp), color = Color.Magenta)
val marker1 = rectangle(55.744 to 38.614, size = DpSize(10.dp, 10.dp)).color(Color.Magenta)
val marker2 = rectangle(55.8 to 38.5, size = DpSize(10.dp, 10.dp)).color(Color.Magenta)
val marker3 = rectangle(56.0 to 38.5, size = DpSize(10.dp, 10.dp)).color(Color.Magenta)
draggableLine(marker1, marker2, color = Color.Blue)
draggableLine(marker2, marker3, color = Color.Blue)
draggableLine(marker3, marker1, color = Color.Blue)
draggableLine(marker1, marker2).color(Color.Blue)
draggableLine(marker2, marker3).color(Color.Blue)
draggableLine(marker3, marker1).color(Color.Blue)
points(
points = listOf(
@ -85,13 +87,16 @@ fun App() {
)
//remember feature ID
circle(
val circleId = circle(
centerCoordinates = pointTwo,
).updated(scope) {
)
scope.launch {
while (isActive) {
delay(200)
//Overwrite a feature with new color
it.copy(color = Color(Random.nextFloat(), Random.nextFloat(), Random.nextFloat()))
circleId.color(Color(Random.nextFloat(), Random.nextFloat(), Random.nextFloat()))
}
}
// draw(position = pointThree) {
// drawLine(start = Offset(-10f, -10f), end = Offset(10f, 10f), color = Color.Red)
@ -105,16 +110,20 @@ fun App() {
centerCoordinates.filterNotNull().onEach {
group(id = "center") {
circle(center = it, color = Color.Blue, id = "circle", size = 1.dp)
text(position = it, it.toShortString(), id = "text", color = Color.Blue)
circle(center = it, id = "circle", size = 1.dp).color(Color.Blue)
text(position = it, it.toShortString(), id = "text").color(Color.Blue)
}
}.launchIn(scope)
features.forEach { (id, feature) ->
visit { id, feature ->
if (feature is PolygonFeature) {
(id as FeatureId<PolygonFeature<Gmc>>).onHover {
println("Hover on $id")
points(feature.points, color = Color.Blue, id = "selected", attributes = Attributes(ZAttribute, 10f))
points(
feature.points,
id = "selected",
attributes = Attributes(ZAttribute, 10f)
).color(Color.Blue)
}
}
}

View File

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

View File

@ -11,7 +11,7 @@ import center.sciprog.maps.features.*
@Composable
public expect fun MapView(
mapState: MapViewScope,
featuresState: FeatureCollection<Gmc>,
featuresState: FeatureGroup<Gmc>,
modifier: Modifier = Modifier.fillMaxSize(),
)
@ -29,7 +29,7 @@ public fun MapView(
) {
val featureState = remember(featureMap) {
FeatureCollection.build(WebMercatorSpace) {
FeatureGroup.build(WebMercatorSpace) {
featureMap.forEach { feature(it.key.id, it.value) }
}
}
@ -38,7 +38,7 @@ public fun MapView(
mapTileProvider,
config,
initialViewPoint = initialViewPoint,
initialRectangle = initialRectangle ?: featureState.features.values.computeBoundingBox(WebMercatorSpace, Float.MAX_VALUE),
initialRectangle = initialRectangle ?: featureState.features.computeBoundingBox(WebMercatorSpace, Float.MAX_VALUE),
)
MapView(mapState, featureState, modifier)
@ -58,16 +58,16 @@ public fun MapView(
initialRectangle: Rectangle<Gmc>? = null,
config: ViewConfig<Gmc> = ViewConfig(),
modifier: Modifier = Modifier.fillMaxSize(),
buildFeatures: FeatureCollection<Gmc>.() -> Unit = {},
buildFeatures: FeatureGroup<Gmc>.() -> Unit = {},
) {
val featureState = FeatureCollection.remember(WebMercatorSpace, buildFeatures)
val featureState = FeatureGroup.remember(WebMercatorSpace, buildFeatures)
val mapState: MapViewScope = rememberMapState(
mapTileProvider,
config,
initialViewPoint = initialViewPoint,
initialRectangle = initialRectangle ?: featureState.features.values.computeBoundingBox(WebMercatorSpace, Float.MAX_VALUE),
initialRectangle = initialRectangle ?: featureState.features.computeBoundingBox(WebMercatorSpace, Float.MAX_VALUE),
)
MapView(mapState, featureState, modifier)

View File

@ -1,6 +1,5 @@
package center.sciprog.maps.compose
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.PointMode
import androidx.compose.ui.graphics.drawscope.DrawScope
import androidx.compose.ui.graphics.vector.ImageVector
@ -11,73 +10,62 @@ import center.sciprog.maps.coordinates.*
import center.sciprog.maps.features.*
internal fun FeatureBuilder<Gmc>.coordinatesOf(pair: Pair<Number, Number>) =
internal fun FeatureGroup<Gmc>.coordinatesOf(pair: Pair<Number, Number>) =
GeodeticMapCoordinates.ofDegrees(pair.first.toDouble(), pair.second.toDouble())
public typealias MapFeature = Feature<Gmc>
public fun FeatureBuilder<Gmc>.circle(
public fun FeatureGroup<Gmc>.circle(
centerCoordinates: Pair<Number, Number>,
zoomRange: FloatRange = defaultZoomRange,
size: Dp = 5.dp,
color: Color = defaultColor,
id: String? = null,
): FeatureId<CircleFeature<Gmc>> = feature(
id, CircleFeature(space, coordinatesOf(centerCoordinates), zoomRange, size, color)
id, CircleFeature(space, coordinatesOf(centerCoordinates), size)
)
public fun FeatureBuilder<Gmc>.rectangle(
public fun FeatureGroup<Gmc>.rectangle(
centerCoordinates: Pair<Number, Number>,
zoomRange: FloatRange = defaultZoomRange,
size: DpSize = DpSize(5.dp, 5.dp),
color: Color = defaultColor,
id: String? = null,
): FeatureId<RectangleFeature<Gmc>> = feature(
id, RectangleFeature(space, coordinatesOf(centerCoordinates), zoomRange, size, color)
id, RectangleFeature(space, coordinatesOf(centerCoordinates), size)
)
public fun FeatureBuilder<Gmc>.draw(
public fun FeatureGroup<Gmc>.draw(
position: Pair<Number, Number>,
zoomRange: FloatRange = defaultZoomRange,
id: String? = null,
draw: DrawScope.() -> Unit,
): FeatureId<DrawFeature<Gmc>> = feature(
id,
DrawFeature(space, coordinatesOf(position), zoomRange, drawFeature = draw)
DrawFeature(space, coordinatesOf(position), drawFeature = draw)
)
public fun FeatureBuilder<Gmc>.line(
public fun FeatureGroup<Gmc>.line(
curve: GmcCurve,
zoomRange: FloatRange = defaultZoomRange,
color: Color = defaultColor,
id: String? = null,
): FeatureId<LineFeature<Gmc>> = feature(
id,
LineFeature(space, curve.forward.coordinates, curve.backward.coordinates, zoomRange, color)
LineFeature(space, curve.forward.coordinates, curve.backward.coordinates)
)
public fun FeatureBuilder<Gmc>.line(
public fun FeatureGroup<Gmc>.line(
aCoordinates: Pair<Double, Double>,
bCoordinates: Pair<Double, Double>,
zoomRange: FloatRange = defaultZoomRange,
color: Color = defaultColor,
id: String? = null,
): FeatureId<LineFeature<Gmc>> = feature(
id,
LineFeature(space, coordinatesOf(aCoordinates), coordinatesOf(bCoordinates), zoomRange, color)
LineFeature(space, coordinatesOf(aCoordinates), coordinatesOf(bCoordinates))
)
public fun FeatureBuilder<Gmc>.arc(
public fun FeatureGroup<Gmc>.arc(
center: Pair<Double, Double>,
radius: Distance,
startAngle: Angle,
arcLength: Angle,
zoomRange: FloatRange = defaultZoomRange,
color: Color = defaultColor,
id: String? = null,
): FeatureId<ArcFeature<Gmc>> = feature(
id,
@ -86,26 +74,21 @@ public fun FeatureBuilder<Gmc>.arc(
oval = space.Rectangle(coordinatesOf(center), radius, radius),
startAngle = startAngle.radians.toFloat(),
arcLength = arcLength.radians.toFloat(),
zoomRange = zoomRange,
color = color
)
)
public fun FeatureBuilder<Gmc>.points(
public fun FeatureGroup<Gmc>.points(
points: List<Pair<Double, Double>>,
zoomRange: FloatRange = defaultZoomRange,
stroke: Float = 2f,
color: Color = defaultColor,
pointMode: PointMode = PointMode.Points,
id: String? = null,
): FeatureId<PointsFeature<Gmc>> =
feature(id, PointsFeature(space, points.map(::coordinatesOf), zoomRange, stroke, color, pointMode))
feature(id, PointsFeature(space, points.map(::coordinatesOf), stroke, pointMode))
public fun FeatureBuilder<Gmc>.image(
public fun FeatureGroup<Gmc>.image(
position: Pair<Double, Double>,
image: ImageVector,
size: DpSize = DpSize(20.dp, 20.dp),
zoomRange: FloatRange = defaultZoomRange,
id: String? = null,
): FeatureId<VectorImageFeature<Gmc>> = feature(
id,
@ -114,18 +97,15 @@ public fun FeatureBuilder<Gmc>.image(
coordinatesOf(position),
size,
image,
zoomRange
)
)
public fun FeatureBuilder<Gmc>.text(
public fun FeatureGroup<Gmc>.text(
position: Pair<Double, Double>,
text: String,
zoomRange: FloatRange = defaultZoomRange,
color: Color = defaultColor,
font: FeatureFont.() -> Unit = { size = 16f },
id: String? = null,
): FeatureId<TextFeature<Gmc>> = feature(
id,
TextFeature(space, coordinatesOf(position), text, zoomRange, color, fontConfig = font)
TextFeature(space, coordinatesOf(position), text, fontConfig = font)
)

View File

@ -15,10 +15,10 @@ import androidx.compose.ui.unit.IntOffset
import androidx.compose.ui.unit.IntSize
import androidx.compose.ui.unit.dp
import center.sciprog.maps.coordinates.Gmc
import center.sciprog.maps.features.FeatureCollection
import center.sciprog.maps.features.FeatureGroup
import center.sciprog.maps.features.PainterFeature
import center.sciprog.maps.features.drawFeature
import center.sciprog.maps.features.z
import center.sciprog.maps.features.zoomRange
import kotlinx.coroutines.launch
import kotlinx.coroutines.supervisorScope
import mu.KotlinLogging
@ -44,7 +44,7 @@ private val logger = KotlinLogging.logger("MapView")
@Composable
public actual fun MapView(
mapState: MapViewScope,
featuresState: FeatureCollection<Gmc>,
featuresState: FeatureGroup<Gmc>,
modifier: Modifier,
): Unit = with(mapState) {
@ -90,9 +90,10 @@ public actual fun MapView(
}
val painterCache: Map<PainterFeature<Gmc>, Painter> = key(featuresState) {
featuresState.features.values.filterIsInstance<PainterFeature<Gmc>>().associateWith { it.getPainter() }
featuresState.features.filterIsInstance<PainterFeature<Gmc>>().associateWith { it.getPainter() }
}
Canvas(modifier = modifier.mapControls(mapState, featuresState.features).fillMaxSize()) {
if (canvasSize != size.toDpSize()) {
@ -119,7 +120,7 @@ public actual fun MapView(
)
}
featuresState.features.values.filter { viewPoint.zoom in it.zoomRange }.sortedBy { it.z }
featuresState.features.filter { viewPoint.zoom in it.zoomRange }
.forEach { feature ->
drawFeature(mapState, painterCache, feature)
}

View File

@ -18,4 +18,6 @@ public object VisibleAttribute : Attribute<Boolean>
public object ColorAttribute : Attribute<Color>
public object ZoomRangeAttribute : Attribute<FloatRange>
public object AlphaAttribute : Attribute<Float>

View File

@ -21,8 +21,6 @@ public interface Feature<T : Any> {
public val space: CoordinateSpace<T>
public val zoomRange: FloatRange
public val attributes: Attributes
public fun getBoundingBox(zoom: Float): Rectangle<T>?
@ -30,6 +28,11 @@ public interface Feature<T : Any> {
public fun withAttributes(modify: Attributes.() -> Attributes): Feature<T>
}
public val Feature<*>.color: Color? get() = attributes[ColorAttribute]
public val Feature<*>.zoomRange: FloatRange
get() = attributes[ZoomRangeAttribute] ?: Float.NEGATIVE_INFINITY..Float.POSITIVE_INFINITY
public interface PainterFeature<T : Any> : Feature<T> {
@Composable
public fun getPainter(): Painter
@ -65,7 +68,6 @@ public fun <T : Any> Iterable<Feature<T>>.computeBoundingBox(
@Stable
public data class FeatureSelector<T : Any>(
override val space: CoordinateSpace<T>,
override val zoomRange: FloatRange,
override val attributes: Attributes = Attributes.EMPTY,
public val selector: (zoom: Float) -> Feature<T>,
) : Feature<T> {
@ -81,7 +83,6 @@ public data class PathFeature<T : Any>(
public val rectangle: Rectangle<T>,
public val path: Path,
public val brush: Brush,
override val zoomRange: FloatRange,
public val style: DrawStyle = Fill,
public val targetRect: Rect = path.getBounds(),
override val attributes: Attributes = Attributes.EMPTY,
@ -94,7 +95,6 @@ public data class PathFeature<T : Any>(
brush = brush,
style = style,
targetRect = targetRect,
zoomRange = zoomRange
)
}
@ -106,9 +106,7 @@ public data class PathFeature<T : Any>(
public data class PointsFeature<T : Any>(
override val space: CoordinateSpace<T>,
public val points: List<T>,
override val zoomRange: FloatRange,
public val stroke: Float = 2f,
public val color: Color = Color.Red,
public val pointMode: PointMode = PointMode.Points,
override val attributes: Attributes = Attributes.EMPTY,
) : Feature<T> {
@ -125,8 +123,6 @@ public data class PointsFeature<T : Any>(
public data class PolygonFeature<T : Any>(
override val space: CoordinateSpace<T>,
public val points: List<T>,
override val zoomRange: FloatRange,
public val color: Color = Color.Red,
override val attributes: Attributes = Attributes.EMPTY,
) : DomainFeature<T> {
@ -136,7 +132,8 @@ public data class PolygonFeature<T : Any>(
override fun getBoundingBox(zoom: Float): Rectangle<T>? = boundingBox
override fun contains(viewPoint: ViewPoint<T>): Boolean = viewPoint.focus in boundingBox!!//with(space) { viewPoint.focus.isInsidePolygon(points) }
override fun contains(viewPoint: ViewPoint<T>): Boolean =
viewPoint.focus in boundingBox!!//with(space) { viewPoint.focus.isInsidePolygon(points) }
override fun withAttributes(modify: (Attributes) -> Attributes): Feature<T> = copy(attributes = modify(attributes))
}
@ -145,9 +142,7 @@ public data class PolygonFeature<T : Any>(
public data class CircleFeature<T : Any>(
override val space: CoordinateSpace<T>,
override val center: T,
override val zoomRange: FloatRange,
public val size: Dp = 5.dp,
public val color: Color = Color.Red,
override val attributes: Attributes = Attributes.EMPTY,
) : MarkerFeature<T> {
override fun getBoundingBox(zoom: Float): Rectangle<T> =
@ -162,9 +157,7 @@ public data class CircleFeature<T : Any>(
public data class RectangleFeature<T : Any>(
override val space: CoordinateSpace<T>,
override val center: T,
override val zoomRange: FloatRange,
public val size: DpSize = DpSize(5.dp, 5.dp),
public val color: Color = Color.Red,
override val attributes: Attributes = Attributes.EMPTY,
) : MarkerFeature<T> {
override fun getBoundingBox(zoom: Float): Rectangle<T> =
@ -180,8 +173,6 @@ public data class LineFeature<T : Any>(
override val space: CoordinateSpace<T>,
public val a: T,
public val b: T,
override val zoomRange: FloatRange,
public val color: Color = Color.Red,
override val attributes: Attributes = Attributes.EMPTY,
) : DomainFeature<T> {
override fun getBoundingBox(zoom: Float): Rectangle<T> =
@ -208,8 +199,6 @@ public data class ArcFeature<T : Any>(
public val oval: Rectangle<T>,
public val startAngle: Float,
public val arcLength: Float,
override val zoomRange: FloatRange,
public val color: Color = Color.Red,
override val attributes: Attributes = Attributes.EMPTY,
) : DraggableFeature<T> {
override fun getBoundingBox(zoom: Float): Rectangle<T> = oval
@ -223,7 +212,6 @@ public data class ArcFeature<T : Any>(
public data class DrawFeature<T : Any>(
override val space: CoordinateSpace<T>,
public val position: T,
override val zoomRange: FloatRange,
override val attributes: Attributes = Attributes.EMPTY,
public val drawFeature: DrawScope.() -> Unit,
) : DraggableFeature<T> {
@ -240,7 +228,6 @@ public data class BitmapImageFeature<T : Any>(
override val center: T,
public val size: DpSize,
public val image: ImageBitmap,
override val zoomRange: FloatRange,
override val attributes: Attributes = Attributes.EMPTY,
) : MarkerFeature<T> {
override fun getBoundingBox(zoom: Float): Rectangle<T> = space.Rectangle(center, zoom, size)
@ -256,7 +243,6 @@ public data class VectorImageFeature<T : Any>(
override val center: T,
public val size: DpSize,
public val image: ImageVector,
override val zoomRange: FloatRange,
override val attributes: Attributes = Attributes.EMPTY,
) : MarkerFeature<T>, PainterFeature<T> {
override fun getBoundingBox(zoom: Float): Rectangle<T> = space.Rectangle(center, zoom, size)
@ -277,7 +263,6 @@ public data class VectorImageFeature<T : Any>(
public data class ScalableImageFeature<T : Any>(
override val space: CoordinateSpace<T>,
public val rectangle: Rectangle<T>,
override val zoomRange: FloatRange,
override val attributes: Attributes = Attributes.EMPTY,
public val painter: @Composable () -> Painter,
) : Feature<T>, PainterFeature<T> {
@ -290,28 +275,10 @@ public data class ScalableImageFeature<T : Any>(
}
/**
* A group of other features
*/
public data class FeatureGroup<T : Any>(
override val space: CoordinateSpace<T>,
public val children: Map<FeatureId<*>, Feature<T>>,
override val zoomRange: FloatRange,
override val attributes: Attributes = Attributes.EMPTY,
) : Feature<T> {
override fun getBoundingBox(zoom: Float): Rectangle<T>? = with(space) {
children.values.mapNotNull { it.getBoundingBox(zoom) }.wrapRectangles()
}
override fun withAttributes(modify: Attributes.() -> Attributes): Feature<T> = copy(attributes = attributes)
}
public data class TextFeature<T : Any>(
override val space: CoordinateSpace<T>,
public val position: T,
public val text: String,
override val zoomRange: FloatRange,
public val color: Color = Color.Black,
override val attributes: Attributes = Attributes.EMPTY,
public val fontConfig: FeatureFont.() -> Unit,
) : DraggableFeature<T> {

View File

@ -1,335 +0,0 @@
package center.sciprog.maps.features
import androidx.compose.runtime.Composable
import androidx.compose.runtime.mutableStateMapOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.snapshots.SnapshotStateMap
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.PointMode
import androidx.compose.ui.graphics.drawscope.DrawScope
import androidx.compose.ui.graphics.painter.Painter
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.input.pointer.PointerEvent
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.DpSize
import androidx.compose.ui.unit.dp
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Job
import kotlinx.coroutines.isActive
import kotlinx.coroutines.launch
import kotlin.jvm.JvmInline
@JvmInline
public value class FeatureId<out F : Feature<*>>(public val id: String)
public interface FeatureBuilder<T : Any> {
public val space: CoordinateSpace<T>
public fun generateUID(feature: Feature<T>?): String
public fun <F : Feature<T>> feature(id: String?, feature: F): FeatureId<F>
public fun <F : Feature<T>, V> FeatureId<F>.withAttribute(key: Attribute<V>, value: V?): FeatureId<F>
public val defaultColor: Color get() = Color.Red
public val defaultZoomRange: FloatRange get() = 0f..Float.POSITIVE_INFINITY
}
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 space: CoordinateSpace<T>,
) : CoordinateSpace<T> by space, FeatureBuilder<T> {
@PublishedApi
internal val featureMap: SnapshotStateMap<String, Feature<T>> = mutableStateMapOf()
public val features: Map<FeatureId<*>, Feature<T>>
get() = featureMap.mapKeys { FeatureId<Feature<T>>(it.key) }
@Suppress("UNCHECKED_CAST")
public operator fun <F : Feature<T>> get(id: FeatureId<F>): F =
featureMap[id.id]?.let { it as F } ?: error("Feature with id=$id not found")
private var uidCounter = 0
override fun generateUID(feature: Feature<T>?): String = if(feature == null){
"@group[${uidCounter++}]"
} else {
"@${feature::class.simpleName}[${uidCounter++}]"
}
override fun <F : Feature<T>> feature(id: String?, feature: F): FeatureId<F> {
val safeId = id ?: generateUID(feature)
featureMap[safeId] = feature
return FeatureId(safeId)
}
public fun <F : Feature<T>> feature(id: FeatureId<F>, feature: F): FeatureId<F> = feature(id.id, feature)
@Suppress("UNCHECKED_CAST")
public fun <A> getAttribute(id: FeatureId<Feature<T>>, key: Attribute<A>): A? =
get(id).attributes[key]
/**
* Process all features with a given attribute from the one with highest [z] to lowest
*/
public inline fun <A> forEachWithAttribute(
key: Attribute<A>,
block: (id: FeatureId<*>, feature: Feature<T>, attributeValue: A) -> Unit,
) {
featureMap.entries.sortedByDescending { it.value.z }.forEach { (id, feature) ->
feature.attributes[key]?.let {
block(FeatureId<Feature<T>>(id), feature, it)
}
}
}
public fun <F : Feature<T>, V> FeatureId<F>.modifyAttributes(modify: Attributes.() -> Attributes) {
feature(this, get(this).withAttributes(modify))
}
override fun <F : Feature<T>, V> FeatureId<F>.withAttribute(key: Attribute<V>, value: V?): FeatureId<F> {
feature(this, get(this).withAttributes { withAttribute(key, value) })
return this
}
/**
* Add drag to this feature
*
* @param constraint optional drag constraint
*
* TODO use context receiver for that
*/
@Suppress("UNCHECKED_CAST")
public fun FeatureId<DraggableFeature<T>>.draggable(
constraint: ((T) -> T)? = null,
listener: (PointerEvent.(from: ViewPoint<T>, to: ViewPoint<T>) -> Unit)? = null,
) {
if (getAttribute(this, DraggableAttribute) == null) {
val handle = DragHandle.withPrimaryButton<Any> { event, start, end ->
val feature = featureMap[id] as? DraggableFeature<T> ?: return@withPrimaryButton DragResult(end)
start as ViewPoint<T>
end as ViewPoint<T>
if (start in feature) {
val finalPosition = constraint?.invoke(end.focus) ?: end.focus
feature(id, feature.withCoordinates(finalPosition))
feature.attributes[DragListenerAttribute]?.forEach {
it.handle(event, start, ViewPoint(finalPosition, end.zoom))
}
DragResult(ViewPoint(finalPosition, end.zoom), false)
} else {
DragResult(end, true)
}
}
this.withAttribute(DraggableAttribute, handle)
}
//Apply callback
if (listener != null) {
onDrag(listener)
}
}
@Suppress("UNCHECKED_CAST")
public fun FeatureId<DraggableFeature<T>>.onDrag(
listener: PointerEvent.(from: ViewPoint<T>, to: ViewPoint<T>) -> Unit,
) {
withAttribute(
DragListenerAttribute,
(getAttribute(this, DragListenerAttribute) ?: emptySet()) +
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,
) {
withAttribute(
ClickListenerAttribute,
(getAttribute(this, ClickListenerAttribute) ?: emptySet()) +
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,
) {
withAttribute(
HoverListenerAttribute,
(getAttribute(this, HoverListenerAttribute) ?: emptySet()) +
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 companion object {
/**
* Build, but do not remember map feature state
*/
public fun <T : Any> build(
coordinateSpace: CoordinateSpace<T>,
builder: FeatureCollection<T>.() -> Unit = {},
): FeatureCollection<T> = FeatureCollection(coordinateSpace).apply(builder)
/**
* Build and remember map feature state
*/
@Composable
public fun <T : Any> remember(
coordinateSpace: CoordinateSpace<T>,
builder: FeatureCollection<T>.() -> Unit = {},
): FeatureCollection<T> = remember(builder) {
build(coordinateSpace, builder)
}
}
}
public fun <T : Any> FeatureBuilder<T>.circle(
center: T,
zoomRange: FloatRange = defaultZoomRange,
size: Dp = 5.dp,
color: Color = defaultColor,
id: String? = null,
): FeatureId<CircleFeature<T>> = feature(
id, CircleFeature(space, center, zoomRange, size, color)
)
public fun <T : Any> FeatureBuilder<T>.rectangle(
centerCoordinates: T,
zoomRange: FloatRange = defaultZoomRange,
size: DpSize = DpSize(5.dp, 5.dp),
color: Color = defaultColor,
id: String? = null,
): FeatureId<RectangleFeature<T>> = feature(
id, RectangleFeature(space, centerCoordinates, zoomRange, size, color)
)
public fun <T : Any> FeatureBuilder<T>.draw(
position: T,
zoomRange: FloatRange = defaultZoomRange,
id: String? = null,
draw: DrawScope.() -> Unit,
): FeatureId<DrawFeature<T>> = feature(
id,
DrawFeature(space, position, zoomRange, drawFeature = draw)
)
public fun <T : Any> FeatureBuilder<T>.line(
aCoordinates: T,
bCoordinates: T,
zoomRange: FloatRange = defaultZoomRange,
color: Color = defaultColor,
id: String? = null,
): FeatureId<LineFeature<T>> = feature(
id,
LineFeature(space, aCoordinates, bCoordinates, zoomRange, color)
)
public fun <T : Any> FeatureBuilder<T>.arc(
oval: Rectangle<T>,
startAngle: Float,
arcLength: Float,
zoomRange: FloatRange = defaultZoomRange,
color: Color = defaultColor,
id: String? = null,
): FeatureId<ArcFeature<T>> = feature(
id,
ArcFeature(space, oval, startAngle, arcLength, zoomRange, color)
)
public fun <T : Any> FeatureBuilder<T>.points(
points: List<T>,
zoomRange: FloatRange = defaultZoomRange,
stroke: Float = 2f,
color: Color = defaultColor,
pointMode: PointMode = PointMode.Points,
attributes: Attributes = Attributes.EMPTY,
id: String? = null,
): FeatureId<PointsFeature<T>> = feature(
id,
PointsFeature(space, points, zoomRange, stroke, color, pointMode, attributes)
)
public fun <T : Any> FeatureBuilder<T>.polygon(
points: List<T>,
zoomRange: FloatRange = defaultZoomRange,
color: Color = defaultColor,
id: String? = null,
): FeatureId<PolygonFeature<T>> = feature(
id,
PolygonFeature(space, points, zoomRange, color)
)
public fun <T : Any> FeatureBuilder<T>.image(
position: T,
image: ImageVector,
zoomRange: FloatRange = defaultZoomRange,
size: DpSize = DpSize(image.defaultWidth, image.defaultHeight),
id: String? = null,
): FeatureId<VectorImageFeature<T>> =
feature(
id,
VectorImageFeature(
space,
position,
size,
image,
zoomRange
)
)
public fun <T : Any> FeatureBuilder<T>.group(
zoomRange: FloatRange = defaultZoomRange,
id: String? = null,
builder: FeatureCollection<T>.() -> Unit,
): FeatureId<FeatureGroup<T>> {
val map = FeatureCollection(space).apply(builder).features
val feature = FeatureGroup(space, map, zoomRange)
return feature(id, feature)
}
public fun <T : Any> FeatureBuilder<T>.scalableImage(
box: Rectangle<T>,
zoomRange: FloatRange = defaultZoomRange,
id: String? = null,
painter: @Composable () -> Painter,
): FeatureId<ScalableImageFeature<T>> = feature(
id,
ScalableImageFeature<T>(space, box, zoomRange, painter = painter)
)
public fun <T : Any> FeatureBuilder<T>.text(
position: T,
text: String,
zoomRange: FloatRange = defaultZoomRange,
color: Color = defaultColor,
font: FeatureFont.() -> Unit = { size = 16f },
id: String? = null,
): FeatureId<TextFeature<T>> = feature(
id,
TextFeature(space, position, text, zoomRange, color, fontConfig = font)
)

View File

@ -1,37 +1,315 @@
package center.sciprog.maps.features
///**
// * A group of other features
import androidx.compose.runtime.Composable
import androidx.compose.runtime.mutableStateMapOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.snapshots.SnapshotStateMap
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.PointMode
import androidx.compose.ui.graphics.drawscope.DrawScope
import androidx.compose.ui.graphics.painter.Painter
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.input.pointer.PointerEvent
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.DpSize
import androidx.compose.ui.unit.dp
import kotlin.jvm.JvmInline
@JvmInline
public value class FeatureId<out F : Feature<*>>(public val id: String)
/**
* A group of other features
*/
public data class FeatureGroup<T : Any>(
override val space: CoordinateSpace<T>,
public val featureMap: SnapshotStateMap<String, Feature<T>> = mutableStateMapOf(),
override val attributes: Attributes = Attributes.EMPTY,
) : CoordinateSpace<T> by space, Feature<T> {
@Suppress("UNCHECKED_CAST")
public operator fun <F : Feature<T>> get(id: FeatureId<F>): F =
featureMap[id.id]?.let { it as F } ?: error("Feature with id=$id not found")
private var uidCounter = 0
private fun generateUID(feature: Feature<T>?): String = if (feature == null) {
"@group[${uidCounter++}]"
} else {
"@${feature::class.simpleName}[${uidCounter++}]"
}
public fun <F : Feature<T>> feature(id: String?, feature: F): FeatureId<F> {
val safeId = id ?: generateUID(feature)
featureMap[safeId] = feature
return FeatureId(safeId)
}
public fun <F : Feature<T>> feature(id: FeatureId<F>, feature: F): FeatureId<F> = feature(id.id, feature)
public val features: Collection<Feature<T>> get() = featureMap.values.sortedByDescending { it.z }
public fun visit(visitor: FeatureGroup<T>.(id: FeatureId<Feature<T>>, feature: Feature<T>) -> Unit) {
featureMap.forEach { (key, feature) ->
if (feature is FeatureGroup<T>) {
feature.visit(visitor)
} else {
visitor(this, FeatureId(key), feature)
}
}
}
@Suppress("UNCHECKED_CAST")
public fun <A> getAttribute(id: FeatureId<Feature<T>>, key: Attribute<A>): A? =
get(id).attributes[key]
/**
* Process all features with a given attribute from the one with highest [z] to lowest
*/
public inline fun <A> forEachWithAttribute(
key: Attribute<A>,
block: (id: FeatureId<*>, feature: Feature<T>, attributeValue: A) -> Unit,
) {
featureMap.entries.sortedByDescending { it.value.z }.forEach { (id, feature) ->
feature.attributes[key]?.let {
block(FeatureId<Feature<T>>(id), feature, it)
}
}
}
public fun <F : Feature<T>, V> FeatureId<F>.modifyAttributes(modify: Attributes.() -> Attributes) {
feature(this, get(this).withAttributes(modify))
}
public fun <F : Feature<T>, V> FeatureId<F>.withAttribute(key: Attribute<V>, value: V?): FeatureId<F> {
feature(this, get(this).withAttributes { withAttribute(key, value) })
return this
}
/**
* Add drag to this feature
*
* @param constraint optional drag constraint
*
* TODO use context receiver for that
*/
@Suppress("UNCHECKED_CAST")
public fun FeatureId<DraggableFeature<T>>.draggable(
constraint: ((T) -> T)? = null,
listener: (PointerEvent.(from: ViewPoint<T>, to: ViewPoint<T>) -> Unit)? = null,
) {
if (getAttribute(this, DraggableAttribute) == null) {
val handle = DragHandle.withPrimaryButton<Any> { event, start, end ->
val feature = featureMap[id] as? DraggableFeature<T> ?: return@withPrimaryButton DragResult(end)
start as ViewPoint<T>
end as ViewPoint<T>
if (start in feature) {
val finalPosition = constraint?.invoke(end.focus) ?: end.focus
feature(id, feature.withCoordinates(finalPosition))
feature.attributes[DragListenerAttribute]?.forEach {
it.handle(event, start, ViewPoint(finalPosition, end.zoom))
}
DragResult(ViewPoint(finalPosition, end.zoom), false)
} else {
DragResult(end, true)
}
}
this.withAttribute(DraggableAttribute, handle)
}
//Apply callback
if (listener != null) {
onDrag(listener)
}
}
@Suppress("UNCHECKED_CAST")
public fun FeatureId<DraggableFeature<T>>.onDrag(
listener: PointerEvent.(from: ViewPoint<T>, to: ViewPoint<T>) -> Unit,
) {
withAttribute(
DragListenerAttribute,
(getAttribute(this, DragListenerAttribute) ?: emptySet()) +
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,
) {
withAttribute(
ClickListenerAttribute,
(getAttribute(this, ClickListenerAttribute) ?: emptySet()) +
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,
) {
withAttribute(
HoverListenerAttribute,
(getAttribute(this, HoverListenerAttribute) ?: emptySet()) +
MouseListener { event, point -> event.onClick(point as ViewPoint<T>) }
)
}
// /**
// * Cyclic update of a feature. Called infinitely until canceled.
// */
//public data class FeatureGroup<T : Any>(
// val parentBuilder: FeatureBuilder<T>,
// private val groupId: String,
// public override val zoomRange: FloatRange,
// override val attributes: Attributes = Attributes.EMPTY,
//) : FeatureBuilder<T>, Feature<T> {
//
// override val space: CoordinateSpace<T> get() = parentBuilder.space
//
// override fun generateUID(feature: Feature<T>?): String = parentBuilder.generateUID(feature)
//
// override fun <F : Feature<T>> feature(id: String?, feature: F): FeatureId<F> =
// parentBuilder.feature("${groupId}.${id ?: parentBuilder.generateUID(feature)}", feature)
//
// override fun <F : Feature<T>, V> FeatureId<F>.withAttribute(key: Attribute<V>, value: V?): FeatureId<F> =
// with(parentBuilder) {
// FeatureId<F>("${groupId}.${this@withAttribute.id}").withAttribute(key, value)
// 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)))
// }
//
// override fun getBoundingBox(zoom: Float): Rectangle<T>? {
// TODO("Not yet implemented")
// }
//
// override fun withAttributes(modify: Attributes.() -> Attributes): Feature<T> =
// copy(attributes = attributes.modify())
//}
//
//public fun <T : Any> FeatureBuilder<T>.group(
// zoomRange: FloatRange = defaultZoomRange,
// id: String? = null,
// builder: FeatureBuilder<T>.() -> Unit,
//): FeatureId<FeatureGroup<T>> = feature(id, FeatureGroup(this, id ?: generateUID(null), zoomRange).apply(builder))
public fun <F : Feature<T>> FeatureId<F>.color(color: Color): FeatureId<F> =
withAttribute(ColorAttribute, color)
public fun <F : Feature<T>> FeatureId<F>.zoomRange(range: FloatRange): FeatureId<F> =
withAttribute(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 = attributes)
public companion object {
/**
* Build, but do not remember map feature state
*/
public fun <T : Any> build(
coordinateSpace: CoordinateSpace<T>,
builder: FeatureGroup<T>.() -> Unit = {},
): FeatureGroup<T> = FeatureGroup(coordinateSpace).apply(builder)
/**
* Build and remember map feature state
*/
@Composable
public fun <T : Any> remember(
coordinateSpace: CoordinateSpace<T>,
builder: FeatureGroup<T>.() -> Unit = {},
): FeatureGroup<T> = remember(builder) {
build(coordinateSpace, builder)
}
}
}
public fun <T : Any> FeatureGroup<T>.circle(
center: T,
size: Dp = 5.dp,
id: String? = null,
): FeatureId<CircleFeature<T>> = feature(
id, CircleFeature(space, center, size)
)
public fun <T : Any> FeatureGroup<T>.rectangle(
centerCoordinates: T,
size: DpSize = DpSize(5.dp, 5.dp),
id: String? = null,
): FeatureId<RectangleFeature<T>> = feature(
id, RectangleFeature(space, centerCoordinates, size)
)
public fun <T : Any> FeatureGroup<T>.draw(
position: T,
id: String? = null,
draw: DrawScope.() -> Unit,
): FeatureId<DrawFeature<T>> = feature(
id,
DrawFeature(space, position, drawFeature = draw)
)
public fun <T : Any> FeatureGroup<T>.line(
aCoordinates: T,
bCoordinates: T,
id: String? = null,
): FeatureId<LineFeature<T>> = feature(
id,
LineFeature(space, aCoordinates, bCoordinates)
)
public fun <T : Any> FeatureGroup<T>.arc(
oval: Rectangle<T>,
startAngle: Float,
arcLength: Float,
id: String? = null,
): FeatureId<ArcFeature<T>> = feature(
id,
ArcFeature(space, oval, startAngle, arcLength)
)
public fun <T : Any> FeatureGroup<T>.points(
points: List<T>,
stroke: Float = 2f,
pointMode: PointMode = PointMode.Points,
attributes: Attributes = Attributes.EMPTY,
id: String? = null,
): FeatureId<PointsFeature<T>> = feature(
id,
PointsFeature(space, points, stroke, pointMode, attributes)
)
public fun <T : Any> FeatureGroup<T>.polygon(
points: List<T>,
id: String? = null,
): FeatureId<PolygonFeature<T>> = feature(
id,
PolygonFeature(space, points)
)
public fun <T : Any> FeatureGroup<T>.image(
position: T,
image: ImageVector,
size: DpSize = DpSize(image.defaultWidth, image.defaultHeight),
id: String? = null,
): FeatureId<VectorImageFeature<T>> =
feature(
id,
VectorImageFeature(
space,
position,
size,
image,
)
)
public fun <T : Any> FeatureGroup<T>.group(
id: String? = null,
builder: FeatureGroup<T>.() -> Unit,
): FeatureId<FeatureGroup<T>> {
val collection = FeatureGroup(space).apply(builder)
val feature = FeatureGroup(space, collection.featureMap)
return feature(id, feature)
}
public fun <T : Any> FeatureGroup<T>.scalableImage(
box: Rectangle<T>,
id: String? = null,
painter: @Composable () -> Painter,
): FeatureId<ScalableImageFeature<T>> = feature(
id,
ScalableImageFeature<T>(space, box, painter = painter)
)
public fun <T : Any> FeatureGroup<T>.text(
position: T,
text: String,
font: FeatureFont.() -> Unit = { size = 16f },
id: String? = null,
): FeatureId<TextFeature<T>> = feature(
id,
TextFeature(space, position, text, fontConfig = font)
)

View File

@ -1,13 +1,9 @@
package center.sciprog.maps.features
import androidx.compose.ui.graphics.Color
public fun <T : Any> FeatureCollection<T>.draggableLine(
public fun <T : Any> FeatureGroup<T>.draggableLine(
aId: FeatureId<MarkerFeature<T>>,
bId: FeatureId<MarkerFeature<T>>,
zoomRange: FloatRange = defaultZoomRange,
color: Color = Color.Red,
id: String? = null,
): FeatureId<LineFeature<T>> {
var lineId: FeatureId<LineFeature<T>>? = null
@ -15,8 +11,6 @@ public fun <T : Any> FeatureCollection<T>.draggableLine(
fun drawLine(): FeatureId<LineFeature<T>> = line(
get(aId).center,
get(bId).center,
zoomRange,
color,
lineId?.id ?: id
).also {
lineId = it

View File

@ -14,10 +14,11 @@ import kotlin.math.min
/**
* Create a modifier for Map/Scheme canvas controls on desktop
* @param features a collection of features to be rendered in descending [ZAttribute] order
*/
public fun <T : Any> Modifier.mapControls(
state: CoordinateViewScope<T>,
features: Map<FeatureId<*>, Feature<T>>,
features: Collection<Feature<T>>,
): Modifier = with(state) {
pointerInput(Unit) {
fun Offset.toDpOffset() = DpOffset(x.toDp(), y.toDp())
@ -27,10 +28,8 @@ public fun <T : Any> Modifier.mapControls(
val coordinates = event.changes.first().position.toDpOffset().toCoordinates()
val point = space.ViewPoint(coordinates, zoom)
val sortedFeatures =features.values.sortedByDescending { it.z }
if (event.type == PointerEventType.Move) {
for (feature in sortedFeatures) {
for (feature in features) {
val listeners = (feature as? DomainFeature)?.attributes?.get(HoverListenerAttribute)
if (listeners != null && point in feature) {
listeners.forEach { it.handle(event, point) }
@ -43,7 +42,7 @@ public fun <T : Any> Modifier.mapControls(
event,
point
)
for (feature in sortedFeatures) {
for (feature in features) {
val listeners = (feature as? DomainFeature)?.attributes?.get(ClickListenerAttribute)
if (listeners != null && point in feature) {
listeners.forEach { it.handle(event, point) }
@ -97,9 +96,8 @@ public fun <T : Any> Modifier.mapControls(
val dragResult = config.dragHandle?.handle(event, dragStart, dragEnd)
if (dragResult?.handleNext == false) return@drag
features.values.asSequence()
features.asSequence()
.filterIsInstance<DraggableFeature<T>>()
.sortedByDescending { it.z }
.mapNotNull {
it.attributes[DraggableAttribute]
}.forEach { handler ->

View File

@ -25,21 +25,21 @@ public fun <T : Any> DrawScope.drawFeature(
state: CoordinateViewScope<T>,
painterCache: Map<PainterFeature<T>, Painter>,
feature: Feature<T>,
): Unit = with(state) {
val color = feature.color ?: Color.Red
val alpha = feature.attributes[AlphaAttribute]?:1f
fun T.toOffset(): Offset = toOffset(this@drawFeature)
when (feature) {
is FeatureSelector -> drawFeature(state, painterCache, feature.selector(state.zoom))
is CircleFeature -> drawCircle(
feature.color,
color,
feature.size.toPx(),
center = feature.center.toOffset()
)
is RectangleFeature -> drawRect(
feature.color,
color,
topLeft = feature.center.toOffset() - Offset(
feature.size.width.toPx() / 2,
feature.size.height.toPx() / 2
@ -47,14 +47,14 @@ public fun <T : Any> DrawScope.drawFeature(
size = feature.size.toSize()
)
is LineFeature -> drawLine(feature.color, feature.a.toOffset(), feature.b.toOffset())
is LineFeature -> drawLine(color, feature.a.toOffset(), feature.b.toOffset())
is ArcFeature -> {
val dpRect = feature.oval.toDpRect().toRect()
val size = Size(dpRect.width, dpRect.height)
drawArc(
color = feature.color,
color = color,
startAngle = feature.startAngle / PI.toFloat() * 180f,
sweepAngle = feature.arcLength / PI.toFloat() * 180f,
useCenter = false,
@ -85,7 +85,7 @@ public fun <T : Any> DrawScope.drawFeature(
offset.x + 5,
offset.y - 5,
Font().apply(feature.fontConfig),
feature.color.toPaint()
(feature.color ?: Color.Black).toPaint()
)
}
@ -97,8 +97,7 @@ public fun <T : Any> DrawScope.drawFeature(
}
is FeatureGroup -> {
//do nothing
feature.children.values.forEach {
feature.featureMap.values.forEach {
drawFeature(state, painterCache, it)
}
}
@ -116,7 +115,7 @@ public fun <T : Any> DrawScope.drawFeature(
val points = feature.points.map { it.toOffset() }
drawPoints(
points = points,
color = feature.color,
color = color,
strokeWidth = feature.stroke,
pointMode = feature.pointMode,
alpha = alpha
@ -133,7 +132,7 @@ public fun <T : Any> DrawScope.drawFeature(
}
drawPath(
path = polygonPath,
color = feature.color,
color = color,
alpha = alpha
)
}

View File

@ -12,14 +12,12 @@ import kotlinx.serialization.json.jsonPrimitive
/**
* Add a single Json geometry to a feature builder
*/
public fun FeatureBuilder<Gmc>.geoJsonGeometry(
public fun FeatureGroup<Gmc>.geoJsonGeometry(
geometry: GeoJsonGeometry,
color: Color = defaultColor,
id: String? = null,
): FeatureId<Feature<Gmc>> = when (geometry) {
is GeoJsonLineString -> points(
geometry.coordinates,
color = color,
pointMode = PointMode.Lines
)
@ -27,7 +25,6 @@ public fun FeatureBuilder<Gmc>.geoJsonGeometry(
geometry.coordinates.forEach {
points(
it,
color = color,
pointMode = PointMode.Lines
)
}
@ -35,7 +32,6 @@ public fun FeatureBuilder<Gmc>.geoJsonGeometry(
is GeoJsonMultiPoint -> points(
geometry.coordinates,
color = color,
pointMode = PointMode.Points
)
@ -43,15 +39,13 @@ public fun FeatureBuilder<Gmc>.geoJsonGeometry(
geometry.coordinates.forEach {
polygon(
it.first(),
color = color,
)
}
}
is GeoJsonPoint -> circle(geometry.coordinates, color = color, id = id)
is GeoJsonPoint -> circle(geometry.coordinates, id = id)
is GeoJsonPolygon -> polygon(
geometry.coordinates.first(),
color = color,
)
is GeoJsonGeometryCollection -> group(id = id) {
@ -63,18 +57,22 @@ public fun FeatureBuilder<Gmc>.geoJsonGeometry(
withAttribute(AlphaAttribute, 0.5f)
}
public fun FeatureBuilder<Gmc>.geoJsonFeature(
public fun FeatureGroup<Gmc>.geoJsonFeature(
geoJson: GeoJsonFeature,
color: Color = defaultColor,
id: String? = null,
): FeatureId<Feature<Gmc>>? {
val geometry = geoJson.geometry ?: return null
val idOverride = geoJson.properties?.get("id")?.jsonPrimitive?.contentOrNull ?: id
val colorOverride = geoJson.properties?.get("color")?.jsonPrimitive?.intOrNull?.let { Color(it) } ?: color
return geoJsonGeometry(geometry, colorOverride, idOverride)
val colorOverride = geoJson.properties?.get("color")?.jsonPrimitive?.intOrNull?.let { Color(it) }
val jsonGeometry = geoJsonGeometry(geometry, idOverride)
return if( colorOverride!= null){
jsonGeometry.color(colorOverride)
} else{
jsonGeometry
}
}
public fun FeatureBuilder<Gmc>.geoJson(
public fun FeatureGroup<Gmc>.geoJson(
geoJson: GeoJson,
id: String? = null,
): FeatureId<Feature<Gmc>>? = when (geoJson) {

View File

@ -1,7 +1,7 @@
package center.sciprog.maps.geojson
import center.sciprog.maps.coordinates.Gmc
import center.sciprog.maps.features.FeatureBuilder
import center.sciprog.maps.features.FeatureGroup
import kotlinx.serialization.json.Json
import kotlinx.serialization.json.jsonObject
import java.net.URL
@ -9,7 +9,7 @@ import java.net.URL
/**
* Add geojson features from url
*/
public fun FeatureBuilder<Gmc>.geoJson(
public fun FeatureGroup<Gmc>.geoJson(
geoJsonUrl: URL,
id: String? = null,
) {

View File

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

View File

@ -22,12 +22,12 @@ private val logger = KotlinLogging.logger("SchemeView")
@Composable
public fun SchemeView(
state: XYViewScope,
featuresState: FeatureCollection<XY>,
featuresState: FeatureGroup<XY>,
modifier: Modifier = Modifier.fillMaxSize(),
) {
with(state) {
val painterCache: Map<PainterFeature<XY>, Painter> = key(featuresState) {
featuresState.features.values.filterIsInstance<PainterFeature<XY>>().associateWith { it.getPainter() }
featuresState.features.filterIsInstance<PainterFeature<XY>>().associateWith { it.getPainter() }
}
Canvas(modifier = modifier.mapControls(state, featuresState.features).fillMaxSize()) {
@ -38,9 +38,8 @@ public fun SchemeView(
}
clipRect {
featuresState.features.values
featuresState.features
.filter { viewPoint.zoom in it.zoomRange }
.sortedBy { it.z }
.forEach { background ->
drawFeature(state, painterCache, background)
}
@ -88,7 +87,7 @@ public fun SchemeView(
val featureState = key(featureMap) {
FeatureCollection.build(XYCoordinateSpace) {
FeatureGroup.build(XYCoordinateSpace) {
featureMap.forEach { feature(it.key.id, it.value) }
}
}
@ -96,7 +95,7 @@ public fun SchemeView(
val state = rememberMapState(
config,
initialViewPoint = initialViewPoint,
initialRectangle = initialRectangle ?: featureState.features.values.computeBoundingBox(XYCoordinateSpace, Float.MAX_VALUE),
initialRectangle = initialRectangle ?: featureState.features.computeBoundingBox(XYCoordinateSpace, Float.MAX_VALUE),
)
SchemeView(state, featureState, modifier)
@ -115,13 +114,13 @@ public fun SchemeView(
initialRectangle: Rectangle<XY>? = null,
config: ViewConfig<XY> = ViewConfig(),
modifier: Modifier = Modifier.fillMaxSize(),
buildFeatures: FeatureCollection<XY>.() -> Unit = {},
buildFeatures: FeatureGroup<XY>.() -> Unit = {},
) {
val featureState = FeatureCollection.remember(XYCoordinateSpace, buildFeatures)
val featureState = FeatureGroup.remember(XYCoordinateSpace, buildFeatures)
val mapState: XYViewScope = rememberMapState(
config,
initialViewPoint = initialViewPoint,
initialRectangle = initialRectangle ?: featureState.features.values.computeBoundingBox(XYCoordinateSpace, Float.MAX_VALUE),
initialRectangle = initialRectangle ?: featureState.features.computeBoundingBox(XYCoordinateSpace, Float.MAX_VALUE),
)
SchemeView(mapState, featureState, modifier)

View File

@ -17,14 +17,14 @@ import kotlin.math.abs
class FeatureStateSnapshot<T : Any>(
val features: Map<FeatureId<*>, Feature<T>>,
val features: Map<String, Feature<T>>,
val painterCache: Map<PainterFeature<T>, Painter>,
)
@Composable
fun <T: Any> FeatureCollection<T>.snapshot(): FeatureStateSnapshot<T> = FeatureStateSnapshot(
features,
features.values.filterIsInstance<PainterFeature<T>>().associateWith { it.getPainter() }
fun <T: Any> FeatureGroup<T>.snapshot(): FeatureStateSnapshot<T> = FeatureStateSnapshot(
featureMap,
features.filterIsInstance<PainterFeature<T>>().associateWith { it.getPainter() }
)
fun FeatureStateSnapshot<XY>.generateSvg(
@ -114,7 +114,7 @@ fun FeatureStateSnapshot<XY>.generateSvg(
}
is FeatureGroup -> {
feature.children.values.forEach {
feature.featureMap.values.forEach {
drawFeature(scale, it)
}
}