From ea8c5571bb7468c43fc00778292f18b6d339720b Mon Sep 17 00:00:00 2001 From: Alexander Nozik Date: Wed, 4 Jan 2023 22:43:51 +0300 Subject: [PATCH] Proper polygon contains check --- demo/maps/src/jvmMain/kotlin/Main.kt | 34 ++++--- .../sciprog/maps/compose/WebMercatorSpace.kt | 18 +++- .../center/sciprog/maps/compose/MapViewJvm.kt | 2 +- .../sciprog/maps/features/FeatureGroup.kt | 91 +++++++++++++------ .../maps/features/compositeFeatures.kt | 17 ++-- .../sciprog/maps/compose/mapControls.kt | 26 ++---- .../sciprog/maps/scheme/XYCoordinateSpace.kt | 18 +++- 7 files changed, 126 insertions(+), 80 deletions(-) diff --git a/demo/maps/src/jvmMain/kotlin/Main.kt b/demo/maps/src/jvmMain/kotlin/Main.kt index b339439..d34bc9b 100644 --- a/demo/maps/src/jvmMain/kotlin/Main.kt +++ b/demo/maps/src/jvmMain/kotlin/Main.kt @@ -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> - id.onClick { - println("Click on $id") + + forEachWithType> { 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() 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 66cf2a5..639fc84 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 @@ -85,11 +85,19 @@ public object WebMercatorSpace : CoordinateSpace { } 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 + 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 } /** diff --git a/maps-kt-compose/src/jvmMain/kotlin/center/sciprog/maps/compose/MapViewJvm.kt b/maps-kt-compose/src/jvmMain/kotlin/center/sciprog/maps/compose/MapViewJvm.kt index c9231f6..5704dd9 100644 --- a/maps-kt-compose/src/jvmMain/kotlin/center/sciprog/maps/compose/MapViewJvm.kt +++ b/maps-kt-compose/src/jvmMain/kotlin/center/sciprog/maps/compose/MapViewJvm.kt @@ -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" } 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 index 97acafa..c6bf469 100644 --- 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 @@ -49,12 +49,22 @@ public data class FeatureGroup( public val features: Collection> get() = featureMap.values.sortedByDescending { it.z } - public fun visit(visitor: FeatureGroup.(id: FeatureId>, feature: Feature) -> Unit) { - featureMap.forEach { (key, feature) -> + public fun visit(visitor: FeatureGroup.(id: String, feature: Feature) -> Unit) { + featureMap.entries.sortedByDescending { it.value.z }.forEach { (key, feature) -> if (feature is FeatureGroup) { feature.visit(visitor) } else { - visitor(this, FeatureId(key), feature) + visitor(this, key, feature) + } + } + } + + public fun visitUntil(visitor: FeatureGroup.(id: String, feature: Feature) -> Boolean) { + featureMap.entries.sortedByDescending { it.value.z }.forEach { (key, feature) -> + if (feature is FeatureGroup) { + feature.visitUntil(visitor) + } else { + if (!visitor(this, key, feature)) return@forEach } } } @@ -63,30 +73,12 @@ public data class FeatureGroup( public fun getAttribute(id: FeatureId>, key: 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: 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) - } - } - } - - public fun , V> FeatureId.modifyAttributes(modify: Attributes.() -> Attributes) { - feature(this, get(this).withAttributes(modify)) - } - - public fun > FeatureId.withAttributes(modify: Attributes.() -> Attributes): FeatureId { + public fun > FeatureId.modifyAttributes(modify: Attributes.() -> Attributes): FeatureId { feature(this, get(this).withAttributes(modify)) return this } - public fun , V> FeatureId.withAttribute(key: Attribute, value: V?): FeatureId { + public fun , V> FeatureId.attribute(key: Attribute, value: V?): FeatureId { feature(this, get(this).withAttributes { withAttribute(key, value) }) return this } @@ -120,7 +112,7 @@ public data class FeatureGroup( DragResult(end, true) } } - this.withAttribute(DraggableAttribute, handle) + this.attribute(DraggableAttribute, handle) } //Apply callback @@ -134,7 +126,7 @@ public data class FeatureGroup( public fun FeatureId>.onDrag( listener: PointerEvent.(from: ViewPoint, to: ViewPoint) -> Unit, ) { - withAttribute( + attribute( DragListenerAttribute, (getAttribute(this, DragListenerAttribute) ?: emptySet()) + DragListener { event, from, to -> @@ -147,7 +139,7 @@ public data class FeatureGroup( public fun > FeatureId.onClick( onClick: PointerEvent.(click: ViewPoint) -> Unit, ) { - withAttribute( + attribute( ClickListenerAttribute, (getAttribute(this, ClickListenerAttribute) ?: emptySet()) + MouseListener { event, point -> event.onClick(point as ViewPoint) } @@ -158,7 +150,7 @@ public data class FeatureGroup( public fun > FeatureId.onHover( onClick: PointerEvent.(move: ViewPoint) -> Unit, ) { - withAttribute( + attribute( HoverListenerAttribute, (getAttribute(this, HoverListenerAttribute) ?: emptySet()) + MouseListener { event, point -> event.onClick(point as ViewPoint) } @@ -178,10 +170,10 @@ public data class FeatureGroup( // } public fun > FeatureId.color(color: Color): FeatureId = - withAttribute(ColorAttribute, color) + attribute(ColorAttribute, color) public fun > FeatureId.zoomRange(range: FloatRange): FeatureId = - withAttribute(ZoomRangeAttribute, range) + attribute(ZoomRangeAttribute, range) override fun getBoundingBox(zoom: Float): Rectangle? = with(space) { featureMap.values.mapNotNull { it.getBoundingBox(zoom) }.wrapRectangles() @@ -213,6 +205,47 @@ public data class FeatureGroup( } } +/** + * Process all features with a given attribute from the one with highest [z] to lowest + */ +public fun FeatureGroup.forEachWithAttribute( + key: Attribute, + block: FeatureGroup.(id: String, feature: Feature, attributeValue: A) -> Unit, +) { + visit { id, feature -> + feature.attributes[key]?.let { + block(id, feature, it) + } + } +} + +public fun FeatureGroup.forEachWithAttributeUntil( + key: Attribute, + block: FeatureGroup.(id: String, feature: Feature, attributeValue: A) -> Boolean, +) { + visitUntil { id, feature -> + feature.attributes[key]?.let { + block(id, feature, it) + } ?: true + } +} + +public inline fun > FeatureGroup.forEachWithType( + crossinline block: FeatureGroup.(FeatureId, feature: F) -> Unit, +) { + visit { id, feature -> + if (feature is F) block(FeatureId(id), feature) + } +} + +public inline fun > FeatureGroup.forEachWithTypeUntil( + crossinline block: FeatureGroup.(FeatureId, feature: F) -> Boolean, +) { + visitUntil { id, feature -> + if (feature is F) block(FeatureId(id), feature) else true + } +} + public fun FeatureGroup.circle( center: T, size: Dp = 5.dp, diff --git a/maps-kt-features/src/commonMain/kotlin/center/sciprog/maps/features/compositeFeatures.kt b/maps-kt-features/src/commonMain/kotlin/center/sciprog/maps/features/compositeFeatures.kt index ab78dc7..c39f879 100644 --- a/maps-kt-features/src/commonMain/kotlin/center/sciprog/maps/features/compositeFeatures.kt +++ b/maps-kt-features/src/commonMain/kotlin/center/sciprog/maps/features/compositeFeatures.kt @@ -8,12 +8,17 @@ public fun FeatureGroup.draggableLine( ): FeatureId> { var lineId: FeatureId>? = null - fun drawLine(): FeatureId> = line( - get(aId).center, - get(bId).center, - lineId?.id ?: id - ).also { - lineId = it + fun drawLine(): FeatureId> { + //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 { _, _ -> 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 c525360..30289c2 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 @@ -18,7 +18,7 @@ import kotlin.math.min */ public fun Modifier.mapControls( state: CoordinateViewScope, - features: Collection>, + features: FeatureGroup, ): Modifier = with(state) { pointerInput(Unit) { fun Offset.toDpOffset() = DpOffset(x.toDp(), y.toDp()) @@ -29,11 +29,10 @@ public fun 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 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 Modifier.mapControls( val dragResult = config.dragHandle?.handle(event, dragStart, dragEnd) if (dragResult?.handleNext == false) return@drag - features.asSequence() - .filterIsInstance>() - .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) { 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 32bf0fa..74e1c6f 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 @@ -73,9 +73,17 @@ object XYCoordinateSpace : CoordinateSpace { ) 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 + 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 } \ No newline at end of file