Skip to content

Commit 2f13850

Browse files
committed
some helpful chatgpt suggested improvements to make polygonContains more robust against some edge cases
1 parent da80400 commit 2f13850

File tree

1 file changed

+28
-25
lines changed

1 file changed

+28
-25
lines changed

src/commonMain/kotlin/com/jillesvangurp/geo/GeoGeometry.kt

Lines changed: 28 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -240,55 +240,58 @@ class GeoGeometry {
240240
vararg polygonPoints: PointCoordinates
241241
): Boolean {
242242
fun wrapLongitude(diff: Double): Double = when {
243-
diff > 180 -> diff - 360
244-
diff < -180 -> diff + 360
245-
else -> diff
243+
diff > 180 -> diff - 360
244+
diff < -180 -> diff + 360
245+
else -> diff
246246
}
247+
247248
validate(latitude, longitude, false)
248249
require(polygonPoints.size >= 3) { "a polygon must have at least three points" }
249250

251+
// used for rounding errors
252+
val epsilon = 1e-9
250253

251-
// Shift longitudes so test-point’s lon → 0, deals with antimeridian
254+
// Normalize longitudes around test point
252255
val norm = polygonPoints.map {
253-
doubleArrayOf(
254-
wrapLongitude(it[0] - longitude),
255-
it[1]
256-
)
256+
doubleArrayOf(wrapLongitude(it[0] - longitude), it[1])
257257
}.toTypedArray()
258258

259-
// Quick bbox check around (0, latitude)
260-
val bbox = boundingBox(norm)
261-
if (!bboxContains(bbox, latitude, 0.0)) {
262-
return false
263-
}
264-
if(polygonPoints.firstOrNull() { it.latitude ==latitude && it.longitude==longitude } != null) {
265-
// edge case for vertices
259+
// Early-out bbox check
260+
if (!bboxContains(boundingBox(norm), latitude, 0.0)) return false
261+
262+
// Direct vertex match
263+
if (polygonPoints.any { abs(it.latitude - latitude) < epsilon && abs(it.longitude - longitude) < epsilon }) {
266264
return true
267265
}
268266

269-
// Check if the point lies exactly on any of the polygon's edges
267+
// Edge match
270268
for (i in norm.indices) {
271269
val (x1, y1) = norm[i]
272270
val (x2, y2) = norm[(i + 1) % norm.size]
273-
val x = 0.0
274-
val y = latitude
275-
val onSegment = onSegment(x, y, x1, y1, x2, y2)
276-
if (onSegment) {
271+
if ((x1 != x2 || y1 != y2) && onSegment(0.0, latitude, x1, y1, x2, y2)) {
277272
return true
278273
}
279274
}
280-
// Ray-cast from (0,latitude) eastward
275+
276+
// Ray-cast eastward from (0, latitude)
281277
var hits = 0
282-
val y = latitude
283278
for (i in norm.indices) {
284279
val (x1, y1) = norm[i]
285280
val (x2, y2) = norm[(i + 1) % norm.size]
286-
if ((y1 > y) != (y2 > y)) {
287-
val xInt = x1 + (y - y1) * (x2 - x1) / (y2 - y1)
281+
282+
// Ignore horizontal segments
283+
if (abs(y1 - y2) < epsilon) continue
284+
285+
// Handle crossing the horizontal ray
286+
val lower = if (y1 < y2) y1 else y2
287+
val upper = if (y1 < y2) y2 else y1
288+
if (latitude > lower && latitude <= upper) {
289+
val xInt = x1 + (latitude - y1) * (x2 - x1) / (y2 - y1)
288290
if (xInt > 0) hits++
289291
}
290292
}
291-
return hits and 1 == 1
293+
294+
return hits % 2 == 1
292295
}
293296

294297
/**

0 commit comments

Comments
 (0)