diff --git a/demo/maps/src/jvmMain/kotlin/Main.kt b/demo/maps/src/jvmMain/kotlin/Main.kt index cca512f..ff157a5 100644 --- a/demo/maps/src/jvmMain/kotlin/Main.kt +++ b/demo/maps/src/jvmMain/kotlin/Main.kt @@ -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>).onHover { + println("Hover on $id") + points(feature.points, color = Color.Blue, id = "selected", attributes = Attributes(ZAttribute, 10f)) + } + } + } } } } diff --git a/maps-kt-compose/src/commonMain/kotlin/center/sciprog/maps/compose/WebMercatorSpace.kt b/maps-kt-compose/src/commonMain/kotlin/center/sciprog/maps/compose/WebMercatorSpace.kt index 5a7af9d..66cf2a5 100644 --- a/maps-kt-compose/src/commonMain/kotlin/center/sciprog/maps/compose/WebMercatorSpace.kt +++ b/maps-kt-compose/src/commonMain/kotlin/center/sciprog/maps/compose/WebMercatorSpace.kt @@ -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 { (mercatorA.y - mercatorB.y).dp * tileScale ) } + + override fun Gmc.isInsidePolygon(points: List): 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 } /** diff --git a/maps-kt-compose/src/commonMain/kotlin/center/sciprog/maps/compose/mapFeatures.kt b/maps-kt-compose/src/commonMain/kotlin/center/sciprog/maps/compose/mapFeatures.kt index 9553115..80fe0eb 100644 --- a/maps-kt-compose/src/commonMain/kotlin/center/sciprog/maps/compose/mapFeatures.kt +++ b/maps-kt-compose/src/commonMain/kotlin/center/sciprog/maps/compose/mapFeatures.kt @@ -18,56 +18,56 @@ public typealias MapFeature = Feature public fun FeatureBuilder.circle( centerCoordinates: Pair, - zoomRange: DoubleRange = defaultZoomRange, + zoomRange: FloatRange = defaultZoomRange, size: Dp = 5.dp, color: Color = defaultColor, id: String? = null, ): FeatureId> = feature( - id, CircleFeature(coordinateSpace, coordinatesOf(centerCoordinates), zoomRange, size, color) + id, CircleFeature(space, coordinatesOf(centerCoordinates), zoomRange, size, color) ) public fun FeatureBuilder.rectangle( centerCoordinates: Pair, - zoomRange: DoubleRange = defaultZoomRange, + zoomRange: FloatRange = defaultZoomRange, size: DpSize = DpSize(5.dp, 5.dp), color: Color = defaultColor, id: String? = null, ): FeatureId> = feature( - id, RectangleFeature(coordinateSpace, coordinatesOf(centerCoordinates), zoomRange, size, color) + id, RectangleFeature(space, coordinatesOf(centerCoordinates), zoomRange, size, color) ) public fun FeatureBuilder.draw( position: Pair, - zoomRange: DoubleRange = defaultZoomRange, + zoomRange: FloatRange = defaultZoomRange, id: String? = null, draw: DrawScope.() -> Unit, ): FeatureId> = feature( id, - DrawFeature(coordinateSpace, coordinatesOf(position), zoomRange, drawFeature = draw) + DrawFeature(space, coordinatesOf(position), zoomRange, drawFeature = draw) ) public fun FeatureBuilder.line( curve: GmcCurve, - zoomRange: DoubleRange = defaultZoomRange, + zoomRange: FloatRange = defaultZoomRange, color: Color = defaultColor, id: String? = null, ): FeatureId> = 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.line( aCoordinates: Pair, bCoordinates: Pair, - zoomRange: DoubleRange = defaultZoomRange, + zoomRange: FloatRange = defaultZoomRange, color: Color = defaultColor, id: String? = null, ): FeatureId> = 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.arc( radius: Distance, startAngle: Angle, arcLength: Angle, - zoomRange: DoubleRange = defaultZoomRange, + zoomRange: FloatRange = defaultZoomRange, color: Color = defaultColor, id: String? = null, ): FeatureId> = 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.arc( public fun FeatureBuilder.points( points: List>, - zoomRange: DoubleRange = defaultZoomRange, + zoomRange: FloatRange = defaultZoomRange, stroke: Float = 2f, color: Color = defaultColor, pointMode: PointMode = PointMode.Points, id: String? = null, ): FeatureId> = - 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.image( position: Pair, image: ImageVector, size: DpSize = DpSize(20.dp, 20.dp), - zoomRange: DoubleRange = defaultZoomRange, + zoomRange: FloatRange = defaultZoomRange, id: String? = null, ): FeatureId> = feature( id, VectorImageFeature( - coordinateSpace, + space, coordinatesOf(position), size, image, @@ -121,11 +121,11 @@ public fun FeatureBuilder.image( public fun FeatureBuilder.text( position: Pair, text: String, - zoomRange: DoubleRange = defaultZoomRange, + zoomRange: FloatRange = defaultZoomRange, color: Color = defaultColor, font: FeatureFont.() -> Unit = { size = 16f }, id: String? = null, ): FeatureId> = feature( id, - TextFeature(coordinateSpace, coordinatesOf(position), text, zoomRange, color, fontConfig = font) + TextFeature(space, coordinatesOf(position), text, zoomRange, color, fontConfig = font) ) diff --git a/maps-kt-features/src/commonMain/kotlin/center/sciprog/maps/features/Attribute.kt b/maps-kt-features/src/commonMain/kotlin/center/sciprog/maps/features/Attribute.kt new file mode 100644 index 0000000..2c10878 --- /dev/null +++ b/maps-kt-features/src/commonMain/kotlin/center/sciprog/maps/features/Attribute.kt @@ -0,0 +1,21 @@ +package center.sciprog.maps.features + +import androidx.compose.ui.graphics.Color + +public interface Attribute + +public object ZAttribute : Attribute + +public object DraggableAttribute : Attribute> + +public object DragListenerAttribute : Attribute>> + +public object ClickListenerAttribute : Attribute>> + +public object HoverListenerAttribute : Attribute>> + +public object VisibleAttribute : Attribute + +public object ColorAttribute : Attribute + +public object AlphaAttribute : Attribute diff --git a/maps-kt-features/src/commonMain/kotlin/center/sciprog/maps/features/AttributeMap.kt b/maps-kt-features/src/commonMain/kotlin/center/sciprog/maps/features/AttributeMap.kt deleted file mode 100644 index ec8de10..0000000 --- a/maps-kt-features/src/commonMain/kotlin/center/sciprog/maps/features/AttributeMap.kt +++ /dev/null @@ -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 - -public object DraggableAttribute : Feature.Attribute> - -public object DragListenerAttribute : Feature.Attribute>> - -public object ClickableListenerAttribute : Feature.Attribute>> - -public object VisibleAttribute : Feature.Attribute - -public object ColorAttribute : Feature.Attribute - -public class AttributeMap { - public val map: MutableMap, Any> = mutableStateMapOf() - - public operator fun > set( - attribute: A, - attrValue: T?, - ) { - if (attrValue == null) { - map.remove(attribute) - } else { - map[attribute] = attrValue - } - } - - @Suppress("UNCHECKED_CAST") - public operator fun get(attribute: Feature.Attribute): 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 - } \ No newline at end of file diff --git a/maps-kt-features/src/commonMain/kotlin/center/sciprog/maps/features/Attributes.kt b/maps-kt-features/src/commonMain/kotlin/center/sciprog/maps/features/Attributes.kt new file mode 100644 index 0000000..1f14e7e --- /dev/null +++ b/maps-kt-features/src/commonMain/kotlin/center/sciprog/maps/features/Attributes.kt @@ -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, Any>) { + @Suppress("UNCHECKED_CAST") + public operator fun get(attribute: Attribute): T? = map[attribute] as? T + + public fun Attribute.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 > Attributes.withAttribute( + attribute: A, + attrValue: T?, +): Attributes = Attributes( + if (attrValue == null) { + map - attribute + } else { + map + (attribute to attrValue) + } +) + +public fun > 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 +// } \ No newline at end of file diff --git a/maps-kt-features/src/commonMain/kotlin/center/sciprog/maps/features/CoordinateSpace.kt b/maps-kt-features/src/commonMain/kotlin/center/sciprog/maps/features/CoordinateSpace.kt index 92f689c..1775008 100644 --- a/maps-kt-features/src/commonMain/kotlin/center/sciprog/maps/features/CoordinateSpace.kt +++ b/maps-kt-features/src/commonMain/kotlin/center/sciprog/maps/features/CoordinateSpace.kt @@ -76,6 +76,8 @@ public interface CoordinateSpace { return distanceVale.dp } + + public fun T.isInsidePolygon(points: List): Boolean } public fun CoordinateSpace.Rectangle(viewPoint: ViewPoint, size: DpSize): Rectangle = diff --git a/maps-kt-features/src/commonMain/kotlin/center/sciprog/maps/features/Feature.kt b/maps-kt-features/src/commonMain/kotlin/center/sciprog/maps/features/Feature.kt index f74e79f..8cda080 100644 --- a/maps-kt-features/src/commonMain/kotlin/center/sciprog/maps/features/Feature.kt +++ b/maps-kt-features/src/commonMain/kotlin/center/sciprog/maps/features/Feature.kt @@ -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 public interface Feature { - public interface Attribute public val space: CoordinateSpace public val zoomRange: FloatRange - public val attributes: AttributeMap + public val attributes: Attributes public fun getBoundingBox(zoom: Float): Rectangle? + + public fun withAttributes(modify: Attributes.() -> Attributes): Feature } public interface PainterFeature : Feature { @@ -34,13 +35,13 @@ public interface PainterFeature : Feature { public fun getPainter(): Painter } -public interface ClickableFeature : Feature { +public interface DomainFeature : Feature { public operator fun contains(viewPoint: ViewPoint): Boolean = getBoundingBox(viewPoint.zoom)?.let { viewPoint.focus in it } ?: false } -public interface DraggableFeature : ClickableFeature { +public interface DraggableFeature : DomainFeature { public fun withCoordinates(newCoordinates: T): Feature } @@ -58,24 +59,24 @@ public fun Iterable>.computeBoundingBox( mapNotNull { it.getBoundingBox(zoom) }.wrapRectangles() } -//public fun Pair.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( +@Stable +public data class FeatureSelector( override val space: CoordinateSpace, override val zoomRange: FloatRange, - override val attributes: AttributeMap = AttributeMap(), + override val attributes: Attributes = Attributes.EMPTY, public val selector: (zoom: Float) -> Feature, ) : Feature { override fun getBoundingBox(zoom: Float): Rectangle? = selector(zoom).getBoundingBox(zoom) + + override fun withAttributes(modify: Attributes.() -> Attributes): Feature = copy(attributes = modify(attributes)) } -public class PathFeature( +@Stable +public data class PathFeature( override val space: CoordinateSpace, public val rectangle: Rectangle, public val path: Path, @@ -83,7 +84,7 @@ public class PathFeature( 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 { override fun withCoordinates(newCoordinates: T): Feature = with(space) { PathFeature( @@ -98,126 +99,172 @@ public class PathFeature( } override fun getBoundingBox(zoom: Float): Rectangle = rectangle - + override fun withAttributes(modify: Attributes.() -> Attributes): Feature = copy(attributes = modify(attributes)) } -public class PointsFeature( +@Stable +public data class PointsFeature( override val space: CoordinateSpace, public val points: List, 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 { - override fun getBoundingBox(zoom: Float): Rectangle? = with(space) { - points.wrapPoints() + + private val boundingBox by lazy { + with(space) { points.wrapPoints() } } + + override fun getBoundingBox(zoom: Float): Rectangle? = boundingBox + override fun withAttributes(modify: (Attributes) -> Attributes): Feature = copy(attributes = modify(attributes)) } +@Stable +public data class PolygonFeature( + override val space: CoordinateSpace, + public val points: List, + override val zoomRange: FloatRange, + public val color: Color = Color.Red, + override val attributes: Attributes = Attributes.EMPTY, +) : DomainFeature { + + private val boundingBox: Rectangle? by lazy { + with(space) { points.wrapPoints() } + } + + override fun getBoundingBox(zoom: Float): Rectangle? = boundingBox + + override fun contains(viewPoint: ViewPoint): Boolean = viewPoint.focus in boundingBox!!//with(space) { viewPoint.focus.isInsidePolygon(points) } + + override fun withAttributes(modify: (Attributes) -> Attributes): Feature = copy(attributes = modify(attributes)) +} + +@Stable public data class CircleFeature( override val space: CoordinateSpace, 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 { override fun getBoundingBox(zoom: Float): Rectangle = space.Rectangle(center, zoom, DpSize(size, size)) - override fun withCoordinates(newCoordinates: T): Feature = - CircleFeature(space, newCoordinates, zoomRange, size, color, attributes) + override fun withCoordinates(newCoordinates: T): Feature = copy(center = newCoordinates) + + override fun withAttributes(modify: (Attributes) -> Attributes): Feature = copy(attributes = modify(attributes)) } -public class RectangleFeature( +@Stable +public data class RectangleFeature( override val space: CoordinateSpace, 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 { override fun getBoundingBox(zoom: Float): Rectangle = space.Rectangle(center, zoom, size) - override fun withCoordinates(newCoordinates: T): Feature = - RectangleFeature(space, newCoordinates, zoomRange, size, color, attributes) + override fun withCoordinates(newCoordinates: T): Feature = copy(center = newCoordinates) + + override fun withAttributes(modify: (Attributes) -> Attributes): Feature = copy(attributes = modify(attributes)) } -public class LineFeature( +@Stable +public data class LineFeature( override val space: CoordinateSpace, public val a: T, public val b: T, override val zoomRange: FloatRange, public val color: Color = Color.Red, - override val attributes: AttributeMap = AttributeMap(), -) : ClickableFeature { + override val attributes: Attributes = Attributes.EMPTY, +) : DomainFeature { override fun getBoundingBox(zoom: Float): Rectangle = space.Rectangle(a, b) override fun contains(viewPoint: ViewPoint): 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 = 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( +@Stable +public data class ArcFeature( override val space: CoordinateSpace, public val oval: Rectangle, 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 { override fun getBoundingBox(zoom: Float): Rectangle = oval - override fun withCoordinates(newCoordinates: T): Feature = with(space) { - ArcFeature(space, oval.withCenter(newCoordinates), startAngle, arcLength, zoomRange, color, attributes) - } + override fun withCoordinates(newCoordinates: T): Feature = + copy(oval = with(space) { oval.withCenter(newCoordinates) }) + + override fun withAttributes(modify: (Attributes) -> Attributes): Feature = copy(attributes = modify(attributes)) } public data class DrawFeature( override val space: CoordinateSpace, 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 { override fun getBoundingBox(zoom: Float): Rectangle = space.Rectangle(position, position) override fun withCoordinates(newCoordinates: T): Feature = copy(position = newCoordinates) + + override fun withAttributes(modify: (Attributes) -> Attributes): Feature = copy(attributes = modify(attributes)) } +@Stable public data class BitmapImageFeature( override val space: CoordinateSpace, 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 { override fun getBoundingBox(zoom: Float): Rectangle = space.Rectangle(center, zoom, size) override fun withCoordinates(newCoordinates: T): Feature = copy(center = newCoordinates) + + override fun withAttributes(modify: (Attributes) -> Attributes): Feature = copy(attributes = modify(attributes)) } +@Stable public data class VectorImageFeature( override val space: CoordinateSpace, 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, PainterFeature { override fun getBoundingBox(zoom: Float): Rectangle = space.Rectangle(center, zoom, size) override fun withCoordinates(newCoordinates: T): Feature = copy(center = newCoordinates) + override fun withAttributes(modify: (Attributes) -> Attributes): Feature = copy(attributes = modify(attributes)) + @Composable override fun getPainter(): VectorPainter = rememberVectorPainter(image) } @@ -227,45 +274,50 @@ public data class VectorImageFeature( * * @param rectangle the size of background in scheme size units. The screen units to scheme units ratio equals scale. */ -public class ScalableImageFeature( +public data class ScalableImageFeature( override val space: CoordinateSpace, public val rectangle: Rectangle, override val zoomRange: FloatRange, - override val attributes: AttributeMap = AttributeMap(), + override val attributes: Attributes = Attributes.EMPTY, public val painter: @Composable () -> Painter, ) : Feature, PainterFeature { @Composable override fun getPainter(): Painter = painter.invoke() override fun getBoundingBox(zoom: Float): Rectangle = rectangle + + override fun withAttributes(modify: (Attributes) -> Attributes): Feature = copy(attributes = modify(attributes)) } /** * A group of other features */ -public class FeatureGroup( +public data class FeatureGroup( override val space: CoordinateSpace, public val children: Map, Feature>, override val zoomRange: FloatRange, - override val attributes: AttributeMap = AttributeMap(), + override val attributes: Attributes = Attributes.EMPTY, ) : Feature { override fun getBoundingBox(zoom: Float): Rectangle? = with(space) { children.values.mapNotNull { it.getBoundingBox(zoom) }.wrapRectangles() } + + override fun withAttributes(modify: Attributes.() -> Attributes): Feature = copy(attributes = attributes) } -public class TextFeature( +public data class TextFeature( override val space: CoordinateSpace, 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 { override fun getBoundingBox(zoom: Float): Rectangle = space.Rectangle(position, position) - override fun withCoordinates(newCoordinates: T): Feature = - TextFeature(space, newCoordinates, text, zoomRange, color, attributes, fontConfig) + override fun withCoordinates(newCoordinates: T): Feature = copy(position = newCoordinates) + + override fun withAttributes(modify: (Attributes) -> Attributes): Feature = copy(attributes = modify(attributes)) } diff --git a/maps-kt-features/src/commonMain/kotlin/center/sciprog/maps/features/FeatureCollection.kt b/maps-kt-features/src/commonMain/kotlin/center/sciprog/maps/features/FeatureCollection.kt index 46b94fa..22463ce 100644 --- a/maps-kt-features/src/commonMain/kotlin/center/sciprog/maps/features/FeatureCollection.kt +++ b/maps-kt-features/src/commonMain/kotlin/center/sciprog/maps/features/FeatureCollection.kt @@ -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>(public val id: String) public interface FeatureBuilder { - public val coordinateSpace: CoordinateSpace + public val space: CoordinateSpace + + public fun generateUID(feature: Feature?): String public fun > feature(id: String?, feature: F): FeatureId - public fun , V> setAttribute(id: FeatureId, key: Feature.Attribute, value: V?) + public fun , V> FeatureId.withAttribute(key: Attribute, value: V?): FeatureId public val defaultColor: Color get() = Color.Red @@ -38,11 +41,11 @@ public fun > FeatureBuilder.feature(id: FeatureId, feature(id.id, feature) public class FeatureCollection( - override val coordinateSpace: CoordinateSpace, -) : CoordinateSpace by coordinateSpace, FeatureBuilder { + override val space: CoordinateSpace, +) : CoordinateSpace by space, FeatureBuilder { @PublishedApi - internal val featureMap: MutableMap> = mutableStateMapOf() + internal val featureMap: SnapshotStateMap> = mutableStateMapOf() public val features: Map, Feature> get() = featureMap.mapKeys { FeatureId>(it.key) } @@ -51,31 +54,47 @@ public class FeatureCollection( public operator fun > get(id: FeatureId): F = featureMap[id.id]?.let { it as F } ?: error("Feature with id=$id not found") - private fun generateID(feature: Feature): String = "@feature[${feature.hashCode().toUInt()}]" + private var uidCounter = 0 + + override fun generateUID(feature: Feature?): String = if(feature == null){ + "@group[${uidCounter++}]" + } else { + "@${feature::class.simpleName}[${uidCounter++}]" + } override fun > feature(id: String?, feature: F): FeatureId { - val safeId = id ?: generateID(feature) + val safeId = id ?: generateUID(feature) featureMap[safeId] = feature return FeatureId(safeId) } public fun > feature(id: FeatureId, feature: F): FeatureId = feature(id.id, feature) + @Suppress("UNCHECKED_CAST") + public fun getAttribute(id: FeatureId>, key: Attribute): A? = + get(id).attributes[key] - override fun , V> setAttribute(id: FeatureId, key: Feature.Attribute, value: V?) { - get(id).attributes[key] = value + /** + * Process all features with a given attribute from the one with highest [z] to lowest + */ + public inline fun forEachWithAttribute( + key: Attribute, + block: (id: FeatureId<*>, feature: Feature, attributeValue: A) -> Unit, + ) { + featureMap.entries.sortedByDescending { it.value.z }.forEach { (id, feature) -> + feature.attributes[key]?.let { + block(FeatureId>(id), feature, it) + } + } } - @Suppress("UNCHECKED_CAST") - public fun FeatureId>.onDrag( - listener: PointerEvent.(from: ViewPoint, to: ViewPoint) -> Unit, - ) { - with(get(this)) { - attributes[DragListenerAttribute] = - (attributes[DragListenerAttribute] ?: emptySet()) + DragListener { event, from, to -> - event.listener(from as ViewPoint, to as ViewPoint) - } - } + public fun , V> FeatureId.modifyAttributes(modify: Attributes.() -> Attributes) { + feature(this, get(this).withAttributes(modify)) + } + + override fun , V> FeatureId.withAttribute(key: Attribute, value: V?): FeatureId { + feature(this, get(this).withAttributes { withAttribute(key, value) }) + return this } @@ -89,7 +108,7 @@ public class FeatureCollection( @Suppress("UNCHECKED_CAST") public fun FeatureId>.draggable( constraint: ((T) -> T)? = null, - listener: (PointerEvent.(from: ViewPoint, to: ViewPoint) -> Unit)? = null + listener: (PointerEvent.(from: ViewPoint, to: ViewPoint) -> Unit)? = null, ) { if (getAttribute(this, DraggableAttribute) == null) { val handle = DragHandle.withPrimaryButton { event, start, end -> @@ -107,7 +126,7 @@ public class FeatureCollection( DragResult(end, true) } } - setAttribute(this, DraggableAttribute, handle) + this.withAttribute(DraggableAttribute, handle) } //Apply callback @@ -116,6 +135,42 @@ public class FeatureCollection( } } + + @Suppress("UNCHECKED_CAST") + public fun FeatureId>.onDrag( + listener: PointerEvent.(from: ViewPoint, to: ViewPoint) -> Unit, + ) { + withAttribute( + DragListenerAttribute, + (getAttribute(this, DragListenerAttribute) ?: emptySet()) + + DragListener { event, from, to -> + event.listener(from as ViewPoint, to as ViewPoint) + } + ) + } + + @Suppress("UNCHECKED_CAST") + public fun > FeatureId.onClick( + onClick: PointerEvent.(click: ViewPoint) -> Unit, + ) { + withAttribute( + ClickListenerAttribute, + (getAttribute(this, ClickListenerAttribute) ?: emptySet()) + + MouseListener { event, point -> event.onClick(point as ViewPoint) } + ) + } + + @Suppress("UNCHECKED_CAST") + public fun > FeatureId.onHover( + onClick: PointerEvent.(move: ViewPoint) -> Unit, + ) { + withAttribute( + HoverListenerAttribute, + (getAttribute(this, HoverListenerAttribute) ?: emptySet()) + + MouseListener { event, point -> event.onClick(point as ViewPoint) } + ) + } + /** * Cyclic update of a feature. Called infinitely until canceled. */ @@ -128,36 +183,6 @@ public class FeatureCollection( } } - @Suppress("UNCHECKED_CAST") - public fun > FeatureId.onClick( - onClick: PointerEvent.(click: ViewPoint) -> Unit, - ) { - with(get(this)) { - attributes[ClickableListenerAttribute] = - (attributes[ClickableListenerAttribute] ?: emptySet()) + ClickListener { event, point -> - event.onClick(point as ViewPoint) - } - } - } - - - @Suppress("UNCHECKED_CAST") - public fun getAttribute(id: FeatureId>, key: Feature.Attribute): A? = - get(id).attributes[key] - - /** - * Process all features with a given attribute from the one with highest [z] to lowest - */ - public inline fun forEachWithAttribute( - key: Feature.Attribute, - block: (id: FeatureId<*>, attributeValue: A) -> Unit, - ) { - featureMap.entries.sortedByDescending { it.value.z }.forEach { (id, feature) -> - feature.attributes[key]?.let { - block(FeatureId>(id), it) - } - } - } public companion object { @@ -190,7 +215,7 @@ public fun FeatureBuilder.circle( color: Color = defaultColor, id: String? = null, ): FeatureId> = feature( - id, CircleFeature(coordinateSpace, center, zoomRange, size, color) + id, CircleFeature(space, center, zoomRange, size, color) ) public fun FeatureBuilder.rectangle( @@ -200,7 +225,7 @@ public fun FeatureBuilder.rectangle( color: Color = defaultColor, id: String? = null, ): FeatureId> = feature( - id, RectangleFeature(coordinateSpace, centerCoordinates, zoomRange, size, color) + id, RectangleFeature(space, centerCoordinates, zoomRange, size, color) ) public fun FeatureBuilder.draw( @@ -210,7 +235,7 @@ public fun FeatureBuilder.draw( draw: DrawScope.() -> Unit, ): FeatureId> = feature( id, - DrawFeature(coordinateSpace, position, zoomRange, drawFeature = draw) + DrawFeature(space, position, zoomRange, drawFeature = draw) ) public fun FeatureBuilder.line( @@ -221,7 +246,7 @@ public fun FeatureBuilder.line( id: String? = null, ): FeatureId> = feature( id, - LineFeature(coordinateSpace, aCoordinates, bCoordinates, zoomRange, color) + LineFeature(space, aCoordinates, bCoordinates, zoomRange, color) ) public fun FeatureBuilder.arc( @@ -233,7 +258,7 @@ public fun FeatureBuilder.arc( id: String? = null, ): FeatureId> = feature( id, - ArcFeature(coordinateSpace, oval, startAngle, arcLength, zoomRange, color) + ArcFeature(space, oval, startAngle, arcLength, zoomRange, color) ) public fun FeatureBuilder.points( @@ -242,9 +267,22 @@ public fun FeatureBuilder.points( stroke: Float = 2f, color: Color = defaultColor, pointMode: PointMode = PointMode.Points, + attributes: Attributes = Attributes.EMPTY, id: String? = null, -): FeatureId> = - feature(id, PointsFeature(coordinateSpace, points, zoomRange, stroke, color, pointMode)) +): FeatureId> = feature( + id, + PointsFeature(space, points, zoomRange, stroke, color, pointMode, attributes) +) + +public fun FeatureBuilder.polygon( + points: List, + zoomRange: FloatRange = defaultZoomRange, + color: Color = defaultColor, + id: String? = null, +): FeatureId> = feature( + id, + PolygonFeature(space, points, zoomRange, color) +) public fun FeatureBuilder.image( position: T, @@ -256,7 +294,7 @@ public fun FeatureBuilder.image( feature( id, VectorImageFeature( - coordinateSpace, + space, position, size, image, @@ -269,8 +307,8 @@ public fun FeatureBuilder.group( id: String? = null, builder: FeatureCollection.() -> Unit, ): FeatureId> { - 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 FeatureBuilder.scalableImage( painter: @Composable () -> Painter, ): FeatureId> = feature( id, - ScalableImageFeature(coordinateSpace, box, zoomRange, painter = painter) + ScalableImageFeature(space, box, zoomRange, painter = painter) ) public fun FeatureBuilder.text( @@ -293,5 +331,5 @@ public fun FeatureBuilder.text( id: String? = null, ): FeatureId> = feature( id, - TextFeature(coordinateSpace, position, text, zoomRange, color, fontConfig = font) + TextFeature(space, position, text, zoomRange, color, fontConfig = font) ) diff --git a/maps-kt-features/src/commonMain/kotlin/center/sciprog/maps/features/FeatureGroup.kt b/maps-kt-features/src/commonMain/kotlin/center/sciprog/maps/features/FeatureGroup.kt new file mode 100644 index 0000000..623d3b4 --- /dev/null +++ b/maps-kt-features/src/commonMain/kotlin/center/sciprog/maps/features/FeatureGroup.kt @@ -0,0 +1,37 @@ +package center.sciprog.maps.features + +///** +// * A group of other features +// */ +//public data class FeatureGroup( +// val parentBuilder: FeatureBuilder, +// private val groupId: String, +// public override val zoomRange: FloatRange, +// override val attributes: Attributes = Attributes.EMPTY, +//) : FeatureBuilder, Feature { +// +// override val space: CoordinateSpace get() = parentBuilder.space +// +// override fun generateUID(feature: Feature?): String = parentBuilder.generateUID(feature) +// +// override fun > feature(id: String?, feature: F): FeatureId = +// parentBuilder.feature("${groupId}.${id ?: parentBuilder.generateUID(feature)}", feature) +// +// override fun , V> FeatureId.withAttribute(key: Attribute, value: V?): FeatureId = +// with(parentBuilder) { +// FeatureId("${groupId}.${this@withAttribute.id}").withAttribute(key, value) +// } +// +// override fun getBoundingBox(zoom: Float): Rectangle? { +// TODO("Not yet implemented") +// } +// +// override fun withAttributes(modify: Attributes.() -> Attributes): Feature = +// copy(attributes = attributes.modify()) +//} +// +//public fun FeatureBuilder.group( +// zoomRange: FloatRange = defaultZoomRange, +// id: String? = null, +// builder: FeatureBuilder.() -> Unit, +//): FeatureId> = feature(id, FeatureGroup(this, id ?: generateUID(null), zoomRange).apply(builder)) diff --git a/maps-kt-features/src/commonMain/kotlin/center/sciprog/maps/features/ViewConfig.kt b/maps-kt-features/src/commonMain/kotlin/center/sciprog/maps/features/ViewConfig.kt index f109e39..7c91f37 100644 --- a/maps-kt-features/src/commonMain/kotlin/center/sciprog/maps/features/ViewConfig.kt +++ b/maps-kt-features/src/commonMain/kotlin/center/sciprog/maps/features/ViewConfig.kt @@ -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 { - public fun handle(event: PointerEvent, click: ViewPoint): Unit +public fun interface MouseListener { + public fun handle(event: PointerEvent, point: ViewPoint): Unit public companion object { public fun withPrimaryButton( block: (event: PointerEvent, click: ViewPoint) -> Unit, - ): ClickListener = ClickListener { event, click -> + ): MouseListener = MouseListener { event, click -> if (event.buttons.isPrimaryPressed) { block(event, click) } @@ -20,7 +20,7 @@ public fun interface ClickListener { public data class ViewConfig( val zoomSpeed: Float = 1f / 3f, - val onClick: ClickListener? = null, + val onClick: MouseListener? = null, val dragHandle: DragHandle? = null, val onViewChange: ViewPoint.() -> Unit = {}, val onSelect: (Rectangle) -> Unit = {}, diff --git a/maps-kt-features/src/jvmMain/kotlin/center/sciprog/maps/compose/mapControls.kt b/maps-kt-features/src/jvmMain/kotlin/center/sciprog/maps/compose/mapControls.kt index c75aa03..9a8e211 100644 --- a/maps-kt-features/src/jvmMain/kotlin/center/sciprog/maps/compose/mapControls.kt +++ b/maps-kt-features/src/jvmMain/kotlin/center/sciprog/maps/compose/mapControls.kt @@ -24,27 +24,31 @@ public fun Modifier.mapControls( awaitPointerEventScope { while (true) { val event = awaitPointerEvent() + val coordinates = event.changes.first().position.toDpOffset().toCoordinates() + val point = space.ViewPoint(coordinates, zoom) + + val sortedFeatures =features.values.sortedByDescending { it.z } + + if (event.type == PointerEventType.Move) { + for (feature in sortedFeatures) { + 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) { - val coordinates = event.changes.first().position.toDpOffset().toCoordinates() - val viewPoint = space.ViewPoint(coordinates, zoom) 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) } } } diff --git a/maps-kt-features/src/jvmMain/kotlin/center/sciprog/maps/features/drawFeature.kt b/maps-kt-features/src/jvmMain/kotlin/center/sciprog/maps/features/drawFeature.kt index 5460d62..71d1706 100644 --- a/maps-kt-features/src/jvmMain/kotlin/center/sciprog/maps/features/drawFeature.kt +++ b/maps-kt-features/src/jvmMain/kotlin/center/sciprog/maps/features/drawFeature.kt @@ -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 DrawScope.drawFeature( state: CoordinateViewScope, painterCache: Map, Painter>, feature: Feature, + ): Unit = with(state) { + val alpha = feature.attributes[AlphaAttribute]?:1f fun T.toOffset(): Offset = toOffset(this@drawFeature) when (feature) { @@ -57,7 +60,8 @@ public fun DrawScope.drawFeature( useCenter = false, topLeft = dpRect.topLeft, size = size, - style = Stroke() + style = Stroke(), + alpha = alpha ) } @@ -93,6 +97,7 @@ public fun DrawScope.drawFeature( } is FeatureGroup -> { + //do nothing feature.children.values.forEach { drawFeature(state, painterCache, it) } @@ -113,7 +118,23 @@ public fun 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 ) } diff --git a/maps-kt-geojson/src/commonMain/kotlin/center/sciprog/maps/geojson/geoJsonFeature.kt b/maps-kt-geojson/src/commonMain/kotlin/center/sciprog/maps/geojson/geoJsonFeature.kt index 29243a5..6a85432 100644 --- a/maps-kt-geojson/src/commonMain/kotlin/center/sciprog/maps/geojson/geoJsonFeature.kt +++ b/maps-kt-geojson/src/commonMain/kotlin/center/sciprog/maps/geojson/geoJsonFeature.kt @@ -41,19 +41,17 @@ public fun FeatureBuilder.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.geoJsonGeometry( geoJsonGeometry(it) } } +}.apply { + withAttribute(AlphaAttribute, 0.5f) } public fun FeatureBuilder.geoJsonFeature( diff --git a/maps-kt-scheme/src/commonMain/kotlin/center/sciprog/maps/scheme/XYCoordinateSpace.kt b/maps-kt-scheme/src/commonMain/kotlin/center/sciprog/maps/scheme/XYCoordinateSpace.kt index ea89b4b..32bf0fa 100644 --- a/maps-kt-scheme/src/commonMain/kotlin/center/sciprog/maps/scheme/XYCoordinateSpace.kt +++ b/maps-kt-scheme/src/commonMain/kotlin/center/sciprog/maps/scheme/XYCoordinateSpace.kt @@ -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 { @@ -70,4 +71,11 @@ object XYCoordinateSpace : CoordinateSpace { (b.x - x).dp * zoom, (b.y - y).dp * zoom ) + + override fun XY.isInsidePolygon(points: List): 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 } \ No newline at end of file diff --git a/maps-kt-scheme/src/commonMain/kotlin/center/sciprog/maps/scheme/schemeFeatures.kt b/maps-kt-scheme/src/commonMain/kotlin/center/sciprog/maps/scheme/schemeFeatures.kt index ced586c..2826827 100644 --- a/maps-kt-scheme/src/commonMain/kotlin/center/sciprog/maps/scheme/schemeFeatures.kt +++ b/maps-kt-scheme/src/commonMain/kotlin/center/sciprog/maps/scheme/schemeFeatures.kt @@ -25,9 +25,13 @@ fun FeatureBuilder.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.arc( startAngle = startAngle, arcLength = arcLength, zoomRange = zoomRange, - color = color + color = color, + id = id ) fun FeatureBuilder.image(