diff --git a/src/commonMain/kotlin/com/jillesvangurp/geo/GeoGeometry.kt b/src/commonMain/kotlin/com/jillesvangurp/geo/GeoGeometry.kt index 2198134..5b77afb 100644 --- a/src/commonMain/kotlin/com/jillesvangurp/geo/GeoGeometry.kt +++ b/src/commonMain/kotlin/com/jillesvangurp/geo/GeoGeometry.kt @@ -1401,13 +1401,22 @@ 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() @@ -1415,7 +1424,7 @@ class GeoGeometry { }) val inner = holes.map { it -> it.let { hole -> - if (hole.isCounterClockWise()) { + if (hole.isClockWise()) { hole } else { hole.changeOrder() @@ -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) { diff --git a/src/commonMain/kotlin/com/jillesvangurp/geojson/geojson.kt b/src/commonMain/kotlin/com/jillesvangurp/geojson/geojson.kt index c67de50..6fef087 100644 --- a/src/commonMain/kotlin/com/jillesvangurp/geojson/geojson.kt +++ b/src/commonMain/kotlin/com/jillesvangurp/geojson/geojson.kt @@ -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 @@ -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() @@ -161,6 +159,8 @@ sealed class Geometry { result = 31 * result + (bbox?.contentHashCode() ?: 0) return result } + + override fun toString(): String = Json.encodeToString(serializer(), this) } @@ -186,6 +186,8 @@ sealed class Geometry { result = 31 * result + (bbox?.contentHashCode() ?: 0) return result } + + override fun toString(): String = Json.encodeToString(serializer(), this) } @Serializable @@ -212,6 +214,8 @@ sealed class Geometry { result = 31 * result + (bbox?.contentHashCode() ?: 0) return result } + + override fun toString(): String = Json.encodeToString(serializer(), this) } @Serializable @@ -236,6 +240,8 @@ sealed class Geometry { result = 31 * result + (bbox?.contentHashCode() ?: 0) return result } + + override fun toString(): String = Json.encodeToString(serializer(), this) } @Serializable @@ -260,6 +266,7 @@ sealed class Geometry { result = 31 * result + (bbox?.contentHashCode() ?: 0) return result } + override fun toString(): String = Json.encodeToString(serializer(), this) } @Serializable @@ -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 { @@ -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 @@ -407,6 +418,8 @@ data class FeatureCollection(val features: List, val bbox: BoundingBox? fun of(vararg features: Feature) = FeatureCollection(features.toList()) } + + override fun toString(): String = Json.encodeToString(serializer(),this) } /** diff --git a/src/commonTest/kotlin/com/jillesvangurp/geogeometry/GeoGeometryTest.kt b/src/commonTest/kotlin/com/jillesvangurp/geogeometry/GeoGeometryTest.kt index 8bc233b..4c83fab 100644 --- a/src/commonTest/kotlin/com/jillesvangurp/geogeometry/GeoGeometryTest.kt +++ b/src/commonTest/kotlin/com/jillesvangurp/geogeometry/GeoGeometryTest.kt @@ -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 @@ -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 }