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,
config = ViewConfig(
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"))
.withAttribute(ColorAttribute, Color.Blue)
.withAttribute(AlphaAttribute, 0.4f)
.attribute(ColorAttribute, Color.Blue)
.attribute(AlphaAttribute, 0.4f)
image(pointOne, Icons.Filled.Home)
@ -117,24 +119,19 @@ fun App() {
}
}.launchIn(scope)
visit { id, feature ->
if (feature is PolygonFeature) {
id as FeatureId<PolygonFeature<Gmc>>
id.onClick {
println("Click on $id")
forEachWithType<Gmc, PolygonFeature<Gmc>> { id, feature ->
id.onClick {
println("Click on $id")
//draw in top-level scope
with(this@MapView) {
points(
feature.points,
stroke = 4f,
pointMode = PointMode.Polygon,
attributes = Attributes(ZAttribute, 10f),
id = "selected",
attributes = Attributes(ZAttribute, 10f)
).color(Color.Blue)
}
id.onHover {
println("Hover on $id")
points(
feature.points,
id = "selected",
attributes = Attributes(ZAttribute, 10f)
).color(Color.Blue)
).color(Color.Magenta)
}
}
}
@ -142,6 +139,7 @@ fun App() {
}
}
fun main() = application {
Window(onCloseRequest = ::exitApplication) {
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) ->
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
val longitudeRange = if(right.longitude >= left.longitude) {
left.longitude..right.longitude
} else {
right.longitude..left.longitude
}
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()) {
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 fun visit(visitor: FeatureGroup<T>.(id: FeatureId<Feature<T>>, feature: Feature<T>) -> Unit) {
featureMap.forEach { (key, feature) ->
public fun visit(visitor: FeatureGroup<T>.(id: String, feature: Feature<T>) -> Unit) {
featureMap.entries.sortedByDescending { it.value.z }.forEach { (key, feature) ->
if (feature is FeatureGroup<T>) {
feature.visit(visitor)
} 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? =
get(id).attributes[key]
/**
* Process all features with a given attribute from the one with highest [z] to lowest
*/
public inline fun <A> forEachWithAttribute(
key: Attribute<A>,
block: (id: FeatureId<*>, feature: Feature<T>, attributeValue: A) -> Unit,
) {
featureMap.entries.sortedByDescending { it.value.z }.forEach { (id, feature) ->
feature.attributes[key]?.let {
block(FeatureId<Feature<T>>(id), feature, it)
}
}
}
public fun <F : Feature<T>, V> FeatureId<F>.modifyAttributes(modify: Attributes.() -> Attributes) {
feature(this, get(this).withAttributes(modify))
}
public fun <F : Feature<T>> FeatureId<F>.withAttributes(modify: Attributes.() -> Attributes): FeatureId<F> {
public fun <F : Feature<T>> FeatureId<F>.modifyAttributes(modify: Attributes.() -> Attributes): FeatureId<F> {
feature(this, get(this).withAttributes(modify))
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) })
return this
}
@ -120,7 +112,7 @@ public data class FeatureGroup<T : Any>(
DragResult(end, true)
}
}
this.withAttribute(DraggableAttribute, handle)
this.attribute(DraggableAttribute, handle)
}
//Apply callback
@ -134,7 +126,7 @@ public data class FeatureGroup<T : Any>(
public fun FeatureId<DraggableFeature<T>>.onDrag(
listener: PointerEvent.(from: ViewPoint<T>, to: ViewPoint<T>) -> Unit,
) {
withAttribute(
attribute(
DragListenerAttribute,
(getAttribute(this, DragListenerAttribute) ?: emptySet()) +
DragListener { event, from, to ->
@ -147,7 +139,7 @@ public data class FeatureGroup<T : Any>(
public fun <F : DomainFeature<T>> FeatureId<F>.onClick(
onClick: PointerEvent.(click: ViewPoint<T>) -> Unit,
) {
withAttribute(
attribute(
ClickListenerAttribute,
(getAttribute(this, ClickListenerAttribute) ?: emptySet()) +
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(
onClick: PointerEvent.(move: ViewPoint<T>) -> Unit,
) {
withAttribute(
attribute(
HoverListenerAttribute,
(getAttribute(this, HoverListenerAttribute) ?: emptySet()) +
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> =
withAttribute(ColorAttribute, color)
attribute(ColorAttribute, color)
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) {
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(
center: T,
size: Dp = 5.dp,

View File

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

View File

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

View File

@ -73,9 +73,17 @@ object XYCoordinateSpace : CoordinateSpace<XY> {
)
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
val yRange = if(right.x >= left.x) {
left.y..right.y
} else {
right.y..left.y
}
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
}