Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
Show all changes
43 commits
Select commit Hold shift + click to select a range
1c5868b
Add intersects util
seadowg Nov 10, 2025
fd4a6a1
Add intersects function handler
seadowg Nov 11, 2025
ab235b8
Use prototypes to get already evaluated args
seadowg Nov 11, 2025
4fb7739
Add test for non geotrace/shape strings
seadowg Nov 17, 2025
3419811
Add test cases for repeated segments and closed traced intersection
seadowg Nov 17, 2025
9641e08
Add intersects function handler to Collect
seadowg Nov 17, 2025
b2f3cc7
Move parseGeometry and parseGeometryPoint tests to geo
seadowg Nov 17, 2025
a11a20d
Convert GeoUtils to Kotlin
seadowg Nov 17, 2025
7508fe8
Move parseGeometryPoint to GeoUtils
seadowg Nov 17, 2025
17e52e8
Fix import in test
seadowg Nov 18, 2025
83a60e2
Throw exception for non-geotrace inputs
seadowg Nov 18, 2025
281fd94
Improve converted code
seadowg Nov 18, 2025
d21470c
Use zipWithNext to simplify creating segments
seadowg Nov 18, 2025
7256252
Add test to check arg length is enforced
seadowg Nov 18, 2025
77bec29
Switch test to Hamcrest
seadowg Nov 18, 2025
2a3cd02
Remove unused imports
seadowg Nov 19, 2025
da8e9a7
Add another test case to make sure we check intersections from both s…
seadowg Nov 19, 2025
d853d05
Add failing tests for non crossing intersections
seadowg Nov 20, 2025
292b7be
Rename method
seadowg Nov 20, 2025
e074729
Fix origin of one segment touching another case
seadowg Nov 20, 2025
7055bdc
Correct test
seadowg Nov 20, 2025
527ae8e
Fix line moving back on itself case
seadowg Nov 20, 2025
e7a8662
Add test for shape that closes outside the origin
seadowg Nov 20, 2025
0800a0e
Simplify non-origin vertex closing
seadowg Nov 21, 2025
602fee7
Use bounding box check based on orientation to solve right angled tri…
seadowg Nov 21, 2025
ad81da9
Fix special case with two segmnt intersection
seadowg Nov 21, 2025
69d4c8a
Pull out specific 2D geometry code
seadowg Nov 21, 2025
8860a4a
Make sure checking instersection between segments is exhaustive
seadowg Nov 21, 2025
e7b2a71
Remove unneeded return
seadowg Nov 25, 2025
30dbba2
Correct bounding box check direction
seadowg Nov 25, 2025
8822207
Add additional (failing) test for 2 segment self intersection
seadowg Nov 26, 2025
8e0e0c7
Account for case where first endpoint intersects with second segment …
seadowg Nov 26, 2025
970b508
Filter out zero length segments in traces
seadowg Nov 26, 2025
1d1a320
Remove unneeded return
seadowg Nov 26, 2025
bba4264
Fix reversed line with 3 points case
seadowg Nov 27, 2025
2fb19f8
Add basic metamorphic test for intersects
seadowg Nov 27, 2025
9f596bc
Improve adding intersecting segment using interpolation
seadowg Nov 27, 2025
29936a8
Add epsilon to colinearity check to prevent precision errors
seadowg Nov 27, 2025
08c5997
Fix accidental map
seadowg Nov 27, 2025
c6aa7c0
Add docs for interpolate
seadowg Nov 27, 2025
6a0e2e0
Increase the number of possible random intersection points in test
seadowg Nov 28, 2025
ac13533
Add tests for LineSegment#interpolate
seadowg Nov 28, 2025
d88de71
Add quickCheck helper
seadowg Nov 28, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Prev Previous commit
Next Next commit
Pull out specific 2D geometry code
  • Loading branch information
seadowg committed Nov 21, 2025
commit 69d4c8a6199c27fa7338167ebf8ded301e03c107
120 changes: 0 additions & 120 deletions geo/src/main/java/org/odk/collect/geo/geopoly/GeoPolyUtils.kt
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,6 @@ package org.odk.collect.geo.geopoly

import org.odk.collect.geo.GeoUtils.parseGeometryPoint
import org.odk.collect.maps.MapPoint
import kotlin.math.max
import kotlin.math.min

object GeoPolyUtils {

Expand All @@ -21,122 +19,4 @@ object GeoPolyUtils {

return points
}

/**
* Returns `true` if any segment of the trace intersects with any other and `false` otherwise.
*/
fun intersects(trace: List<MapPoint>): Boolean {
return if (trace.size >= 3) {
val isClosed = trace.first() == trace.last()
val segments = trace.zipWithNext()

return if (segments.size == 2) {
val orientation = orientation(segments[1].second, segments[0].first, segments[0].second)
return orientation == Orientation.Collinear && within(segments[1].second, segments[0])
} else {
segments.filterIndexed { line1Index, line1 ->
segments.filterIndexed { line2Index, line2 ->
if (isClosed && line1Index == 0 && line2Index == segments.size - 1) {
false
} else if (line2Index >= line1Index + 2) {
intersects(line1, line2)
} else {
false
}
}.isNotEmpty()
}.isNotEmpty()
}
} else {
false
}
}

/**
* Check if a point is within the bounding box of a line
*/
fun within(
point: MapPoint,
line: Pair<MapPoint, MapPoint>
): Boolean {
val lineLatMin = min(line.first.latitude, line.second.latitude)
val lineLatMax = max(line.first.latitude, line.second.latitude)
val lineLongMin = min(line.first.longitude, line.second.longitude)
val lineLongMax = max(line.first.longitude, line.second.longitude)
val latRange = lineLatMin..lineLatMax
val longRange = lineLongMin..lineLongMax

return point.latitude in latRange && point.longitude in longRange
}

/**
* Work out whether two line segments intersect by calculating if the endpoints of one segment
* are on opposite sides (or touching of the other segment **and** vice versa. This is
* determined by finding the orientation of endpoints relative to the other line.
*/
private fun intersects(
aB: Pair<MapPoint, MapPoint>,
cD: Pair<MapPoint, MapPoint>
): Boolean {
val (a, b) = aB
val (c, d) = cD

val orientationA = orientation(a, c, d)
val orientationB = orientation(b, c, d)
val orientationC = orientation(a, b, c)
val orientationD = orientation(a, b, d)

return if (orientationA.isOpposing(orientationB) && orientationC.isOpposing(orientationD)) {
true
} else if (orientationA == Orientation.Collinear && within(a, cD)) {
true
} else {
false
}
}

/**
* Calculate the "orientation" (or "direction") of three points using the cross product of the
* vectors of the pairs of points (see
* [here](https://en.wikipedia.org/wiki/Cross_product#Computational_geometry)). This can
* either be clockwise, anticlockwise or collinear (the three points form a straight line).
*
*/
private fun orientation(a: MapPoint, b: MapPoint, c: MapPoint): Orientation {
val ab = Pair(b.latitude - a.latitude, b.longitude - a.longitude)
val ac = Pair(c.latitude - a.latitude, c.longitude - a.longitude)
val crossProduct = crossProduct(ab, ac)

return if (crossProduct > 0) {
Orientation.AntiClockwise
} else if (crossProduct < 0) {
Orientation.Clockwise
} else {
Orientation.Collinear
}
}

/**
* [https://en.wikipedia.org/wiki/Cross_product](https://en.wikipedia.org/wiki/Cross_product)
*/
private fun crossProduct(x: Pair<Double, Double>, y: Pair<Double, Double>): Double {
return (x.first * y.second) - (y.first * x.second)
}

private enum class Orientation {
Collinear,
Clockwise,
AntiClockwise;

fun isOpposing(other: Orientation): Boolean {
return if (this == Collinear) {
false
} else if (this == Clockwise && other == AntiClockwise) {
true
} else if (this == AntiClockwise && other == Clockwise) {
true
} else {
false
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,10 @@ package org.odk.collect.geo.javarosa
import org.javarosa.core.model.condition.EvaluationContext
import org.javarosa.core.model.condition.IFunctionHandler
import org.javarosa.xpath.XPathTypeMismatchException
import org.odk.collect.geo.geopoly.GeoPolyUtils.intersects
import org.odk.collect.geo.geopoly.GeoPolyUtils.parseGeometry
import org.odk.collect.maps.toPoint
import org.odk.collect.shared.geometry.Trace
import org.odk.collect.shared.geometry.intersects

class IntersectsFunctionHandler : IFunctionHandler {
override fun getName(): String {
Expand All @@ -28,8 +30,9 @@ class IntersectsFunctionHandler : IFunctionHandler {
ec: EvaluationContext
): Any {
try {
val trace = parseGeometry(args[0] as String, strict = true)
return intersects(trace)
val mapPoints = parseGeometry(args[0] as String, strict = true)
val trace = Trace(mapPoints.map { it.toPoint() })
return trace.intersects()
} catch (_: IllegalArgumentException) {
throw XPathTypeMismatchException()
}
Expand Down
126 changes: 0 additions & 126 deletions geo/src/test/java/org/odk/collect/geo/geopoly/GeoPolyUtilsTest.kt
Original file line number Diff line number Diff line change
Expand Up @@ -8,132 +8,6 @@ import org.odk.collect.maps.MapPoint

class GeoPolyUtilsTest {

@Test
fun `#intersects returns false for an empty list`() {
assertThat(GeoPolyUtils.intersects(emptyList()), equalTo(false))
}

@Test
fun `#intersects returns false when there is only one point`() {
val trace = listOf(MapPoint(0.0, 0.0))
assertThat(GeoPolyUtils.intersects(trace), equalTo(false))
}

@Test
fun `#intersects returns false when there is only one segment`() {
val trace = listOf(MapPoint(0.0, 0.0), MapPoint(1.0, 0.0))
assertThat(GeoPolyUtils.intersects(trace), equalTo(false))
}

@Test
fun `#intersects returns false when no segment intersects with another`() {
val trace = listOf(
MapPoint(0.0, 0.0),
MapPoint(1.0, 1.0),
MapPoint(2.0, 0.0)
)

assertThat(GeoPolyUtils.intersects(trace), equalTo(false))
}

@Test
fun `#intersects returns false when no segment intersects with another in a closed trace`() {
val trace = listOf(
MapPoint(0.0, 0.0),
MapPoint(1.0, 1.0),
MapPoint(2.0, 0.0),
MapPoint(0.0, 0.0)
)

assertThat(GeoPolyUtils.intersects(trace), equalTo(false))
}

@Test
fun `#intersects returns true when a segment intersects with another`() {
val trace = listOf(
MapPoint(1.0, 1.0),
MapPoint(1.0, 3.0),
MapPoint(2.0, 3.0),
MapPoint(2.0, 2.0),
MapPoint(0.0, 2.0)
)

assertThat(GeoPolyUtils.intersects(trace), equalTo(true))
}

@Test
fun `#intersects returns true when a segment intersects with another in a closed trace`() {
val trace = listOf(
MapPoint(1.0, 1.0),
MapPoint(1.0, 3.0),
MapPoint(2.0, 3.0),
MapPoint(2.0, 2.0),
MapPoint(0.0, 2.0),
MapPoint(1.0, 1.0)
)

assertThat(GeoPolyUtils.intersects(trace), equalTo(true))
}

@Test
fun `#intersects returns false when a segment's end points are both on different sides of another, but the segments do not intersect`() {
val trace = listOf(
MapPoint(1.0, 1.0),
MapPoint(1.0, 2.0),
MapPoint(3.0, 3.0),
MapPoint(0.0, 3.0)
)

assertThat(GeoPolyUtils.intersects(trace), equalTo(false))
}

@Test
fun `#intersects returns true when just an endpoint touches another segment`() {
val trace = listOf(
MapPoint(0.0, 0.0),
MapPoint(1.0, 1.0),
MapPoint(2.0, 0.0),
MapPoint(-1.0, 0.0)
)

assertThat(GeoPolyUtils.intersects(trace), equalTo(true))
}

@Test
fun `#intersects returns true when a segment is collinear and within another`() {
val trace = listOf(
MapPoint(0.0, 0.0),
MapPoint(0.0, 1.0),
MapPoint(0.0, 0.5)
)

assertThat(GeoPolyUtils.intersects(trace), equalTo(true))
}

@Test
fun `#intersects returns true when the trace closes on a non-origin vertex`() {
val trace = listOf(
MapPoint(0.0, 0.0),
MapPoint(0.0, 1.0), // Close back on this point
MapPoint(0.0, 2.0),
MapPoint(1.0, 2.0),
MapPoint(0.0, 1.0)
)

assertThat(GeoPolyUtils.intersects(trace), equalTo(true))
}

@Test
fun `#intersects returns false for right angled triangle`() {
val trace = listOf(
MapPoint(0.0, 0.0),
MapPoint(10.0, 10.0),
MapPoint(0.0, 10.0)
)

assertThat(GeoPolyUtils.intersects(trace), equalTo(false))
}

@Test
fun parseGeometryTest() {
assertThat(parseGeometry("1.0 2.0 3 4"), equalTo(listOf(MapPoint(1.0, 2.0, 3.0, 4.0))))
Expand Down
5 changes: 5 additions & 0 deletions maps/src/main/java/org/odk/collect/maps/MapPoint.kt
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ package org.odk.collect.maps

import android.os.Parcelable
import kotlinx.parcelize.Parcelize
import org.odk.collect.shared.geometry.Point

@Parcelize
data class MapPoint @JvmOverloads constructor(
Expand All @@ -23,3 +24,7 @@ data class MapPoint @JvmOverloads constructor(
@JvmField val altitude: Double = 0.0,
@JvmField val accuracy: Double = 0.0
) : Parcelable

fun MapPoint.toPoint(): Point {
return Point(latitude, longitude)
}
Loading