Skip to content

Commit

Permalink
I messed up and applied the right hand rule wrong.
Browse files Browse the repository at this point in the history
Fixed it and add some documentation for this.

Also improved geometry toString to spit out geojson using its own serializer.
  • Loading branch information
jillesvangurp committed Apr 23, 2021
1 parent 415a94f commit c063755
Show file tree
Hide file tree
Showing 3 changed files with 83 additions and 14 deletions.
55 changes: 51 additions & 4 deletions src/commonMain/kotlin/com/jillesvangurp/geo/GeoGeometry.kt
Original file line number Diff line number Diff line change
Expand Up @@ -1401,21 +1401,30 @@ class GeoGeometry {
}
}

/**
* Checks if the polygon follows the geojson right hand side rule that requires
* the outer linear ring to have the coordinates in counter clockwise order (right hand)
* and the inner ones in clockwise order.
*
* Returns a copy of the polygon with the outer ring and inner rings corrected.
*
* https://tools.ietf.org/html/rfc7946#section-3.1.6
*/
fun PolygonCoordinates.ensureFollowsRightHandSideRule(): PolygonCoordinates {
val holes = holes()
if(this.isValid()) {
return this
} else {
val newPolygonCoordinates = listOf(outer().let {
if (it.isClockWise()) {
if (it.isCounterClockWise()) {
it
} else {
it.changeOrder()
}
})
val inner = holes.map { it ->
it.let { hole ->
if (hole.isCounterClockWise()) {
if (hole.isClockWise()) {
hole
} else {
hole.changeOrder()
Expand All @@ -1425,22 +1434,60 @@ class GeoGeometry {
return (newPolygonCoordinates + inner).toTypedArray()
}
}

/**
* Applies the right hand rule to all of the polygons. Returns a corrected copy.
*
* https://tools.ietf.org/html/rfc7946#section-3.1.6
*/
fun MultiPolygonCoordinates.ensureFollowsRightHandSideRule() = this.map { it.ensureFollowsRightHandSideRule() }.toTypedArray()

/**
* True if start and end are the same.
*/
fun LinearRingCoordinates.hasSameStartAndEnd() = this.first().contentEquals(this.last()) && this.size>1
fun LinearRingCoordinates.isCounterClockWise() = !this.isClockWise()

/**
* Returns the first linear ring, which is the outer ring of the polygon. The remaining rings are holes.
*/
fun PolygonCoordinates.outer() = this[0]

/**
* Return the holes in the polygon.
*/
fun PolygonCoordinates.holes() = if(this.size > 1) this.slice(1 until this.size) else listOf()
fun PolygonCoordinates.isValid() = this[0].isClockWise() && this.holes().map { it.isClockWise() }.none { it } && this.all { it.hasSameStartAndEnd() }

/**
* True if the rings are in the correct order (right hand rule), all rings have the same start and end, and all rings have at least 3 points.
*/
fun PolygonCoordinates.isValid() = this[0].isCounterClockWise() && this.holes().map { it.isCounterClockWise() }.none { it } && this.all { it.hasSameStartAndEnd() } && this.all {it.size>2}

/**
* True if all polygons in the multipolygon are valid.
*/
fun MultiPolygonCoordinates.isValid() = this.all { it.isValid() }

/**
* Returns a copy of the ring with the points in reverse order.
*/
fun LinearRingCoordinates.changeOrder() = this.let {
// make a copy because reverse modifies the array in place.
val copy = it.copyOf()
copy.reverse()
copy
}

/**
* True if the order of the coordinates in the ring is counter clockwise. You can use this to check
* for the [right hand rule]( https://tools.ietf.org/html/rfc7946#section-3.1.6) on polygons
*/
fun LinearRingCoordinates.isCounterClockWise() = !this.isClockWise()

/**
* True if the order of the coordinates in the ring is clockwise.
*
* Based on some suggestions on [stackoverflow](https://stackoverflow.com/a/1165943/1041442)
*/
fun LinearRingCoordinates.isClockWise() : Boolean {
// Sum over the edges, (x2 − x1)(y2 + y1): https://stackoverflow.com/a/1165943/1041442
if(this.size <2) {
Expand Down
21 changes: 17 additions & 4 deletions src/commonMain/kotlin/com/jillesvangurp/geojson/geojson.kt
Original file line number Diff line number Diff line change
Expand Up @@ -106,10 +106,6 @@ operator fun Geometry.GeometryCollection.plus(other: Geometry.GeometryCollection
sealed class Geometry {
abstract val type: GeometryType

override fun toString(): String {
return Json.encodeToString(serializer(), this)
}

@Serializable
data class Point(val coordinates: PointCoordinates?, val bbox: BoundingBox? = null) : Geometry() {
@Required
Expand All @@ -132,6 +128,8 @@ sealed class Geometry {
return result
}

override fun toString(): String = Json.encodeToString(serializer(), this)

companion object {

fun featureOf(lon: Double, lat: Double) = of(lon, lat).asFeature()
Expand Down Expand Up @@ -161,6 +159,8 @@ sealed class Geometry {
result = 31 * result + (bbox?.contentHashCode() ?: 0)
return result
}

override fun toString(): String = Json.encodeToString(serializer(), this)
}


Expand All @@ -186,6 +186,8 @@ sealed class Geometry {
result = 31 * result + (bbox?.contentHashCode() ?: 0)
return result
}

override fun toString(): String = Json.encodeToString(serializer(), this)
}

@Serializable
Expand All @@ -212,6 +214,8 @@ sealed class Geometry {
result = 31 * result + (bbox?.contentHashCode() ?: 0)
return result
}

override fun toString(): String = Json.encodeToString(serializer(), this)
}

@Serializable
Expand All @@ -236,6 +240,8 @@ sealed class Geometry {
result = 31 * result + (bbox?.contentHashCode() ?: 0)
return result
}

override fun toString(): String = Json.encodeToString(serializer(), this)
}

@Serializable
Expand All @@ -260,6 +266,7 @@ sealed class Geometry {
result = 31 * result + (bbox?.contentHashCode() ?: 0)
return result
}
override fun toString(): String = Json.encodeToString(serializer(), this)
}

@Serializable
Expand Down Expand Up @@ -288,6 +295,8 @@ sealed class Geometry {
result = 31 * result + (bbox?.contentHashCode() ?: 0)
return result
}

override fun toString(): String = Json.encodeToString(serializer(), this)
}

companion object : KSerializer<Geometry> {
Expand Down Expand Up @@ -371,6 +380,8 @@ data class Feature(val geometry: Geometry?, val properties: JsonObject? = null,
result = 31 * result + type.hashCode()
return result
}

override fun toString(): String = Json.encodeToString(serializer(), this)
}

@Serializable
Expand Down Expand Up @@ -407,6 +418,8 @@ data class FeatureCollection(val features: List<Feature>, val bbox: BoundingBox?

fun of(vararg features: Feature) = FeatureCollection(features.toList())
}

override fun toString(): String = Json.encodeToString(serializer(),this)
}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import com.jillesvangurp.geo.GeoGeometry.Companion.changeOrder
import com.jillesvangurp.geo.GeoGeometry.Companion.ensureFollowsRightHandSideRule
import com.jillesvangurp.geo.GeoGeometry.Companion.hasSameStartAndEnd
import com.jillesvangurp.geo.GeoGeometry.Companion.isValid
import com.jillesvangurp.geojson.Geometry
import io.kotest.matchers.shouldBe
import kotlin.test.Test

Expand All @@ -23,12 +24,20 @@ class GeoGeometryTest {
val smallRing = arrayOf(rosenthalerPlatz,oranienburgerTor,bergstr16Berlin,rosenthalerPlatz)
bigRing.hasSameStartAndEnd() shouldBe true
smallRing.hasSameStartAndEnd() shouldBe true
arrayOf(bigRing).isValid() shouldBe true
arrayOf(smallRing).isValid() shouldBe true
arrayOf(bigRing, smallRing.changeOrder()).isValid() shouldBe true
arrayOf(bigRing, smallRing).isValid() shouldBe false
arrayOf(bigRing, smallRing).ensureFollowsRightHandSideRule().isValid() shouldBe true
arrayOf(bigRing.changeOrder(), smallRing).isValid() shouldBe false

arrayOf(bigRing).isValid() shouldBe false
arrayOf(bigRing.changeOrder()).isValid() shouldBe true

arrayOf(smallRing).isValid() shouldBe false
arrayOf(smallRing.changeOrder()).isValid() shouldBe true

arrayOf(bigRing, smallRing.changeOrder()).also { println(Geometry.Polygon(it)) }.isValid() shouldBe false
arrayOf(bigRing.changeOrder(), smallRing).also {
println(Geometry.Polygon(it))
}.isValid() shouldBe true
arrayOf(bigRing.changeOrder(), smallRing.changeOrder()).isValid() shouldBe false

arrayOf(bigRing, smallRing.changeOrder()).ensureFollowsRightHandSideRule().isValid() shouldBe true
arrayOf(bigRing.changeOrder(), smallRing).ensureFollowsRightHandSideRule().isValid() shouldBe true
arrayOf(bigRing.changeOrder(), smallRing.changeOrder()).ensureFollowsRightHandSideRule().isValid() shouldBe true
}
Expand Down

0 comments on commit c063755

Please sign in to comment.