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.filterNotNull
import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.isActive
import kotlinx.coroutines.launch
import java.net.URL import java.net.URL
import java.nio.file.Path import java.nio.file.Path
import kotlin.math.PI import kotlin.math.PI
@ -64,13 +66,13 @@ fun App() {
image(pointOne, Icons.Filled.Home) image(pointOne, Icons.Filled.Home)
val marker1 = rectangle(55.744 to 38.614, 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 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 marker3 = rectangle(56.0 to 38.5, size = DpSize(10.dp, 10.dp)).color(Color.Magenta)
draggableLine(marker1, marker2, color = Color.Blue) draggableLine(marker1, marker2).color(Color.Blue)
draggableLine(marker2, marker3, color = Color.Blue) draggableLine(marker2, marker3).color(Color.Blue)
draggableLine(marker3, marker1, color = Color.Blue) draggableLine(marker3, marker1).color(Color.Blue)
points( points(
points = listOf( points = listOf(
@ -85,13 +87,16 @@ fun App() {
) )
//remember feature ID //remember feature ID
circle( val circleId = circle(
centerCoordinates = pointTwo, centerCoordinates = pointTwo,
).updated(scope) { )
scope.launch {
while (isActive) {
delay(200) delay(200)
//Overwrite a feature with new color circleId.color(Color(Random.nextFloat(), Random.nextFloat(), Random.nextFloat()))
it.copy(color = Color(Random.nextFloat(), Random.nextFloat(), Random.nextFloat()))
} }
}
// draw(position = pointThree) { // draw(position = pointThree) {
// drawLine(start = Offset(-10f, -10f), end = Offset(10f, 10f), color = Color.Red) // drawLine(start = Offset(-10f, -10f), end = Offset(10f, 10f), color = Color.Red)
@ -105,16 +110,20 @@ fun App() {
centerCoordinates.filterNotNull().onEach { centerCoordinates.filterNotNull().onEach {
group(id = "center") { group(id = "center") {
circle(center = it, color = Color.Blue, id = "circle", size = 1.dp) circle(center = it, id = "circle", size = 1.dp).color(Color.Blue)
text(position = it, it.toShortString(), id = "text", color = Color.Blue) text(position = it, it.toShortString(), id = "text").color(Color.Blue)
} }
}.launchIn(scope) }.launchIn(scope)
features.forEach { (id, feature) -> visit { id, feature ->
if (feature is PolygonFeature) { if (feature is PolygonFeature) {
(id as FeatureId<PolygonFeature<Gmc>>).onHover { (id as FeatureId<PolygonFeature<Gmc>>).onHover {
println("Hover on $id") 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.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.FeatureCollection import center.sciprog.maps.features.FeatureGroup
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 = FeatureCollection.remember(XYCoordinateSpace) { val schemeFeaturesState = FeatureGroup.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)
@ -53,7 +53,7 @@ fun App() {
} }
val initialViewPoint: ViewPoint<XY> = remember { val initialViewPoint: ViewPoint<XY> = remember {
schemeFeaturesState.features.values.computeBoundingBox(XYCoordinateSpace, 1f)?.computeViewPoint() schemeFeaturesState.features.computeBoundingBox(XYCoordinateSpace, 1f)?.computeViewPoint()
?: XYViewPoint(XY(0f, 0f)) ?: XYViewPoint(XY(0f, 0f))
} }

View File

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

View File

@ -1,6 +1,5 @@
package center.sciprog.maps.compose package center.sciprog.maps.compose
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.PointMode import androidx.compose.ui.graphics.PointMode
import androidx.compose.ui.graphics.drawscope.DrawScope import androidx.compose.ui.graphics.drawscope.DrawScope
import androidx.compose.ui.graphics.vector.ImageVector import androidx.compose.ui.graphics.vector.ImageVector
@ -11,73 +10,62 @@ import center.sciprog.maps.coordinates.*
import center.sciprog.maps.features.* 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()) GeodeticMapCoordinates.ofDegrees(pair.first.toDouble(), pair.second.toDouble())
public typealias MapFeature = Feature<Gmc> public typealias MapFeature = Feature<Gmc>
public fun FeatureBuilder<Gmc>.circle( public fun FeatureGroup<Gmc>.circle(
centerCoordinates: Pair<Number, Number>, centerCoordinates: Pair<Number, Number>,
zoomRange: FloatRange = defaultZoomRange,
size: Dp = 5.dp, size: Dp = 5.dp,
color: Color = defaultColor,
id: String? = null, id: String? = null,
): FeatureId<CircleFeature<Gmc>> = feature( ): 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>, centerCoordinates: Pair<Number, Number>,
zoomRange: FloatRange = defaultZoomRange,
size: DpSize = DpSize(5.dp, 5.dp), size: DpSize = DpSize(5.dp, 5.dp),
color: Color = defaultColor,
id: String? = null, id: String? = null,
): FeatureId<RectangleFeature<Gmc>> = feature( ): 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>, position: Pair<Number, Number>,
zoomRange: FloatRange = defaultZoomRange,
id: String? = null, id: String? = null,
draw: DrawScope.() -> Unit, draw: DrawScope.() -> Unit,
): FeatureId<DrawFeature<Gmc>> = feature( ): FeatureId<DrawFeature<Gmc>> = feature(
id, 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, curve: GmcCurve,
zoomRange: FloatRange = defaultZoomRange,
color: Color = defaultColor,
id: String? = null, id: String? = null,
): FeatureId<LineFeature<Gmc>> = feature( ): FeatureId<LineFeature<Gmc>> = feature(
id, 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>, aCoordinates: Pair<Double, Double>,
bCoordinates: Pair<Double, Double>, bCoordinates: Pair<Double, Double>,
zoomRange: FloatRange = defaultZoomRange,
color: Color = defaultColor,
id: String? = null, id: String? = null,
): FeatureId<LineFeature<Gmc>> = feature( ): FeatureId<LineFeature<Gmc>> = feature(
id, 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>, center: Pair<Double, Double>,
radius: Distance, radius: Distance,
startAngle: Angle, startAngle: Angle,
arcLength: Angle, arcLength: Angle,
zoomRange: FloatRange = defaultZoomRange,
color: Color = defaultColor,
id: String? = null, id: String? = null,
): FeatureId<ArcFeature<Gmc>> = feature( ): FeatureId<ArcFeature<Gmc>> = feature(
id, id,
@ -86,26 +74,21 @@ public fun FeatureBuilder<Gmc>.arc(
oval = space.Rectangle(coordinatesOf(center), radius, radius), oval = space.Rectangle(coordinatesOf(center), radius, radius),
startAngle = startAngle.radians.toFloat(), startAngle = startAngle.radians.toFloat(),
arcLength = arcLength.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>>, points: List<Pair<Double, Double>>,
zoomRange: FloatRange = defaultZoomRange,
stroke: Float = 2f, stroke: Float = 2f,
color: Color = defaultColor,
pointMode: PointMode = PointMode.Points, pointMode: PointMode = PointMode.Points,
id: String? = null, id: String? = null,
): FeatureId<PointsFeature<Gmc>> = ): 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>, position: Pair<Double, Double>,
image: ImageVector, image: ImageVector,
size: DpSize = DpSize(20.dp, 20.dp), size: DpSize = DpSize(20.dp, 20.dp),
zoomRange: FloatRange = defaultZoomRange,
id: String? = null, id: String? = null,
): FeatureId<VectorImageFeature<Gmc>> = feature( ): FeatureId<VectorImageFeature<Gmc>> = feature(
id, id,
@ -114,18 +97,15 @@ public fun FeatureBuilder<Gmc>.image(
coordinatesOf(position), coordinatesOf(position),
size, size,
image, image,
zoomRange
) )
) )
public fun FeatureBuilder<Gmc>.text( public fun FeatureGroup<Gmc>.text(
position: Pair<Double, Double>, position: Pair<Double, Double>,
text: String, text: String,
zoomRange: FloatRange = defaultZoomRange,
color: Color = defaultColor,
font: FeatureFont.() -> Unit = { size = 16f }, font: FeatureFont.() -> Unit = { size = 16f },
id: String? = null, id: String? = null,
): FeatureId<TextFeature<Gmc>> = feature( ): FeatureId<TextFeature<Gmc>> = feature(
id, 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.IntSize
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import center.sciprog.maps.coordinates.Gmc 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.PainterFeature
import center.sciprog.maps.features.drawFeature 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.launch
import kotlinx.coroutines.supervisorScope import kotlinx.coroutines.supervisorScope
import mu.KotlinLogging import mu.KotlinLogging
@ -44,7 +44,7 @@ private val logger = KotlinLogging.logger("MapView")
@Composable @Composable
public actual fun MapView( public actual fun MapView(
mapState: MapViewScope, mapState: MapViewScope,
featuresState: FeatureCollection<Gmc>, featuresState: FeatureGroup<Gmc>,
modifier: Modifier, modifier: Modifier,
): Unit = with(mapState) { ): Unit = with(mapState) {
@ -90,9 +90,10 @@ public actual fun MapView(
} }
val painterCache: Map<PainterFeature<Gmc>, Painter> = key(featuresState) { 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()) { Canvas(modifier = modifier.mapControls(mapState, featuresState.features).fillMaxSize()) {
if (canvasSize != size.toDpSize()) { 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 -> .forEach { feature ->
drawFeature(mapState, painterCache, feature) drawFeature(mapState, painterCache, feature)
} }

View File

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

View File

@ -21,8 +21,6 @@ public interface Feature<T : Any> {
public val space: CoordinateSpace<T> public val space: CoordinateSpace<T>
public val zoomRange: FloatRange
public val attributes: Attributes public val attributes: Attributes
public fun getBoundingBox(zoom: Float): Rectangle<T>? 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 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> { public interface PainterFeature<T : Any> : Feature<T> {
@Composable @Composable
public fun getPainter(): Painter public fun getPainter(): Painter
@ -65,7 +68,6 @@ public fun <T : Any> Iterable<Feature<T>>.computeBoundingBox(
@Stable @Stable
public data class FeatureSelector<T : Any>( public data class FeatureSelector<T : Any>(
override val space: CoordinateSpace<T>, override val space: CoordinateSpace<T>,
override val zoomRange: FloatRange,
override val attributes: Attributes = Attributes.EMPTY, override val attributes: Attributes = Attributes.EMPTY,
public val selector: (zoom: Float) -> Feature<T>, public val selector: (zoom: Float) -> Feature<T>,
) : Feature<T> { ) : Feature<T> {
@ -81,7 +83,6 @@ public data class PathFeature<T : Any>(
public val rectangle: Rectangle<T>, public val rectangle: Rectangle<T>,
public val path: Path, public val path: Path,
public val brush: Brush, public val brush: Brush,
override val zoomRange: FloatRange,
public val style: DrawStyle = Fill, public val style: DrawStyle = Fill,
public val targetRect: Rect = path.getBounds(), public val targetRect: Rect = path.getBounds(),
override val attributes: Attributes = Attributes.EMPTY, override val attributes: Attributes = Attributes.EMPTY,
@ -94,7 +95,6 @@ public data class PathFeature<T : Any>(
brush = brush, brush = brush,
style = style, style = style,
targetRect = targetRect, targetRect = targetRect,
zoomRange = zoomRange
) )
} }
@ -106,9 +106,7 @@ public data class PathFeature<T : Any>(
public data class PointsFeature<T : Any>( public data class PointsFeature<T : Any>(
override val space: CoordinateSpace<T>, override val space: CoordinateSpace<T>,
public val points: List<T>, public val points: List<T>,
override val zoomRange: FloatRange,
public val stroke: Float = 2f, public val stroke: Float = 2f,
public val color: Color = Color.Red,
public val pointMode: PointMode = PointMode.Points, public val pointMode: PointMode = PointMode.Points,
override val attributes: Attributes = Attributes.EMPTY, override val attributes: Attributes = Attributes.EMPTY,
) : Feature<T> { ) : Feature<T> {
@ -125,8 +123,6 @@ public data class PointsFeature<T : Any>(
public data class PolygonFeature<T : Any>( public data class PolygonFeature<T : Any>(
override val space: CoordinateSpace<T>, override val space: CoordinateSpace<T>,
public val points: List<T>, public val points: List<T>,
override val zoomRange: FloatRange,
public val color: Color = Color.Red,
override val attributes: Attributes = Attributes.EMPTY, override val attributes: Attributes = Attributes.EMPTY,
) : DomainFeature<T> { ) : DomainFeature<T> {
@ -136,7 +132,8 @@ public data class PolygonFeature<T : Any>(
override fun getBoundingBox(zoom: Float): Rectangle<T>? = boundingBox 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)) 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>( public data class CircleFeature<T : Any>(
override val space: CoordinateSpace<T>, override val space: CoordinateSpace<T>,
override val center: T, override val center: T,
override val zoomRange: FloatRange,
public val size: Dp = 5.dp, public val size: Dp = 5.dp,
public val color: Color = Color.Red,
override val attributes: Attributes = Attributes.EMPTY, override val attributes: Attributes = Attributes.EMPTY,
) : MarkerFeature<T> { ) : MarkerFeature<T> {
override fun getBoundingBox(zoom: Float): Rectangle<T> = override fun getBoundingBox(zoom: Float): Rectangle<T> =
@ -162,9 +157,7 @@ public data class CircleFeature<T : Any>(
public data class RectangleFeature<T : Any>( public data class RectangleFeature<T : Any>(
override val space: CoordinateSpace<T>, override val space: CoordinateSpace<T>,
override val center: T, override val center: T,
override val zoomRange: FloatRange,
public val size: DpSize = DpSize(5.dp, 5.dp), public val size: DpSize = DpSize(5.dp, 5.dp),
public val color: Color = Color.Red,
override val attributes: Attributes = Attributes.EMPTY, override val attributes: Attributes = Attributes.EMPTY,
) : MarkerFeature<T> { ) : MarkerFeature<T> {
override fun getBoundingBox(zoom: Float): Rectangle<T> = override fun getBoundingBox(zoom: Float): Rectangle<T> =
@ -180,8 +173,6 @@ public data class LineFeature<T : Any>(
override val space: CoordinateSpace<T>, override val space: CoordinateSpace<T>,
public val a: T, public val a: T,
public val b: T, public val b: T,
override val zoomRange: FloatRange,
public val color: Color = Color.Red,
override val attributes: Attributes = Attributes.EMPTY, override val attributes: Attributes = Attributes.EMPTY,
) : DomainFeature<T> { ) : DomainFeature<T> {
override fun getBoundingBox(zoom: Float): Rectangle<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 oval: Rectangle<T>,
public val startAngle: Float, public val startAngle: Float,
public val arcLength: Float, public val arcLength: Float,
override val zoomRange: FloatRange,
public val color: Color = Color.Red,
override val attributes: Attributes = Attributes.EMPTY, override val attributes: Attributes = Attributes.EMPTY,
) : DraggableFeature<T> { ) : DraggableFeature<T> {
override fun getBoundingBox(zoom: Float): Rectangle<T> = oval override fun getBoundingBox(zoom: Float): Rectangle<T> = oval
@ -223,7 +212,6 @@ public data class ArcFeature<T : Any>(
public data class DrawFeature<T : Any>( public data class DrawFeature<T : Any>(
override val space: CoordinateSpace<T>, override val space: CoordinateSpace<T>,
public val position: T, public val position: T,
override val zoomRange: FloatRange,
override val attributes: Attributes = Attributes.EMPTY, override val attributes: Attributes = Attributes.EMPTY,
public val drawFeature: DrawScope.() -> Unit, public val drawFeature: DrawScope.() -> Unit,
) : DraggableFeature<T> { ) : DraggableFeature<T> {
@ -240,7 +228,6 @@ public data class BitmapImageFeature<T : Any>(
override val center: 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,
override val attributes: Attributes = Attributes.EMPTY, override val attributes: Attributes = Attributes.EMPTY,
) : MarkerFeature<T> { ) : MarkerFeature<T> {
override fun getBoundingBox(zoom: Float): Rectangle<T> = space.Rectangle(center, zoom, size) 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, override val center: T,
public val size: DpSize, public val size: DpSize,
public val image: ImageVector, public val image: ImageVector,
override val zoomRange: FloatRange,
override val attributes: Attributes = Attributes.EMPTY, override val attributes: Attributes = Attributes.EMPTY,
) : MarkerFeature<T>, PainterFeature<T> { ) : MarkerFeature<T>, PainterFeature<T> {
override fun getBoundingBox(zoom: Float): Rectangle<T> = space.Rectangle(center, zoom, size) 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>( public data class ScalableImageFeature<T : Any>(
override val space: CoordinateSpace<T>, override val space: CoordinateSpace<T>,
public val rectangle: Rectangle<T>, public val rectangle: Rectangle<T>,
override val zoomRange: FloatRange,
override val attributes: Attributes = Attributes.EMPTY, override val attributes: Attributes = Attributes.EMPTY,
public val painter: @Composable () -> Painter, public val painter: @Composable () -> Painter,
) : Feature<T>, PainterFeature<T> { ) : 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>( public data class TextFeature<T : Any>(
override val space: CoordinateSpace<T>, override val space: CoordinateSpace<T>,
public val position: T, public val position: T,
public val text: String, public val text: String,
override val zoomRange: FloatRange,
public val color: Color = Color.Black,
override val attributes: Attributes = Attributes.EMPTY, override val attributes: Attributes = Attributes.EMPTY,
public val fontConfig: FeatureFont.() -> Unit, public val fontConfig: FeatureFont.() -> Unit,
) : DraggableFeature<T> { ) : 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 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 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>) }
)
}
// /** // /**
// * A group of other features // * Cyclic update of a feature. Called infinitely until canceled.
// */ // */
//public data class FeatureGroup<T : Any>( // public fun <F : Feature<T>> FeatureId<F>.updated(
// val parentBuilder: FeatureBuilder<T>, // scope: CoroutineScope,
// private val groupId: String, // update: suspend (F) -> F,
// public override val zoomRange: FloatRange, // ): Job = scope.launch {
// override val attributes: Attributes = Attributes.EMPTY, // while (isActive) {
//) : FeatureBuilder<T>, Feature<T> { // feature(this@updated, update(get(this@updated)))
//
// 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)
// } // }
//
// override fun getBoundingBox(zoom: Float): Rectangle<T>? {
// TODO("Not yet implemented")
// } // }
//
// override fun withAttributes(modify: Attributes.() -> Attributes): Feature<T> = public fun <F : Feature<T>> FeatureId<F>.color(color: Color): FeatureId<F> =
// copy(attributes = attributes.modify()) withAttribute(ColorAttribute, color)
//} public fun <F : Feature<T>> FeatureId<F>.zoomRange(range: FloatRange): FeatureId<F> =
// withAttribute(ZoomRangeAttribute, range)
//public fun <T : Any> FeatureBuilder<T>.group(
// zoomRange: FloatRange = defaultZoomRange, override fun getBoundingBox(zoom: Float): Rectangle<T>? = with(space) {
// id: String? = null, featureMap.values.mapNotNull { it.getBoundingBox(zoom) }.wrapRectangles()
// builder: FeatureBuilder<T>.() -> Unit, }
//): FeatureId<FeatureGroup<T>> = feature(id, FeatureGroup(this, id ?: generateUID(null), zoomRange).apply(builder))
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 package center.sciprog.maps.features
import androidx.compose.ui.graphics.Color
public fun <T : Any> FeatureGroup<T>.draggableLine(
public fun <T : Any> FeatureCollection<T>.draggableLine(
aId: FeatureId<MarkerFeature<T>>, aId: FeatureId<MarkerFeature<T>>,
bId: FeatureId<MarkerFeature<T>>, bId: FeatureId<MarkerFeature<T>>,
zoomRange: FloatRange = defaultZoomRange,
color: Color = Color.Red,
id: String? = null, id: String? = null,
): FeatureId<LineFeature<T>> { ): FeatureId<LineFeature<T>> {
var lineId: FeatureId<LineFeature<T>>? = null var lineId: FeatureId<LineFeature<T>>? = null
@ -15,8 +11,6 @@ public fun <T : Any> FeatureCollection<T>.draggableLine(
fun drawLine(): FeatureId<LineFeature<T>> = line( fun drawLine(): FeatureId<LineFeature<T>> = line(
get(aId).center, get(aId).center,
get(bId).center, get(bId).center,
zoomRange,
color,
lineId?.id ?: id lineId?.id ?: id
).also { ).also {
lineId = it lineId = it

View File

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

View File

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

View File

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

View File

@ -1,7 +1,7 @@
package center.sciprog.maps.geojson package center.sciprog.maps.geojson
import center.sciprog.maps.coordinates.Gmc 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.Json
import kotlinx.serialization.json.jsonObject import kotlinx.serialization.json.jsonObject
import java.net.URL import java.net.URL
@ -9,7 +9,7 @@ import java.net.URL
/** /**
* Add geojson features from url * Add geojson features from url
*/ */
public fun FeatureBuilder<Gmc>.geoJson( public fun FeatureGroup<Gmc>.geoJson(
geoJsonUrl: URL, geoJsonUrl: URL,
id: String? = null, 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()) internal fun Pair<Number, Number>.toCoordinates(): XY = XY(first.toFloat(), second.toFloat())
fun FeatureBuilder<XY>.background( fun FeatureGroup<XY>.background(
width: Float, width: Float,
height: Float, height: Float,
offset: XY = XY(0f, 0f), 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>, centerCoordinates: Pair<Number, Number>,
zoomRange: FloatRange = defaultZoomRange, zoomRange: FloatRange = defaultZoomRange,
size: Dp = 5.dp, size: Dp = 5.dp,
@ -43,14 +43,14 @@ fun FeatureBuilder<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 FeatureBuilder<XY>.draw( fun FeatureGroup<XY>.draw(
position: Pair<Number, Number>, position: Pair<Number, Number>,
zoomRange: FloatRange = defaultZoomRange, zoomRange: FloatRange = 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 FeatureBuilder<XY>.line( fun FeatureGroup<XY>.line(
aCoordinates: Pair<Number, Number>, aCoordinates: Pair<Number, Number>,
bCoordinates: Pair<Number, Number>, bCoordinates: Pair<Number, Number>,
scaleRange: FloatRange = defaultZoomRange, scaleRange: FloatRange = defaultZoomRange,
@ -59,7 +59,7 @@ fun FeatureBuilder<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 FeatureBuilder<XY>.arc( public fun FeatureGroup<XY>.arc(
center: Pair<Double, Double>, center: Pair<Double, Double>,
radius: Float, radius: Float,
startAngle: Float, startAngle: Float,
@ -76,7 +76,7 @@ public fun FeatureBuilder<XY>.arc(
id = id id = id
) )
fun FeatureBuilder<XY>.image( fun FeatureGroup<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),
@ -85,7 +85,7 @@ fun FeatureBuilder<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 FeatureBuilder<XY>.text( fun FeatureGroup<XY>.text(
position: Pair<Number, Number>, position: Pair<Number, Number>,
text: String, text: String,
zoomRange: FloatRange = defaultZoomRange, zoomRange: FloatRange = defaultZoomRange,

View File

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

View File

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