Skip to content

Commit

Permalink
fix heading calculation to not return negative numbers
Browse files Browse the repository at this point in the history
add sanity tests for this that test north, south, east, west to be what they should be
  • Loading branch information
jillesvangurp committed Oct 8, 2021
1 parent 32b84ed commit 33fa8c3
Show file tree
Hide file tree
Showing 3 changed files with 61 additions and 28 deletions.
33 changes: 20 additions & 13 deletions src/commonMain/kotlin/com/jillesvangurp/geo/GeoGeometry.kt
Original file line number Diff line number Diff line change
Expand Up @@ -1088,23 +1088,30 @@ class GeoGeometry {
/**
* Returns the heading from one LatLng to another LatLng as a compass direction.
*
* @see https://www.igismap.com/formula-to-find-bearing-or-heading-angle-between-two-points-latitude-longitude/
* @see https://stackoverflow.com/questions/9457988/bearing-from-one-coordinate-to-another
*
* @return The heading in degrees clockwise from north.
*/
fun headingFromTwoPoints(from: PointCoordinates, to: PointCoordinates): Double {
val fromLat = toRadians(from.latitude)
val fromLng = toRadians(from.longitude)
val toLat = toRadians(to.latitude)
val toLng = toRadians(to.longitude)
val dLng = toLng - fromLng
val headingInRadians = atan2(
sin(dLng) * cos(toLat),
cos(fromLat) * sin(toLat) - sin(fromLat) * cos(toLat) * cos(dLng)
)
return fromRadians(headingInRadians)
}
fun headingFromTwoPoints(from: PointCoordinates, to: PointCoordinates): Double = headingFromTwoPoints(from.latitude,from.longitude,to.latitude,to.longitude)

/**
* Returns the heading from one LatLng to another LatLng as a compass direction.
*
* @see https://stackoverflow.com/questions/9457988/bearing-from-one-coordinate-to-another
*
* @return The heading in degrees clockwise from north.
*/
fun headingFromTwoPoints(lat1: Double, lon1: Double, lat2: Double, lon2: Double): Double {
val latitude1: Double = toRadians(lat1)
val latitude2: Double = toRadians(lat2)
val longDiff: Double = toRadians(lon2 - lon1)
val y: Double = sin(longDiff) * cos(latitude2)
val x: Double =
cos(latitude1) * sin(latitude2) - sin(latitude1) * cos(
latitude2
) * cos(longDiff)
return (fromRadians(atan2(y, x)) + 360) % 360
}
/**
* @param point point
* @return a json representation of the point
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import io.kotest.matchers.shouldBe
import kotlinx.serialization.json.JsonElement
import kotlinx.serialization.json.JsonObject
import kotlin.math.absoluteValue
import kotlin.math.roundToLong
import kotlin.test.Test

// val bigRing = arrayOf(potsDammerPlatz, brandenBurgerGate, naturkundeMuseum, senefelderPlatz, moritzPlatz, potsDammerPlatz)
Expand Down Expand Up @@ -85,7 +86,10 @@ class GeoGeometryTest {
val polygonObject = json.decodeFromString(JsonObject.serializer(), testPolygon)
val polygon = json.decodeFromJsonElement(Geometry.serializer(), polygonObject) as Geometry.Polygon
val serializedPolygonObject = json.encodeToJsonElement(Geometry.serializer(), polygon)
jsonPretty.encodeToString(JsonElement.serializer(), serializedPolygonObject) shouldBe jsonPretty.encodeToString(JsonElement.serializer(), polygonObject)
jsonPretty.encodeToString(JsonElement.serializer(), serializedPolygonObject) shouldBe jsonPretty.encodeToString(
JsonElement.serializer(),
polygonObject
)
}

@Test
Expand All @@ -112,7 +116,40 @@ class GeoGeometryTest {
println(FeatureCollection(features))
}

val testPolygon = """
@Test
fun headingFromTwoPoints() {
GeoGeometry.headingFromTwoPoints(
doubleArrayOf(13.0, 52.0),
doubleArrayOf(14.0, 53.0)
).roundToLong() shouldBe 31
GeoGeometry.headingFromTwoPoints(
doubleArrayOf(14.0, 53.0),
doubleArrayOf(13.0, 52.0)
).roundToLong() shouldBe 212
}

@Test
fun headingFromTwoPointsShouldBeBetweenZeroAnd360() {
GeoGeometry.headingFromTwoPoints(
doubleArrayOf(13.0, 52.0),
doubleArrayOf(13.0, 52.0001)
).roundToLong() shouldBe 0
GeoGeometry.headingFromTwoPoints(
doubleArrayOf(12.999, 52.0),
doubleArrayOf(13.0, 52.0)
).roundToLong() shouldBe 90
GeoGeometry.headingFromTwoPoints(
doubleArrayOf(13.0, 52.0001),
doubleArrayOf(13.0, 52.0)
).roundToLong() shouldBe 180
GeoGeometry.headingFromTwoPoints(
doubleArrayOf(13.0, 52.0),
doubleArrayOf(12.999, 52.0)
).roundToLong() shouldBe 270
}
}

val testPolygon = """
{
"coordinates": [
[
Expand Down Expand Up @@ -158,7 +195,7 @@ class GeoGeometryTest {
}
""".trimIndent()

val badGeo = """
val badGeo = """
{
"coordinates": [
[
Expand Down Expand Up @@ -1278,4 +1315,3 @@ class GeoGeometryTest {
"type": "Polygon"
}
""".trimIndent()
}
Original file line number Diff line number Diff line change
Expand Up @@ -869,15 +869,5 @@ class GeoGeometryJvmTest {
MatcherAssert.assertThat("should contain the point", polygonContains(point, polygon))
}

@Test
fun headingFromTwoPoints() {
GeoGeometry.headingFromTwoPoints(
doubleArrayOf(13.0, 52.0),
doubleArrayOf(14.0, 53.0)
) shouldBeApproximately 30.93571619
GeoGeometry.headingFromTwoPoints(
doubleArrayOf(14.0, 53.0),
doubleArrayOf(13.0, 52.0)
) shouldBeApproximately -148.270892801
}

}

0 comments on commit 33fa8c3

Please sign in to comment.