Split points and multiline

This commit is contained in:
Alexander Nozik 2023-02-13 16:49:36 +03:00
parent 3219e13fa7
commit 90eb7b4575
12 changed files with 173 additions and 92 deletions

View File

@ -10,7 +10,7 @@ val kmathVersion: String by extra("0.3.1-dev-10")
allprojects {
group = "center.sciprog"
version = "0.2.2-dev-4"
version = "0.2.2-dev-5"
repositories {
mavenLocal()

View File

@ -8,7 +8,6 @@ import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.PointMode
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.unit.DpSize
import androidx.compose.ui.unit.dp
@ -91,7 +90,7 @@ fun App() {
println("line 3 clicked")
}
points(
multiLine(
points = listOf(
55.742465 to 37.615812,
55.742713 to 37.616370,
@ -100,7 +99,6 @@ fun App() {
55.742086 to 37.616566,
55.741715 to 37.616716
),
pointMode = PointMode.Polygon
)
//remember feature ID
@ -138,13 +136,11 @@ fun App() {
println("Click on ${ref.id}")
//draw in top-level scope
with(this@MapView) {
points(
multiLine(
ref.resolve().points,
stroke = 4f,
pointMode = PointMode.Polygon,
attributes = Attributes(ZAttribute, 10f),
id = "selected",
).color(Color.Magenta)
).modifyAttribute(StrokeAttribute, 4f).color(Color.Magenta)
}
}
}

View File

@ -6,7 +6,6 @@ import androidx.compose.material.MaterialTheme
import androidx.compose.material.Text
import androidx.compose.runtime.*
import androidx.compose.runtime.snapshots.SnapshotStateList
import androidx.compose.ui.graphics.PointMode
import androidx.compose.ui.input.pointer.isSecondaryPressed
import androidx.compose.ui.window.Window
import androidx.compose.ui.window.application
@ -26,9 +25,8 @@ fun App() {
val myPolygon: SnapshotStateList<XY> = remember { mutableStateListOf<XY>() }
val featureState: FeatureGroup<XY> = FeatureGroup.remember(XYCoordinateSpace) {
points(
multiLine(
listOf(XY(0f, 0f), XY(0f, 1f), XY(1f, 1f), XY(1f, 0f), XY(0f, 0f)),
pointMode = PointMode.Polygon,
id = "frame"
)
}
@ -36,9 +34,8 @@ fun App() {
if(myPolygon.isNotEmpty()) {
featureState.group("polygon") {
points(
multiLine(
myPolygon + myPolygon.first(),
pointMode = PointMode.Polygon,
)
myPolygon.forEachIndexed { index, xy ->
circle(xy, id = "point[$index]").draggable { _, to ->

View File

@ -1,6 +1,5 @@
package center.sciprog.maps.compose
import androidx.compose.ui.graphics.PointMode
import androidx.compose.ui.graphics.drawscope.DrawScope
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.unit.Dp
@ -32,7 +31,7 @@ public fun FeatureGroup<Gmc>.rectangle(
size: DpSize = DpSize(5.dp, 5.dp),
id: String? = null,
): FeatureRef<Gmc, RectangleFeature<Gmc>> = feature(
id, RectangleFeature(space, coordinatesOf(centerCoordinates), size)
id, RectangleFeature(space, coordinatesOf(centerCoordinates), size)
)
@ -83,11 +82,13 @@ public fun FeatureGroup<Gmc>.arc(
public fun FeatureGroup<Gmc>.points(
points: List<Pair<Double, Double>>,
stroke: Float = 2f,
pointMode: PointMode = PointMode.Points,
id: String? = null,
): FeatureRef<Gmc, PointsFeature<Gmc>> =
feature(id, PointsFeature(space, points.map(::coordinatesOf), stroke, pointMode))
): FeatureRef<Gmc, PointsFeature<Gmc>> = feature(id, PointsFeature(space, points.map(::coordinatesOf)))
public fun FeatureGroup<Gmc>.multiLine(
points: List<Pair<Double, Double>>,
id: String? = null,
): FeatureRef<Gmc, MultiLineFeature<Gmc>> = feature(id, MultiLineFeature(space, points.map(::coordinatesOf)))
public fun FeatureGroup<Gmc>.image(
position: Pair<Double, Double>,

View File

@ -3,7 +3,10 @@ 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.Brush
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.ImageBitmap
import androidx.compose.ui.graphics.Path
import androidx.compose.ui.graphics.drawscope.DrawScope
import androidx.compose.ui.graphics.drawscope.DrawStyle
import androidx.compose.ui.graphics.drawscope.Fill
@ -112,8 +115,6 @@ public data class PathFeature<T : Any>(
public data class PointsFeature<T : Any>(
override val space: CoordinateSpace<T>,
public val points: List<T>,
public val stroke: Float = 2f,
public val pointMode: PointMode = PointMode.Points,
override val attributes: Attributes = Attributes.EMPTY,
) : Feature<T> {
@ -125,6 +126,59 @@ public data class PointsFeature<T : Any>(
override fun withAttributes(modify: (Attributes) -> Attributes): Feature<T> = copy(attributes = modify(attributes))
}
public interface LineSegmentFeature<T : Any> : Feature<T>
@Stable
public data class LineFeature<T : Any>(
override val space: CoordinateSpace<T>,
public val a: T,
public val b: T,
override val attributes: Attributes = Attributes.EMPTY,
) : DomainFeature<T>, LineSegmentFeature<T> {
override fun getBoundingBox(zoom: Float): Rectangle<T> =
space.Rectangle(a, b)
override fun contains(viewPoint: ViewPoint<T>): Boolean = with(space) {
viewPoint.focus in getBoundingBox(viewPoint.zoom) && viewPoint.focus.distanceToLine(
a,
b,
viewPoint.zoom
).value < clickRadius
}
override fun withAttributes(modify: (Attributes) -> Attributes): Feature<T> = copy(attributes = modify(attributes))
}
public data class MultiLineFeature<T : Any>(
override val space: CoordinateSpace<T>,
public val points: List<T>,
override val attributes: Attributes = Attributes.EMPTY,
) : DomainFeature<T>, LineSegmentFeature<T> {
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))
override fun contains(viewPoint: ViewPoint<T>): Boolean = with(space) {
val boundingBox = getBoundingBox(viewPoint.zoom) ?: return@with false
viewPoint.focus in boundingBox && points.zipWithNext().minOf { (a, b) ->
viewPoint.focus.distanceToLine(
a,
b,
viewPoint.zoom
).value
} < clickRadius
}
}
private val <T : Any, F : LineSegmentFeature<T>> F.clickRadius get() = attributes[ClickRadius] ?: 10f
@Stable
public data class PolygonFeature<T : Any>(
override val space: CoordinateSpace<T>,
@ -176,29 +230,6 @@ public data class RectangleFeature<T : Any>(
override fun withAttributes(modify: (Attributes) -> Attributes): Feature<T> = copy(attributes = modify(attributes))
}
@Stable
public data class LineFeature<T : Any>(
override val space: CoordinateSpace<T>,
public val a: T,
public val b: T,
override val attributes: Attributes = Attributes.EMPTY,
) : DomainFeature<T> {
override fun getBoundingBox(zoom: Float): Rectangle<T> =
space.Rectangle(a, b)
private val clickRadius get() = attributes[ClickRadius] ?: 10f
override fun contains(viewPoint: ViewPoint<T>): Boolean = with(space) {
viewPoint.focus in getBoundingBox(viewPoint.zoom) && viewPoint.focus.distanceToLine(
a,
b,
viewPoint.zoom
).value < clickRadius
}
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

View File

@ -4,7 +4,6 @@ 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.PointMode
import androidx.compose.ui.graphics.drawscope.DrawScope
import androidx.compose.ui.graphics.painter.Painter
import androidx.compose.ui.graphics.vector.ImageVector
@ -156,56 +155,68 @@ public inline fun <T : Any, reified F : Feature<T>> FeatureGroup<T>.forEachWithT
public fun <T : Any> FeatureGroup<T>.circle(
center: T,
size: Dp = 5.dp,
attributes: Attributes = Attributes.EMPTY,
id: String? = null,
): FeatureRef<T, CircleFeature<T>> = feature(
id, CircleFeature(space, center, size)
id, CircleFeature(space, center, size, attributes)
)
public fun <T : Any> FeatureGroup<T>.rectangle(
centerCoordinates: T,
size: DpSize = DpSize(5.dp, 5.dp),
attributes: Attributes = Attributes.EMPTY,
id: String? = null,
): FeatureRef<T, RectangleFeature<T>> = feature(
id, RectangleFeature(space, centerCoordinates, size)
id, RectangleFeature(space, centerCoordinates, size, attributes)
)
public fun <T : Any> FeatureGroup<T>.draw(
position: T,
attributes: Attributes = Attributes.EMPTY,
id: String? = null,
draw: DrawScope.() -> Unit,
): FeatureRef<T, DrawFeature<T>> = feature(
id,
DrawFeature(space, position, drawFeature = draw)
DrawFeature(space, position, drawFeature = draw, attributes = attributes)
)
public fun <T : Any> FeatureGroup<T>.line(
aCoordinates: T,
bCoordinates: T,
attributes: Attributes = Attributes.EMPTY,
id: String? = null,
): FeatureRef<T, LineFeature<T>> = feature(
id,
LineFeature(space, aCoordinates, bCoordinates)
LineFeature(space, aCoordinates, bCoordinates, attributes)
)
public fun <T : Any> FeatureGroup<T>.arc(
oval: Rectangle<T>,
startAngle: Angle,
arcLength: Angle,
attributes: Attributes = Attributes.EMPTY,
id: String? = null,
): FeatureRef<T, ArcFeature<T>> = feature(
id,
ArcFeature(space, oval, startAngle, arcLength)
ArcFeature(space, oval, startAngle, arcLength, attributes)
)
public fun <T : Any> FeatureGroup<T>.points(
points: List<T>,
stroke: Float = 2f,
pointMode: PointMode = PointMode.Points,
attributes: Attributes = Attributes.EMPTY,
id: String? = null,
): FeatureRef<T, PointsFeature<T>> = feature(
id,
PointsFeature(space, points, stroke, pointMode, attributes)
PointsFeature(space, points, attributes)
)
public fun <T : Any> FeatureGroup<T>.multiLine(
points: List<T>,
attributes: Attributes = Attributes.EMPTY,
id: String? = null,
): FeatureRef<T, MultiLineFeature<T>> = feature(
id,
MultiLineFeature(space, points, attributes)
)
public fun <T : Any> FeatureGroup<T>.polygon(

View File

@ -32,8 +32,18 @@ public object VisibleAttribute : Attribute<Boolean>
public object ColorAttribute : Attribute<Color>
public fun <T : Any, F : Feature<T>> FeatureRef<T, F>.color(color: Color): FeatureRef<T, F> =
modifyAttribute(ColorAttribute, color)
public object ZoomRangeAttribute : Attribute<FloatRange>
public fun <T : Any, F : Feature<T>> FeatureRef<T, F>.zoomRange(range: FloatRange): FeatureRef<T, F> =
modifyAttribute(ZoomRangeAttribute, range)
public object AlphaAttribute : Attribute<Float>
public fun <T : Any, F : Feature<T>> FeatureRef<T, F>.modifyAttributes(modify: AttributesBuilder.() -> Unit): FeatureRef<T, F> {
@ -47,7 +57,10 @@ public fun <T : Any, F : Feature<T>> FeatureRef<T, F>.modifyAttributes(modify: A
return this
}
public fun <T : Any, F : Feature<T>, V> FeatureRef<T, F>.modifyAttribute(key: Attribute<V>, value: V?): FeatureRef<T, F>{
public fun <T : Any, F : Feature<T>, V> FeatureRef<T, F>.modifyAttribute(
key: Attribute<V>,
value: V?,
): FeatureRef<T, F> {
@Suppress("UNCHECKED_CAST")
parent.feature(id, resolve().withAttributes { withAttribute(key, value) } as F)
return this
@ -59,10 +72,10 @@ public fun <T : Any, F : Feature<T>, V> FeatureRef<T, F>.modifyAttribute(key: A
* @param constraint optional drag constraint
*/
@Suppress("UNCHECKED_CAST")
public fun <T: Any, F : DraggableFeature<T>> FeatureRef<T, F>.draggable(
public fun <T : Any, F : DraggableFeature<T>> FeatureRef<T, F>.draggable(
constraint: ((T) -> T)? = null,
listener: (PointerEvent.(from: ViewPoint<T>, to: ViewPoint<T>) -> Unit)? = null,
): FeatureRef<T, F> = with(parent){
): FeatureRef<T, F> = with(parent) {
if (attributes[DraggableAttribute] == null) {
val handle = DragHandle.withPrimaryButton<Any> { event, start, end ->
val feature = featureMap[id] as? DraggableFeature<T> ?: return@withPrimaryButton DragResult(end)
@ -112,7 +125,7 @@ public fun <T : Any, F : DomainFeature<T>> FeatureRef<T, F>.onClick(
@OptIn(ExperimentalFoundationApi::class)
@Suppress("UNCHECKED_CAST")
public fun <T: Any, F : DomainFeature<T>> FeatureRef<T, F>.onClick(
public fun <T : Any, F : DomainFeature<T>> FeatureRef<T, F>.onClick(
pointerMatcher: PointerMatcher,
keyboardModifiers: PointerKeyboardModifiers.() -> Boolean = { true },
onClick: PointerEvent.(click: ViewPoint<T>) -> Unit,
@ -127,7 +140,7 @@ public fun <T: Any, F : DomainFeature<T>> FeatureRef<T, F>.onClick(
}
@Suppress("UNCHECKED_CAST")
public fun <T: Any, F : DomainFeature<T>> FeatureRef<T, F>.onHover(
public fun <T : Any, F : DomainFeature<T>> FeatureRef<T, F>.onHover(
onClick: PointerEvent.(move: ViewPoint<T>) -> Unit,
): FeatureRef<T, F> = modifyAttributes {
HoverListenerAttribute.add(
@ -147,15 +160,13 @@ public fun <T: Any, F : DomainFeature<T>> FeatureRef<T, F>.onHover(
// )
// }
public fun <T: Any, F : Feature<T>> FeatureRef<T, F>.color(color: Color): FeatureRef<T, F> =
modifyAttribute(ColorAttribute, color)
public fun <T: Any, F : Feature<T>> FeatureRef<T, F>.zoomRange(range: FloatRange): FeatureRef<T, F> =
modifyAttribute(ZoomRangeAttribute, range)
public object PathEffectAttribute : Attribute<PathEffect>
public fun <T : Any> FeatureRef<T, LineSegmentFeature<T>>.pathEffect(effect: PathEffect): FeatureRef<T, LineSegmentFeature<T>> =
modifyAttribute(PathEffectAttribute, effect)
public object StrokeAttribute : Attribute<Float>
public object PathEffectAttribute: Attribute<PathEffect>
public fun <T: Any> FeatureRef<T, PointsFeature<T>>.pathEffect(effect: PathEffect): FeatureRef<T, PointsFeature<T>> =
modifyAttribute(PathEffectAttribute, effect)
public fun <T : Any, F : LineSegmentFeature<T>> FeatureRef<T, F>.stroke(width: Float): FeatureRef<T, F> =
modifyAttribute(StrokeAttribute, width)

View File

@ -94,13 +94,13 @@ public suspend fun PointerInputScope.detectClicks(
if (upOrCancel != null) {
// tap was successful.
if (onDoubleClick == null) {
onClick?.invoke(this, upOrCancel) // no need to check for double-tap.
onClick?.invoke(this, down) // no need to check for double-tap.
} else {
// check for second tap
val secondDown = awaitSecondDown(upOrCancel.firstChange)
if (secondDown == null) {
onClick?.invoke(this, upOrCancel) // no valid second tap started
onClick?.invoke(this, down) // no valid second tap started
} else {
// Second tap down detected
pressScope.reset()
@ -115,16 +115,16 @@ public suspend fun PointerInputScope.detectClicks(
if (secondUp != null) {
secondUp.consume()
pressScope.release()
onDoubleClick(down)
onDoubleClick(secondDown)
} else {
pressScope.cancel()
onClick?.invoke(this, upOrCancel)
onClick?.invoke(this, down)
}
}
} catch (e: PointerEventTimeoutCancellationException) {
// The first tap was valid, but the second tap is a long press.
// notify for the first tap
onClick?.invoke(this, upOrCancel)
onClick?.invoke(this, down)
// notify for the long press
onLongClick?.invoke(this, secondDown)
@ -209,7 +209,6 @@ private suspend fun AwaitPointerEventScope.awaitSecondDown(
/**
* Reads events until the first down is received. If [requireUnconsumed] is `true` and the first
* down is consumed in the [PointerEventPass.Main] pass, that gesture is ignored.
* If it was down caused by [PointerType.Mouse], this function reacts only on primary button.
*/
internal suspend fun AwaitPointerEventScope.awaitFirstDownEvent(
requireUnconsumed: Boolean = true,

View File

@ -2,15 +2,12 @@ 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.*
import androidx.compose.ui.graphics.drawscope.DrawScope
import androidx.compose.ui.graphics.drawscope.Stroke
import androidx.compose.ui.graphics.drawscope.drawIntoCanvas
import androidx.compose.ui.graphics.drawscope.translate
import androidx.compose.ui.graphics.nativeCanvas
import androidx.compose.ui.graphics.painter.Painter
import androidx.compose.ui.graphics.toArgb
import center.sciprog.attributes.plus
import org.jetbrains.skia.Font
import org.jetbrains.skia.Paint
@ -28,7 +25,7 @@ public fun <T : Any> DrawScope.drawFeature(
feature: Feature<T>,
): 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)
when (feature) {
@ -48,7 +45,14 @@ public fun <T : Any> DrawScope.drawFeature(
size = feature.size.toSize()
)
is LineFeature -> drawLine(color, feature.a.toOffset(), feature.b.toOffset())
is LineFeature -> drawLine(
color,
feature.a.toOffset(),
feature.b.toOffset(),
strokeWidth = feature.attributes[StrokeAttribute] ?: Stroke.HairlineWidth,
pathEffect = feature.attributes[PathEffectAttribute]
)
is ArcFeature -> {
val dpRect = feature.oval.toDpRect().toRect()
@ -119,8 +123,20 @@ public fun <T : Any> DrawScope.drawFeature(
drawPoints(
points = points,
color = color,
strokeWidth = feature.stroke,
pointMode = feature.pointMode,
strokeWidth = feature.attributes[StrokeAttribute] ?: Stroke.HairlineWidth,
pointMode = PointMode.Points,
pathEffect = feature.attributes[PathEffectAttribute],
alpha = alpha
)
}
is MultiLineFeature -> {
val points = feature.points.map { it.toOffset() }
drawPoints(
points = points,
color = color,
strokeWidth = feature.attributes[StrokeAttribute] ?: Stroke.HairlineWidth,
pointMode = PointMode.Polygon,
pathEffect = feature.attributes[PathEffectAttribute],
alpha = alpha
)
@ -131,8 +147,8 @@ public fun <T : Any> DrawScope.drawFeature(
val last = points.last()
val polygonPath = Path()
polygonPath.moveTo(last.x, last.y)
for ((x,y) in points){
polygonPath.lineTo(x,y)
for ((x, y) in points) {
polygonPath.lineTo(x, y)
}
drawPath(
path = polygonPath,

View File

@ -1,7 +1,6 @@
package center.sciprog.maps.geojson
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.PointMode
import center.sciprog.attributes.NameAttribute
import center.sciprog.maps.coordinates.Gmc
import center.sciprog.maps.features.*
@ -17,23 +16,18 @@ public fun FeatureGroup<Gmc>.geoJsonGeometry(
geometry: GeoJsonGeometry,
id: String? = null,
): FeatureRef<Gmc, Feature<Gmc>> = when (geometry) {
is GeoJsonLineString -> points(
is GeoJsonLineString -> multiLine(
geometry.coordinates,
pointMode = PointMode.Lines
)
is GeoJsonMultiLineString -> group(id = id) {
geometry.coordinates.forEach {
points(
it,
pointMode = PointMode.Lines
)
multiLine(it)
}
}
is GeoJsonMultiPoint -> points(
geometry.coordinates,
pointMode = PointMode.Points
)
is GeoJsonMultiPolygon -> group(id = id) {

View File

@ -51,7 +51,7 @@ fun FeatureGroup<XY>.line(
aCoordinates: Pair<Number, Number>,
bCoordinates: Pair<Number, Number>,
id: String? = null,
): FeatureRef<XY, LineFeature<XY>> = line(aCoordinates.toCoordinates(), bCoordinates.toCoordinates(), id)
): FeatureRef<XY, LineFeature<XY>> = line(aCoordinates.toCoordinates(), bCoordinates.toCoordinates(), id = id)
public fun FeatureGroup<XY>.arc(

View File

@ -4,6 +4,7 @@ import androidx.compose.runtime.Composable
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.geometry.Size
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.PointMode
import androidx.compose.ui.graphics.drawscope.Stroke
import androidx.compose.ui.graphics.drawscope.drawIntoCanvas
import androidx.compose.ui.graphics.drawscope.translate
@ -77,6 +78,30 @@ fun FeatureStateSnapshot<XY>.generateSvg(
alpha = alpha
)
is PointsFeature -> {
val points = feature.points.map { it.toOffset() }
drawPoints(
points = points,
color = color,
strokeWidth = feature.attributes[StrokeAttribute] ?: Stroke.HairlineWidth,
pointMode = PointMode.Points,
pathEffect = feature.attributes[PathEffectAttribute],
alpha = alpha
)
}
is MultiLineFeature -> {
val points = feature.points.map { it.toOffset() }
drawPoints(
points = points,
color = color,
strokeWidth = feature.attributes[StrokeAttribute] ?: Stroke.HairlineWidth,
pointMode = PointMode.Polygon,
pathEffect = feature.attributes[PathEffectAttribute],
alpha = alpha
)
}
is ArcFeature -> {
val topLeft = feature.oval.leftTop.toOffset()
val bottomRight = feature.oval.rightBottom.toOffset()