Immutable attributes

This commit is contained in:
Alexander Nozik 2023-01-01 09:10:01 +03:00
parent 7643968d39
commit 26c3e589da
16 changed files with 405 additions and 212 deletions

View File

@ -109,6 +109,15 @@ fun App() {
text(position = it, it.toShortString(), id = "text", color = Color.Blue)
}
}.launchIn(scope)
features.forEach { (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))
}
}
}
}
}
}

View File

@ -7,6 +7,7 @@ import center.sciprog.maps.coordinates.*
import center.sciprog.maps.features.CoordinateSpace
import center.sciprog.maps.features.Rectangle
import center.sciprog.maps.features.ViewPoint
import kotlin.math.abs
import kotlin.math.floor
import kotlin.math.pow
@ -82,6 +83,13 @@ public object WebMercatorSpace : CoordinateSpace<Gmc> {
(mercatorA.y - mercatorB.y).dp * tileScale
)
}
override fun Gmc.isInsidePolygon(points: List<Gmc>): Boolean = points.zipWithNext().count { (left, right) ->
val dist = right.latitude - left.latitude
val intersection = left.latitude * abs((right.longitude - longitude) / dist) +
right.latitude * abs((longitude - left.longitude) / dist)
longitude in left.longitude..right.longitude && intersection >= latitude
} % 2 == 0
}
/**

View File

@ -18,56 +18,56 @@ public typealias MapFeature = Feature<Gmc>
public fun FeatureBuilder<Gmc>.circle(
centerCoordinates: Pair<Number, Number>,
zoomRange: DoubleRange = defaultZoomRange,
zoomRange: FloatRange = defaultZoomRange,
size: Dp = 5.dp,
color: Color = defaultColor,
id: String? = null,
): FeatureId<CircleFeature<Gmc>> = feature(
id, CircleFeature(coordinateSpace, coordinatesOf(centerCoordinates), zoomRange, size, color)
id, CircleFeature(space, coordinatesOf(centerCoordinates), zoomRange, size, color)
)
public fun FeatureBuilder<Gmc>.rectangle(
centerCoordinates: Pair<Number, Number>,
zoomRange: DoubleRange = defaultZoomRange,
zoomRange: FloatRange = defaultZoomRange,
size: DpSize = DpSize(5.dp, 5.dp),
color: Color = defaultColor,
id: String? = null,
): FeatureId<RectangleFeature<Gmc>> = feature(
id, RectangleFeature(coordinateSpace, coordinatesOf(centerCoordinates), zoomRange, size, color)
id, RectangleFeature(space, coordinatesOf(centerCoordinates), zoomRange, size, color)
)
public fun FeatureBuilder<Gmc>.draw(
position: Pair<Number, Number>,
zoomRange: DoubleRange = defaultZoomRange,
zoomRange: FloatRange = defaultZoomRange,
id: String? = null,
draw: DrawScope.() -> Unit,
): FeatureId<DrawFeature<Gmc>> = feature(
id,
DrawFeature(coordinateSpace, coordinatesOf(position), zoomRange, drawFeature = draw)
DrawFeature(space, coordinatesOf(position), zoomRange, drawFeature = draw)
)
public fun FeatureBuilder<Gmc>.line(
curve: GmcCurve,
zoomRange: DoubleRange = defaultZoomRange,
zoomRange: FloatRange = defaultZoomRange,
color: Color = defaultColor,
id: String? = null,
): FeatureId<LineFeature<Gmc>> = feature(
id,
LineFeature(coordinateSpace, curve.forward.coordinates, curve.backward.coordinates, zoomRange, color)
LineFeature(space, curve.forward.coordinates, curve.backward.coordinates, zoomRange, color)
)
public fun FeatureBuilder<Gmc>.line(
aCoordinates: Pair<Double, Double>,
bCoordinates: Pair<Double, Double>,
zoomRange: DoubleRange = defaultZoomRange,
zoomRange: FloatRange = defaultZoomRange,
color: Color = defaultColor,
id: String? = null,
): FeatureId<LineFeature<Gmc>> = feature(
id,
LineFeature(coordinateSpace, coordinatesOf(aCoordinates), coordinatesOf(bCoordinates), zoomRange, color)
LineFeature(space, coordinatesOf(aCoordinates), coordinatesOf(bCoordinates), zoomRange, color)
)
@ -76,14 +76,14 @@ public fun FeatureBuilder<Gmc>.arc(
radius: Distance,
startAngle: Angle,
arcLength: Angle,
zoomRange: DoubleRange = defaultZoomRange,
zoomRange: FloatRange = defaultZoomRange,
color: Color = defaultColor,
id: String? = null,
): FeatureId<ArcFeature<Gmc>> = feature(
id,
ArcFeature(
coordinateSpace,
oval = coordinateSpace.Rectangle(coordinatesOf(center), radius, radius),
space,
oval = space.Rectangle(coordinatesOf(center), radius, radius),
startAngle = startAngle.radians.toFloat(),
arcLength = arcLength.radians.toFloat(),
zoomRange = zoomRange,
@ -93,24 +93,24 @@ public fun FeatureBuilder<Gmc>.arc(
public fun FeatureBuilder<Gmc>.points(
points: List<Pair<Double, Double>>,
zoomRange: DoubleRange = defaultZoomRange,
zoomRange: FloatRange = defaultZoomRange,
stroke: Float = 2f,
color: Color = defaultColor,
pointMode: PointMode = PointMode.Points,
id: String? = null,
): FeatureId<PointsFeature<Gmc>> =
feature(id, PointsFeature(coordinateSpace, points.map(::coordinatesOf), zoomRange, stroke, color, pointMode))
feature(id, PointsFeature(space, points.map(::coordinatesOf), zoomRange, stroke, color, pointMode))
public fun FeatureBuilder<Gmc>.image(
position: Pair<Double, Double>,
image: ImageVector,
size: DpSize = DpSize(20.dp, 20.dp),
zoomRange: DoubleRange = defaultZoomRange,
zoomRange: FloatRange = defaultZoomRange,
id: String? = null,
): FeatureId<VectorImageFeature<Gmc>> = feature(
id,
VectorImageFeature(
coordinateSpace,
space,
coordinatesOf(position),
size,
image,
@ -121,11 +121,11 @@ public fun FeatureBuilder<Gmc>.image(
public fun FeatureBuilder<Gmc>.text(
position: Pair<Double, Double>,
text: String,
zoomRange: DoubleRange = defaultZoomRange,
zoomRange: FloatRange = defaultZoomRange,
color: Color = defaultColor,
font: FeatureFont.() -> Unit = { size = 16f },
id: String? = null,
): FeatureId<TextFeature<Gmc>> = feature(
id,
TextFeature(coordinateSpace, coordinatesOf(position), text, zoomRange, color, fontConfig = font)
TextFeature(space, coordinatesOf(position), text, zoomRange, color, fontConfig = font)
)

View File

@ -0,0 +1,21 @@
package center.sciprog.maps.features
import androidx.compose.ui.graphics.Color
public interface Attribute<T>
public object ZAttribute : Attribute<Float>
public object DraggableAttribute : Attribute<DragHandle<Any>>
public object DragListenerAttribute : Attribute<Set<DragListener<Any>>>
public object ClickListenerAttribute : Attribute<Set<MouseListener<Any>>>
public object HoverListenerAttribute : Attribute<Set<MouseListener<Any>>>
public object VisibleAttribute : Attribute<Boolean>
public object ColorAttribute : Attribute<Color>
public object AlphaAttribute : Attribute<Float>

View File

@ -1,55 +0,0 @@
package center.sciprog.maps.features
import androidx.compose.runtime.mutableStateMapOf
import androidx.compose.ui.graphics.Color
public object ZAttribute : Feature.Attribute<Float>
public object DraggableAttribute : Feature.Attribute<DragHandle<Any>>
public object DragListenerAttribute : Feature.Attribute<Set<DragListener<Any>>>
public object ClickableListenerAttribute : Feature.Attribute<Set<ClickListener<Any>>>
public object VisibleAttribute : Feature.Attribute<Boolean>
public object ColorAttribute : Feature.Attribute<Color>
public class AttributeMap {
public val map: MutableMap<Feature.Attribute<*>, Any> = mutableStateMapOf()
public operator fun <T, A : Feature.Attribute<T>> set(
attribute: A,
attrValue: T?,
) {
if (attrValue == null) {
map.remove(attribute)
} else {
map[attribute] = attrValue
}
}
@Suppress("UNCHECKED_CAST")
public operator fun <T> get(attribute: Feature.Attribute<T>): T? = map[attribute] as? T
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (other == null || this::class != other::class) return false
other as AttributeMap
if (map != other.map) return false
return true
}
override fun hashCode(): Int = map.hashCode()
override fun toString(): String = "AttributeMap(value=${map.entries})"
}
public var Feature<*>.z: Float
get() = attributes[ZAttribute] ?: 0f
set(value) {
attributes[ZAttribute] = value
}

View File

@ -0,0 +1,43 @@
package center.sciprog.maps.features
import androidx.compose.runtime.Stable
import kotlin.jvm.JvmInline
@Stable
@JvmInline
public value class Attributes internal constructor(internal val map: Map<Attribute<*>, Any>) {
@Suppress("UNCHECKED_CAST")
public operator fun <T> get(attribute: Attribute<T>): T? = map[attribute] as? T
public fun <T> Attribute<T>.invoke(value: T?): Attributes = withAttribute(this, value)
override fun toString(): String = "AttributeMap(value=${map.entries})"
public companion object {
public val EMPTY: Attributes = Attributes(emptyMap())
}
}
public fun <T, A : Attribute<T>> Attributes.withAttribute(
attribute: A,
attrValue: T?,
): Attributes = Attributes(
if (attrValue == null) {
map - attribute
} else {
map + (attribute to attrValue)
}
)
public fun <T : Any, A : Attribute<T>> Attributes(
attribute: A,
attrValue: T,
): Attributes = Attributes(mapOf(attribute to attrValue))
public operator fun Attributes.plus(other: Attributes): Attributes = Attributes(map + other.map)
public val Feature<*>.z: Float
get() = attributes[ZAttribute] ?: 0f
// set(value) {
// attributes[ZAttribute] = value
// }

View File

@ -76,6 +76,8 @@ public interface CoordinateSpace<T : Any> {
return distanceVale.dp
}
public fun T.isInsidePolygon(points: List<T>): Boolean
}
public fun <T : Any> CoordinateSpace<T>.Rectangle(viewPoint: ViewPoint<T>, size: DpSize): Rectangle<T> =

View File

@ -1,6 +1,7 @@
package center.sciprog.maps.features
import androidx.compose.runtime.Composable
import androidx.compose.runtime.Stable
import androidx.compose.ui.geometry.Rect
import androidx.compose.ui.graphics.*
import androidx.compose.ui.graphics.drawscope.DrawScope
@ -14,19 +15,19 @@ import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.DpSize
import androidx.compose.ui.unit.dp
public typealias DoubleRange = FloatRange
public typealias FloatRange = ClosedFloatingPointRange<Float>
public interface Feature<T : Any> {
public interface Attribute<T>
public val space: CoordinateSpace<T>
public val zoomRange: FloatRange
public val attributes: AttributeMap
public val attributes: Attributes
public fun getBoundingBox(zoom: Float): Rectangle<T>?
public fun withAttributes(modify: Attributes.() -> Attributes): Feature<T>
}
public interface PainterFeature<T : Any> : Feature<T> {
@ -34,13 +35,13 @@ public interface PainterFeature<T : Any> : Feature<T> {
public fun getPainter(): Painter
}
public interface ClickableFeature<T : Any> : Feature<T> {
public interface DomainFeature<T : Any> : Feature<T> {
public operator fun contains(viewPoint: ViewPoint<T>): Boolean = getBoundingBox(viewPoint.zoom)?.let {
viewPoint.focus in it
} ?: false
}
public interface DraggableFeature<T : Any> : ClickableFeature<T> {
public interface DraggableFeature<T : Any> : DomainFeature<T> {
public fun withCoordinates(newCoordinates: T): Feature<T>
}
@ -58,24 +59,24 @@ public fun <T : Any> Iterable<Feature<T>>.computeBoundingBox(
mapNotNull { it.getBoundingBox(zoom) }.wrapRectangles()
}
//public fun Pair<Number, Number>.toCoordinates(): GeodeticMapCoordinates =
// GeodeticMapCoordinates.ofDegrees(first.toDouble(), second.toDouble())
/**
* A feature that decides what to show depending on the zoom value (it could change size of shape)
*/
public class FeatureSelector<T : Any>(
@Stable
public data class FeatureSelector<T : Any>(
override val space: CoordinateSpace<T>,
override val zoomRange: FloatRange,
override val attributes: AttributeMap = AttributeMap(),
override val attributes: Attributes = Attributes.EMPTY,
public val selector: (zoom: Float) -> Feature<T>,
) : Feature<T> {
override fun getBoundingBox(zoom: Float): Rectangle<T>? = selector(zoom).getBoundingBox(zoom)
override fun withAttributes(modify: Attributes.() -> Attributes): Feature<T> = copy(attributes = modify(attributes))
}
public class PathFeature<T : Any>(
@Stable
public data class PathFeature<T : Any>(
override val space: CoordinateSpace<T>,
public val rectangle: Rectangle<T>,
public val path: Path,
@ -83,7 +84,7 @@ public class PathFeature<T : Any>(
override val zoomRange: FloatRange,
public val style: DrawStyle = Fill,
public val targetRect: Rect = path.getBounds(),
override val attributes: AttributeMap = AttributeMap(),
override val attributes: Attributes = Attributes.EMPTY,
) : DraggableFeature<T> {
override fun withCoordinates(newCoordinates: T): Feature<T> = with(space) {
PathFeature(
@ -98,126 +99,172 @@ public class PathFeature<T : Any>(
}
override fun getBoundingBox(zoom: Float): Rectangle<T> = rectangle
override fun withAttributes(modify: Attributes.() -> Attributes): Feature<T> = copy(attributes = modify(attributes))
}
public class PointsFeature<T : Any>(
@Stable
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: AttributeMap = AttributeMap(),
override val attributes: Attributes = Attributes.EMPTY,
) : Feature<T> {
override fun getBoundingBox(zoom: Float): Rectangle<T>? = with(space) {
points.wrapPoints()
private val boundingBox by lazy {
with(space) { points.wrapPoints() }
}
override fun getBoundingBox(zoom: Float): Rectangle<T>? = boundingBox
override fun withAttributes(modify: (Attributes) -> Attributes): Feature<T> = copy(attributes = modify(attributes))
}
@Stable
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> {
private val boundingBox: Rectangle<T>? by lazy {
with(space) { points.wrapPoints() }
}
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 withAttributes(modify: (Attributes) -> Attributes): Feature<T> = copy(attributes = modify(attributes))
}
@Stable
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: AttributeMap = AttributeMap(),
override val attributes: Attributes = Attributes.EMPTY,
) : MarkerFeature<T> {
override fun getBoundingBox(zoom: Float): Rectangle<T> =
space.Rectangle(center, zoom, DpSize(size, size))
override fun withCoordinates(newCoordinates: T): Feature<T> =
CircleFeature(space, newCoordinates, zoomRange, size, color, attributes)
override fun withCoordinates(newCoordinates: T): Feature<T> = copy(center = newCoordinates)
override fun withAttributes(modify: (Attributes) -> Attributes): Feature<T> = copy(attributes = modify(attributes))
}
public class RectangleFeature<T : Any>(
@Stable
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: AttributeMap = AttributeMap(),
override val attributes: Attributes = Attributes.EMPTY,
) : MarkerFeature<T> {
override fun getBoundingBox(zoom: Float): Rectangle<T> =
space.Rectangle(center, zoom, size)
override fun withCoordinates(newCoordinates: T): Feature<T> =
RectangleFeature(space, newCoordinates, zoomRange, size, color, attributes)
override fun withCoordinates(newCoordinates: T): Feature<T> = copy(center = newCoordinates)
override fun withAttributes(modify: (Attributes) -> Attributes): Feature<T> = copy(attributes = modify(attributes))
}
public class LineFeature<T : Any>(
@Stable
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: AttributeMap = AttributeMap(),
) : ClickableFeature<T> {
override val attributes: Attributes = Attributes.EMPTY,
) : DomainFeature<T> {
override fun getBoundingBox(zoom: Float): Rectangle<T> =
space.Rectangle(a, b)
override fun contains(viewPoint: ViewPoint<T>): Boolean = with(space) {
viewPoint.focus in space.Rectangle(a, b) && viewPoint.focus.distanceToLine(a, b, viewPoint.zoom).value < 5f
viewPoint.focus in getBoundingBox(viewPoint.zoom) && viewPoint.focus.distanceToLine(
a,
b,
viewPoint.zoom
).value < 5f
}
override fun withAttributes(modify: (Attributes) -> Attributes): Feature<T> = copy(attributes = modify(attributes))
}
/**
* @param startAngle the angle from 3 o'clock downwards for the start of the arc in radians
* @param arcLength arc length in radians
*/
public class ArcFeature<T : Any>(
@Stable
public data class ArcFeature<T : Any>(
override val space: CoordinateSpace<T>,
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: AttributeMap = AttributeMap(),
override val attributes: Attributes = Attributes.EMPTY,
) : DraggableFeature<T> {
override fun getBoundingBox(zoom: Float): Rectangle<T> = oval
override fun withCoordinates(newCoordinates: T): Feature<T> = with(space) {
ArcFeature(space, oval.withCenter(newCoordinates), startAngle, arcLength, zoomRange, color, attributes)
}
override fun withCoordinates(newCoordinates: T): Feature<T> =
copy(oval = with(space) { oval.withCenter(newCoordinates) })
override fun withAttributes(modify: (Attributes) -> Attributes): Feature<T> = copy(attributes = modify(attributes))
}
public data class DrawFeature<T : Any>(
override val space: CoordinateSpace<T>,
public val position: T,
override val zoomRange: FloatRange,
override val attributes: AttributeMap = AttributeMap(),
override val attributes: Attributes = Attributes.EMPTY,
public val drawFeature: DrawScope.() -> Unit,
) : DraggableFeature<T> {
override fun getBoundingBox(zoom: Float): Rectangle<T> = space.Rectangle(position, position)
override fun withCoordinates(newCoordinates: T): Feature<T> = copy(position = newCoordinates)
override fun withAttributes(modify: (Attributes) -> Attributes): Feature<T> = copy(attributes = modify(attributes))
}
@Stable
public data class BitmapImageFeature<T : Any>(
override val space: CoordinateSpace<T>,
override val center: T,
public val size: DpSize,
public val image: ImageBitmap,
override val zoomRange: FloatRange,
override val attributes: AttributeMap = AttributeMap(),
override val attributes: Attributes = Attributes.EMPTY,
) : MarkerFeature<T> {
override fun getBoundingBox(zoom: Float): Rectangle<T> = space.Rectangle(center, zoom, size)
override fun withCoordinates(newCoordinates: T): Feature<T> = copy(center = newCoordinates)
override fun withAttributes(modify: (Attributes) -> Attributes): Feature<T> = copy(attributes = modify(attributes))
}
@Stable
public data class VectorImageFeature<T : Any>(
override val space: CoordinateSpace<T>,
override val center: T,
public val size: DpSize,
public val image: ImageVector,
override val zoomRange: FloatRange,
override val attributes: AttributeMap = AttributeMap(),
override val attributes: Attributes = Attributes.EMPTY,
) : MarkerFeature<T>, PainterFeature<T> {
override fun getBoundingBox(zoom: Float): Rectangle<T> = space.Rectangle(center, zoom, size)
override fun withCoordinates(newCoordinates: T): Feature<T> = copy(center = newCoordinates)
override fun withAttributes(modify: (Attributes) -> Attributes): Feature<T> = copy(attributes = modify(attributes))
@Composable
override fun getPainter(): VectorPainter = rememberVectorPainter(image)
}
@ -227,45 +274,50 @@ public data class VectorImageFeature<T : Any>(
*
* @param rectangle the size of background in scheme size units. The screen units to scheme units ratio equals scale.
*/
public class ScalableImageFeature<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: AttributeMap = AttributeMap(),
override val attributes: Attributes = Attributes.EMPTY,
public val painter: @Composable () -> Painter,
) : Feature<T>, PainterFeature<T> {
@Composable
override fun getPainter(): Painter = painter.invoke()
override fun getBoundingBox(zoom: Float): Rectangle<T> = rectangle
override fun withAttributes(modify: (Attributes) -> Attributes): Feature<T> = copy(attributes = modify(attributes))
}
/**
* A group of other features
*/
public class FeatureGroup<T : Any>(
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: AttributeMap = AttributeMap(),
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 class TextFeature<T : Any>(
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: AttributeMap = AttributeMap(),
override val attributes: Attributes = Attributes.EMPTY,
public val fontConfig: FeatureFont.() -> Unit,
) : DraggableFeature<T> {
override fun getBoundingBox(zoom: Float): Rectangle<T> = space.Rectangle(position, position)
override fun withCoordinates(newCoordinates: T): Feature<T> =
TextFeature(space, newCoordinates, text, zoomRange, color, attributes, fontConfig)
override fun withCoordinates(newCoordinates: T): Feature<T> = copy(position = newCoordinates)
override fun withAttributes(modify: (Attributes) -> Attributes): Feature<T> = copy(attributes = modify(attributes))
}

View File

@ -3,6 +3,7 @@ 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
@ -23,11 +24,13 @@ public value class FeatureId<out F : Feature<*>>(public val id: String)
public interface FeatureBuilder<T : Any> {
public val coordinateSpace: CoordinateSpace<T>
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> setAttribute(id: FeatureId<F>, key: Feature.Attribute<V>, value: V?)
public fun <F : Feature<T>, V> FeatureId<F>.withAttribute(key: Attribute<V>, value: V?): FeatureId<F>
public val defaultColor: Color get() = Color.Red
@ -38,11 +41,11 @@ public fun <T : Any, F : Feature<T>> FeatureBuilder<T>.feature(id: FeatureId<F>,
feature(id.id, feature)
public class FeatureCollection<T : Any>(
override val coordinateSpace: CoordinateSpace<T>,
) : CoordinateSpace<T> by coordinateSpace, FeatureBuilder<T> {
override val space: CoordinateSpace<T>,
) : CoordinateSpace<T> by space, FeatureBuilder<T> {
@PublishedApi
internal val featureMap: MutableMap<String, Feature<T>> = mutableStateMapOf()
internal val featureMap: SnapshotStateMap<String, Feature<T>> = mutableStateMapOf()
public val features: Map<FeatureId<*>, Feature<T>>
get() = featureMap.mapKeys { FeatureId<Feature<T>>(it.key) }
@ -51,33 +54,49 @@ public class FeatureCollection<T : Any>(
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 fun generateID(feature: Feature<T>): String = "@feature[${feature.hashCode().toUInt()}]"
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 ?: generateID(feature)
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)
override fun <F : Feature<T>, V> setAttribute(id: FeatureId<F>, key: Feature.Attribute<V>, value: V?) {
get(id).attributes[key] = value
}
@Suppress("UNCHECKED_CAST")
public fun FeatureId<DraggableFeature<T>>.onDrag(
listener: PointerEvent.(from: ViewPoint<T>, to: ViewPoint<T>) -> Unit,
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,
) {
with(get(this)) {
attributes[DragListenerAttribute] =
(attributes[DragListenerAttribute] ?: emptySet()) + DragListener { event, from, to ->
event.listener(from as ViewPoint<T>, to as ViewPoint<T>)
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
@ -89,7 +108,7 @@ public class FeatureCollection<T : Any>(
@Suppress("UNCHECKED_CAST")
public fun FeatureId<DraggableFeature<T>>.draggable(
constraint: ((T) -> T)? = null,
listener: (PointerEvent.(from: ViewPoint<T>, to: ViewPoint<T>) -> Unit)? = null
listener: (PointerEvent.(from: ViewPoint<T>, to: ViewPoint<T>) -> Unit)? = null,
) {
if (getAttribute(this, DraggableAttribute) == null) {
val handle = DragHandle.withPrimaryButton<Any> { event, start, end ->
@ -107,7 +126,7 @@ public class FeatureCollection<T : Any>(
DragResult(end, true)
}
}
setAttribute(this, DraggableAttribute, handle)
this.withAttribute(DraggableAttribute, handle)
}
//Apply callback
@ -116,6 +135,42 @@ public class FeatureCollection<T : Any>(
}
}
@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.
*/
@ -128,36 +183,6 @@ public class FeatureCollection<T : Any>(
}
}
@Suppress("UNCHECKED_CAST")
public fun <F : ClickableFeature<T>> FeatureId<F>.onClick(
onClick: PointerEvent.(click: ViewPoint<T>) -> Unit,
) {
with(get(this)) {
attributes[ClickableListenerAttribute] =
(attributes[ClickableListenerAttribute] ?: emptySet()) + ClickListener { event, point ->
event.onClick(point as ViewPoint<T>)
}
}
}
@Suppress("UNCHECKED_CAST")
public fun <A> getAttribute(id: FeatureId<Feature<T>>, key: Feature.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: Feature.Attribute<A>,
block: (id: FeatureId<*>, attributeValue: A) -> Unit,
) {
featureMap.entries.sortedByDescending { it.value.z }.forEach { (id, feature) ->
feature.attributes[key]?.let {
block(FeatureId<Feature<T>>(id), it)
}
}
}
public companion object {
@ -190,7 +215,7 @@ public fun <T : Any> FeatureBuilder<T>.circle(
color: Color = defaultColor,
id: String? = null,
): FeatureId<CircleFeature<T>> = feature(
id, CircleFeature(coordinateSpace, center, zoomRange, size, color)
id, CircleFeature(space, center, zoomRange, size, color)
)
public fun <T : Any> FeatureBuilder<T>.rectangle(
@ -200,7 +225,7 @@ public fun <T : Any> FeatureBuilder<T>.rectangle(
color: Color = defaultColor,
id: String? = null,
): FeatureId<RectangleFeature<T>> = feature(
id, RectangleFeature(coordinateSpace, centerCoordinates, zoomRange, size, color)
id, RectangleFeature(space, centerCoordinates, zoomRange, size, color)
)
public fun <T : Any> FeatureBuilder<T>.draw(
@ -210,7 +235,7 @@ public fun <T : Any> FeatureBuilder<T>.draw(
draw: DrawScope.() -> Unit,
): FeatureId<DrawFeature<T>> = feature(
id,
DrawFeature(coordinateSpace, position, zoomRange, drawFeature = draw)
DrawFeature(space, position, zoomRange, drawFeature = draw)
)
public fun <T : Any> FeatureBuilder<T>.line(
@ -221,7 +246,7 @@ public fun <T : Any> FeatureBuilder<T>.line(
id: String? = null,
): FeatureId<LineFeature<T>> = feature(
id,
LineFeature(coordinateSpace, aCoordinates, bCoordinates, zoomRange, color)
LineFeature(space, aCoordinates, bCoordinates, zoomRange, color)
)
public fun <T : Any> FeatureBuilder<T>.arc(
@ -233,7 +258,7 @@ public fun <T : Any> FeatureBuilder<T>.arc(
id: String? = null,
): FeatureId<ArcFeature<T>> = feature(
id,
ArcFeature(coordinateSpace, oval, startAngle, arcLength, zoomRange, color)
ArcFeature(space, oval, startAngle, arcLength, zoomRange, color)
)
public fun <T : Any> FeatureBuilder<T>.points(
@ -242,9 +267,22 @@ public fun <T : Any> FeatureBuilder<T>.points(
stroke: Float = 2f,
color: Color = defaultColor,
pointMode: PointMode = PointMode.Points,
attributes: Attributes = Attributes.EMPTY,
id: String? = null,
): FeatureId<PointsFeature<T>> =
feature(id, PointsFeature(coordinateSpace, points, zoomRange, stroke, color, pointMode))
): 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,
@ -256,7 +294,7 @@ public fun <T : Any> FeatureBuilder<T>.image(
feature(
id,
VectorImageFeature(
coordinateSpace,
space,
position,
size,
image,
@ -269,8 +307,8 @@ public fun <T : Any> FeatureBuilder<T>.group(
id: String? = null,
builder: FeatureCollection<T>.() -> Unit,
): FeatureId<FeatureGroup<T>> {
val map = FeatureCollection(coordinateSpace).apply(builder).features
val feature = FeatureGroup(coordinateSpace, map, zoomRange)
val map = FeatureCollection(space).apply(builder).features
val feature = FeatureGroup(space, map, zoomRange)
return feature(id, feature)
}
@ -281,7 +319,7 @@ public fun <T : Any> FeatureBuilder<T>.scalableImage(
painter: @Composable () -> Painter,
): FeatureId<ScalableImageFeature<T>> = feature(
id,
ScalableImageFeature<T>(coordinateSpace, box, zoomRange, painter = painter)
ScalableImageFeature<T>(space, box, zoomRange, painter = painter)
)
public fun <T : Any> FeatureBuilder<T>.text(
@ -293,5 +331,5 @@ public fun <T : Any> FeatureBuilder<T>.text(
id: String? = null,
): FeatureId<TextFeature<T>> = feature(
id,
TextFeature(coordinateSpace, position, text, zoomRange, color, fontConfig = font)
TextFeature(space, position, text, zoomRange, color, fontConfig = font)
)

View File

@ -0,0 +1,37 @@
package center.sciprog.maps.features
///**
// * A group of other features
// */
//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)
// }
//
// 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))

View File

@ -4,13 +4,13 @@ import androidx.compose.ui.input.pointer.PointerEvent
import androidx.compose.ui.input.pointer.isPrimaryPressed
import androidx.compose.ui.unit.DpSize
public fun interface ClickListener<in T : Any> {
public fun handle(event: PointerEvent, click: ViewPoint<T>): Unit
public fun interface MouseListener<in T : Any> {
public fun handle(event: PointerEvent, point: ViewPoint<T>): Unit
public companion object {
public fun <T : Any> withPrimaryButton(
block: (event: PointerEvent, click: ViewPoint<T>) -> Unit,
): ClickListener<T> = ClickListener { event, click ->
): MouseListener<T> = MouseListener { event, click ->
if (event.buttons.isPrimaryPressed) {
block(event, click)
}
@ -20,7 +20,7 @@ public fun interface ClickListener<in T : Any> {
public data class ViewConfig<T : Any>(
val zoomSpeed: Float = 1f / 3f,
val onClick: ClickListener<T>? = null,
val onClick: MouseListener<T>? = null,
val dragHandle: DragHandle<T>? = null,
val onViewChange: ViewPoint<T>.() -> Unit = {},
val onSelect: (Rectangle<T>) -> Unit = {},

View File

@ -24,27 +24,31 @@ public fun <T : Any> Modifier.mapControls(
awaitPointerEventScope {
while (true) {
val event = awaitPointerEvent()
if (event.type == PointerEventType.Release) {
val coordinates = event.changes.first().position.toDpOffset().toCoordinates()
val viewPoint = space.ViewPoint(coordinates, zoom)
val point = space.ViewPoint(coordinates, zoom)
val sortedFeatures =features.values.sortedByDescending { it.z }
if (event.type == PointerEventType.Move) {
for (feature in sortedFeatures) {
val listeners = (feature as? DomainFeature)?.attributes?.get(HoverListenerAttribute)
if (listeners != null && point in feature) {
listeners.forEach { it.handle(event, point) }
break
}
}
}
if (event.type == PointerEventType.Release) {
config.onClick?.handle(
event,
viewPoint
point
)
features.values.mapNotNull { feature ->
val clickableFeature = feature as? ClickableFeature
?: return@mapNotNull null
val listeners = clickableFeature.attributes[ClickableListenerAttribute]
?: return@mapNotNull null
if (viewPoint in clickableFeature) {
feature to listeners
} else {
null
for (feature in sortedFeatures) {
val listeners = (feature as? DomainFeature)?.attributes?.get(ClickListenerAttribute)
if (listeners != null && point in feature) {
listeners.forEach { it.handle(event, point) }
break
}
}.maxByOrNull {
it.first.z
}?.second?.forEach {
it.handle(event, viewPoint)
}
}
}

View File

@ -3,6 +3,7 @@ package center.sciprog.maps.features
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.geometry.Size
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.Path
import androidx.compose.ui.graphics.drawscope.DrawScope
import androidx.compose.ui.graphics.drawscope.Stroke
import androidx.compose.ui.graphics.drawscope.drawIntoCanvas
@ -24,7 +25,9 @@ public fun <T : Any> DrawScope.drawFeature(
state: CoordinateViewScope<T>,
painterCache: Map<PainterFeature<T>, Painter>,
feature: Feature<T>,
): Unit = with(state) {
val alpha = feature.attributes[AlphaAttribute]?:1f
fun T.toOffset(): Offset = toOffset(this@drawFeature)
when (feature) {
@ -57,7 +60,8 @@ public fun <T : Any> DrawScope.drawFeature(
useCenter = false,
topLeft = dpRect.topLeft,
size = size,
style = Stroke()
style = Stroke(),
alpha = alpha
)
}
@ -93,6 +97,7 @@ public fun <T : Any> DrawScope.drawFeature(
}
is FeatureGroup -> {
//do nothing
feature.children.values.forEach {
drawFeature(state, painterCache, it)
}
@ -113,7 +118,23 @@ public fun <T : Any> DrawScope.drawFeature(
points = points,
color = feature.color,
strokeWidth = feature.stroke,
pointMode = feature.pointMode
pointMode = feature.pointMode,
alpha = alpha
)
}
is PolygonFeature -> {
val points = feature.points.map { it.toOffset() }
val last = points.last()
val polygonPath = Path()
polygonPath.moveTo(last.x, last.y)
for ((x,y) in points){
polygonPath.lineTo(x,y)
}
drawPath(
path = polygonPath,
color = feature.color,
alpha = alpha
)
}

View File

@ -41,19 +41,17 @@ public fun FeatureBuilder<Gmc>.geoJsonGeometry(
is GeoJsonMultiPolygon -> group(id = id) {
geometry.coordinates.forEach {
points(
polygon(
it.first(),
color = color,
pointMode = PointMode.Polygon
)
}
}
is GeoJsonPoint -> circle(geometry.coordinates, color = color, id = id)
is GeoJsonPolygon -> points(
is GeoJsonPolygon -> polygon(
geometry.coordinates.first(),
color = color,
pointMode = PointMode.Polygon
)
is GeoJsonGeometryCollection -> group(id = id) {
@ -61,6 +59,8 @@ public fun FeatureBuilder<Gmc>.geoJsonGeometry(
geoJsonGeometry(it)
}
}
}.apply {
withAttribute(AlphaAttribute, 0.5f)
}
public fun FeatureBuilder<Gmc>.geoJsonFeature(

View File

@ -6,6 +6,7 @@ import androidx.compose.ui.unit.dp
import center.sciprog.maps.features.CoordinateSpace
import center.sciprog.maps.features.Rectangle
import center.sciprog.maps.features.ViewPoint
import kotlin.math.abs
import kotlin.math.pow
object XYCoordinateSpace : CoordinateSpace<XY> {
@ -70,4 +71,11 @@ object XYCoordinateSpace : CoordinateSpace<XY> {
(b.x - x).dp * zoom,
(b.y - y).dp * zoom
)
override fun XY.isInsidePolygon(points: List<XY>): Boolean = points.zipWithNext().count { (left, right) ->
val dist = right.y - left.y
val intersection = left.y * abs((right.x - x) / dist) +
right.y * abs((x - left.x) / dist)
x in left.x..right.x && intersection >= y
} % 2 == 0
}

View File

@ -25,9 +25,13 @@ fun FeatureBuilder<XY>.background(
)
return feature(
id,
ScalableImageFeature(coordinateSpace, box, zoomRange = defaultZoomRange, painter = painter).apply {
z = -100f
}
ScalableImageFeature(
space,
box,
zoomRange = defaultZoomRange,
painter = painter,
attributes = Attributes(ZAttribute, -100f)
)
)
}
@ -68,7 +72,8 @@ public fun FeatureBuilder<XY>.arc(
startAngle = startAngle,
arcLength = arcLength,
zoomRange = zoomRange,
color = color
color = color,
id = id
)
fun FeatureBuilder<XY>.image(