Proper polygon contains check

This commit is contained in:
Alexander Nozik 2023-01-04 22:43:51 +03:00
parent ba962acf5c
commit ea8c5571bb
7 changed files with 126 additions and 80 deletions

View File

@ -58,13 +58,15 @@ fun App() {
mapTileProvider = mapTileProvider, mapTileProvider = mapTileProvider,
config = ViewConfig( config = ViewConfig(
onViewChange = { centerCoordinates.value = focus }, onViewChange = { centerCoordinates.value = focus },
onClick = { _, viewPoint -> println(viewPoint) } onClick = { _, viewPoint ->
println(viewPoint)
}
) )
) { ) {
geoJson(URL("https://raw.githubusercontent.com/ggolikov/cities-comparison/master/src/moscow.geo.json")) geoJson(URL("https://raw.githubusercontent.com/ggolikov/cities-comparison/master/src/moscow.geo.json"))
.withAttribute(ColorAttribute, Color.Blue) .attribute(ColorAttribute, Color.Blue)
.withAttribute(AlphaAttribute, 0.4f) .attribute(AlphaAttribute, 0.4f)
image(pointOne, Icons.Filled.Home) image(pointOne, Icons.Filled.Home)
@ -117,24 +119,19 @@ fun App() {
} }
}.launchIn(scope) }.launchIn(scope)
visit { id, feature ->
if (feature is PolygonFeature) { forEachWithType<Gmc, PolygonFeature<Gmc>> { id, feature ->
id as FeatureId<PolygonFeature<Gmc>>
id.onClick { id.onClick {
println("Click on $id") println("Click on $id")
//draw in top-level scope
with(this@MapView) {
points( points(
feature.points, feature.points,
stroke = 4f,
pointMode = PointMode.Polygon,
attributes = Attributes(ZAttribute, 10f),
id = "selected", id = "selected",
attributes = Attributes(ZAttribute, 10f) ).color(Color.Magenta)
).color(Color.Blue)
}
id.onHover {
println("Hover on $id")
points(
feature.points,
id = "selected",
attributes = Attributes(ZAttribute, 10f)
).color(Color.Blue)
} }
} }
} }
@ -142,6 +139,7 @@ fun App() {
} }
} }
fun main() = application { fun main() = application {
Window(onCloseRequest = ::exitApplication) { Window(onCloseRequest = ::exitApplication) {
App() App()

View File

@ -85,11 +85,19 @@ public object WebMercatorSpace : CoordinateSpace<Gmc> {
} }
override fun Gmc.isInsidePolygon(points: List<Gmc>): Boolean = points.zipWithNext().count { (left, right) -> override fun Gmc.isInsidePolygon(points: List<Gmc>): Boolean = points.zipWithNext().count { (left, right) ->
val dist = right.latitude - left.latitude val longitudeRange = if(right.longitude >= left.longitude) {
val intersection = left.latitude * abs((right.longitude - longitude) / dist) + left.longitude..right.longitude
right.latitude * abs((longitude - left.longitude) / dist) } else {
longitude in left.longitude..right.longitude && intersection >= latitude right.longitude..left.longitude
} % 2 == 0 }
if(longitude !in longitudeRange) return@count false
val longitudeDelta = right.longitude - left.longitude
left.latitude * abs((right.longitude - longitude) / longitudeDelta) +
right.latitude * abs((longitude - left.longitude) / longitudeDelta) >= latitude
} % 2 == 1
} }
/** /**

View File

@ -94,7 +94,7 @@ public actual fun MapView(
} }
Canvas(modifier = modifier.mapControls(mapState, featuresState.features).fillMaxSize()) { Canvas(modifier = modifier.mapControls(mapState, featuresState).fillMaxSize()) {
if (canvasSize != size.toDpSize()) { if (canvasSize != size.toDpSize()) {
logger.debug { "Recalculate canvas. Size: $size" } logger.debug { "Recalculate canvas. Size: $size" }

View File

@ -49,12 +49,22 @@ public data class FeatureGroup<T : Any>(
public val features: Collection<Feature<T>> get() = featureMap.values.sortedByDescending { it.z } public val features: Collection<Feature<T>> get() = featureMap.values.sortedByDescending { it.z }
public fun visit(visitor: FeatureGroup<T>.(id: FeatureId<Feature<T>>, feature: Feature<T>) -> Unit) { public fun visit(visitor: FeatureGroup<T>.(id: String, feature: Feature<T>) -> Unit) {
featureMap.forEach { (key, feature) -> featureMap.entries.sortedByDescending { it.value.z }.forEach { (key, feature) ->
if (feature is FeatureGroup<T>) { if (feature is FeatureGroup<T>) {
feature.visit(visitor) feature.visit(visitor)
} else { } else {
visitor(this, FeatureId(key), feature) visitor(this, key, feature)
}
}
}
public fun visitUntil(visitor: FeatureGroup<T>.(id: String, feature: Feature<T>) -> Boolean) {
featureMap.entries.sortedByDescending { it.value.z }.forEach { (key, feature) ->
if (feature is FeatureGroup<T>) {
feature.visitUntil(visitor)
} else {
if (!visitor(this, key, feature)) return@forEach
} }
} }
} }
@ -63,30 +73,12 @@ public data class FeatureGroup<T : Any>(
public fun <A> getAttribute(id: FeatureId<Feature<T>>, key: Attribute<A>): A? = public fun <A> getAttribute(id: FeatureId<Feature<T>>, key: Attribute<A>): A? =
get(id).attributes[key] get(id).attributes[key]
/** public fun <F : Feature<T>> FeatureId<F>.modifyAttributes(modify: Attributes.() -> Attributes): FeatureId<F> {
* Process all features with a given attribute from the one with highest [z] to lowest
*/
public inline fun <A> forEachWithAttribute(
key: Attribute<A>,
block: (id: FeatureId<*>, feature: Feature<T>, attributeValue: A) -> Unit,
) {
featureMap.entries.sortedByDescending { it.value.z }.forEach { (id, feature) ->
feature.attributes[key]?.let {
block(FeatureId<Feature<T>>(id), feature, it)
}
}
}
public fun <F : Feature<T>, V> FeatureId<F>.modifyAttributes(modify: Attributes.() -> Attributes) {
feature(this, get(this).withAttributes(modify))
}
public fun <F : Feature<T>> FeatureId<F>.withAttributes(modify: Attributes.() -> Attributes): FeatureId<F> {
feature(this, get(this).withAttributes(modify)) feature(this, get(this).withAttributes(modify))
return this return this
} }
public fun <F : Feature<T>, V> FeatureId<F>.withAttribute(key: Attribute<V>, value: V?): FeatureId<F> { public fun <F : Feature<T>, V> FeatureId<F>.attribute(key: Attribute<V>, value: V?): FeatureId<F> {
feature(this, get(this).withAttributes { withAttribute(key, value) }) feature(this, get(this).withAttributes { withAttribute(key, value) })
return this return this
} }
@ -120,7 +112,7 @@ public data class FeatureGroup<T : Any>(
DragResult(end, true) DragResult(end, true)
} }
} }
this.withAttribute(DraggableAttribute, handle) this.attribute(DraggableAttribute, handle)
} }
//Apply callback //Apply callback
@ -134,7 +126,7 @@ public data class FeatureGroup<T : Any>(
public fun FeatureId<DraggableFeature<T>>.onDrag( public fun FeatureId<DraggableFeature<T>>.onDrag(
listener: PointerEvent.(from: ViewPoint<T>, to: ViewPoint<T>) -> Unit, listener: PointerEvent.(from: ViewPoint<T>, to: ViewPoint<T>) -> Unit,
) { ) {
withAttribute( attribute(
DragListenerAttribute, DragListenerAttribute,
(getAttribute(this, DragListenerAttribute) ?: emptySet()) + (getAttribute(this, DragListenerAttribute) ?: emptySet()) +
DragListener { event, from, to -> DragListener { event, from, to ->
@ -147,7 +139,7 @@ public data class FeatureGroup<T : Any>(
public fun <F : DomainFeature<T>> FeatureId<F>.onClick( public fun <F : DomainFeature<T>> FeatureId<F>.onClick(
onClick: PointerEvent.(click: ViewPoint<T>) -> Unit, onClick: PointerEvent.(click: ViewPoint<T>) -> Unit,
) { ) {
withAttribute( attribute(
ClickListenerAttribute, ClickListenerAttribute,
(getAttribute(this, ClickListenerAttribute) ?: emptySet()) + (getAttribute(this, ClickListenerAttribute) ?: emptySet()) +
MouseListener { event, point -> event.onClick(point as ViewPoint<T>) } MouseListener { event, point -> event.onClick(point as ViewPoint<T>) }
@ -158,7 +150,7 @@ public data class FeatureGroup<T : Any>(
public fun <F : DomainFeature<T>> FeatureId<F>.onHover( public fun <F : DomainFeature<T>> FeatureId<F>.onHover(
onClick: PointerEvent.(move: ViewPoint<T>) -> Unit, onClick: PointerEvent.(move: ViewPoint<T>) -> Unit,
) { ) {
withAttribute( attribute(
HoverListenerAttribute, HoverListenerAttribute,
(getAttribute(this, HoverListenerAttribute) ?: emptySet()) + (getAttribute(this, HoverListenerAttribute) ?: emptySet()) +
MouseListener { event, point -> event.onClick(point as ViewPoint<T>) } MouseListener { event, point -> event.onClick(point as ViewPoint<T>) }
@ -178,10 +170,10 @@ public data class FeatureGroup<T : Any>(
// } // }
public fun <F : Feature<T>> FeatureId<F>.color(color: Color): FeatureId<F> = public fun <F : Feature<T>> FeatureId<F>.color(color: Color): FeatureId<F> =
withAttribute(ColorAttribute, color) attribute(ColorAttribute, color)
public fun <F : Feature<T>> FeatureId<F>.zoomRange(range: FloatRange): FeatureId<F> = public fun <F : Feature<T>> FeatureId<F>.zoomRange(range: FloatRange): FeatureId<F> =
withAttribute(ZoomRangeAttribute, range) attribute(ZoomRangeAttribute, range)
override fun getBoundingBox(zoom: Float): Rectangle<T>? = with(space) { override fun getBoundingBox(zoom: Float): Rectangle<T>? = with(space) {
featureMap.values.mapNotNull { it.getBoundingBox(zoom) }.wrapRectangles() featureMap.values.mapNotNull { it.getBoundingBox(zoom) }.wrapRectangles()
@ -213,6 +205,47 @@ public data class FeatureGroup<T : Any>(
} }
} }
/**
* Process all features with a given attribute from the one with highest [z] to lowest
*/
public fun <T : Any, A> FeatureGroup<T>.forEachWithAttribute(
key: Attribute<A>,
block: FeatureGroup<T>.(id: String, feature: Feature<T>, attributeValue: A) -> Unit,
) {
visit { id, feature ->
feature.attributes[key]?.let {
block(id, feature, it)
}
}
}
public fun <T : Any, A> FeatureGroup<T>.forEachWithAttributeUntil(
key: Attribute<A>,
block: FeatureGroup<T>.(id: String, feature: Feature<T>, attributeValue: A) -> Boolean,
) {
visitUntil { id, feature ->
feature.attributes[key]?.let {
block(id, feature, it)
} ?: true
}
}
public inline fun <T : Any, reified F : Feature<T>> FeatureGroup<T>.forEachWithType(
crossinline block: FeatureGroup<T>.(FeatureId<F>, feature: F) -> Unit,
) {
visit { id, feature ->
if (feature is F) block(FeatureId(id), feature)
}
}
public inline fun <T : Any, reified F : Feature<T>> FeatureGroup<T>.forEachWithTypeUntil(
crossinline block: FeatureGroup<T>.(FeatureId<F>, feature: F) -> Boolean,
) {
visitUntil { id, feature ->
if (feature is F) block(FeatureId(id), feature) else true
}
}
public fun <T : Any> FeatureGroup<T>.circle( public fun <T : Any> FeatureGroup<T>.circle(
center: T, center: T,
size: Dp = 5.dp, size: Dp = 5.dp,

View File

@ -8,12 +8,17 @@ public fun <T : Any> FeatureGroup<T>.draggableLine(
): FeatureId<LineFeature<T>> { ): FeatureId<LineFeature<T>> {
var lineId: FeatureId<LineFeature<T>>? = null var lineId: FeatureId<LineFeature<T>>? = null
fun drawLine(): FeatureId<LineFeature<T>> = line( fun drawLine(): FeatureId<LineFeature<T>> {
//save attributes before update
val attributes: Attributes? = lineId?.let(::get)?.attributes
val currentId = line(
get(aId).center, get(aId).center,
get(bId).center, get(bId).center,
lineId?.id ?: id lineId?.id ?: id
).also { )
lineId = it if (attributes != null) currentId.modifyAttributes { attributes.withAttribute(ZAttribute, -10f) }
lineId = currentId
return currentId
} }
aId.draggable { _, _ -> aId.draggable { _, _ ->

View File

@ -18,7 +18,7 @@ import kotlin.math.min
*/ */
public fun <T : Any> Modifier.mapControls( public fun <T : Any> Modifier.mapControls(
state: CoordinateViewScope<T>, state: CoordinateViewScope<T>,
features: Collection<Feature<T>>, features: FeatureGroup<T>,
): Modifier = with(state) { ): Modifier = with(state) {
pointerInput(Unit) { pointerInput(Unit) {
fun Offset.toDpOffset() = DpOffset(x.toDp(), y.toDp()) fun Offset.toDpOffset() = DpOffset(x.toDp(), y.toDp())
@ -29,11 +29,10 @@ public fun <T : Any> Modifier.mapControls(
val point = space.ViewPoint(coordinates, zoom) val point = space.ViewPoint(coordinates, zoom)
if (event.type == PointerEventType.Move) { if (event.type == PointerEventType.Move) {
for (feature in features) { features.forEachWithAttribute(HoverListenerAttribute) { _, feature, listeners ->
val listeners = (feature as? DomainFeature)?.attributes?.get(HoverListenerAttribute) if (point in feature as DomainFeature) {
if (listeners != null && point in feature) {
listeners.forEach { it.handle(event, point) } listeners.forEach { it.handle(event, point) }
break return@forEachWithAttribute
} }
} }
} }
@ -42,11 +41,10 @@ public fun <T : Any> Modifier.mapControls(
event, event,
point point
) )
for (feature in features) { features.forEachWithAttribute(ClickListenerAttribute) { _, feature, listeners ->
val listeners = (feature as? DomainFeature)?.attributes?.get(ClickListenerAttribute) if (point in feature as DomainFeature) {
if (listeners != null && point in feature) {
listeners.forEach { it.handle(event, point) } listeners.forEach { it.handle(event, point) }
break return@forEachWithAttribute
} }
} }
} }
@ -96,12 +94,8 @@ public fun <T : Any> Modifier.mapControls(
val dragResult = config.dragHandle?.handle(event, dragStart, dragEnd) val dragResult = config.dragHandle?.handle(event, dragStart, dragEnd)
if (dragResult?.handleNext == false) return@drag if (dragResult?.handleNext == false) return@drag
features.asSequence() features.forEachWithAttributeUntil(DraggableAttribute) { _, _, handler ->
.filterIsInstance<DraggableFeature<T>>() handler.handle(event, dragStart, dragEnd).handleNext
.mapNotNull {
it.attributes[DraggableAttribute]
}.forEach { handler ->
if (!handler.handle(event, dragStart, dragEnd).handleNext) return@drag
} }
} }

View File

@ -73,9 +73,17 @@ object XYCoordinateSpace : CoordinateSpace<XY> {
) )
override fun XY.isInsidePolygon(points: List<XY>): Boolean = points.zipWithNext().count { (left, right) -> override fun XY.isInsidePolygon(points: List<XY>): Boolean = points.zipWithNext().count { (left, right) ->
val dist = right.y - left.y val yRange = if(right.x >= left.x) {
val intersection = left.y * abs((right.x - x) / dist) + left.y..right.y
right.y * abs((x - left.x) / dist) } else {
x in left.x..right.x && intersection >= y right.y..left.y
} % 2 == 0 }
if(y !in yRange) return@count false
val longitudeDelta = right.y - left.y
left.x * abs((right.y - y) / longitudeDelta) +
right.x * abs((y - left.y) / longitudeDelta) >= x
} % 2 == 1
} }