Skip to content

Commit

Permalink
add mgrs test for different precisions
Browse files Browse the repository at this point in the history
normalize pointcoordinates (there were some issues with longitudes on the dateline)
  • Loading branch information
jillesvangurp committed Oct 23, 2024
1 parent 7d74ea6 commit 1ccb29a
Show file tree
Hide file tree
Showing 4 changed files with 55 additions and 27 deletions.
22 changes: 7 additions & 15 deletions src/commonMain/kotlin/com/jillesvangurp/geo/mgrs.kt
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
package com.jillesvangurp.geo

import com.jillesvangurp.geojson.PointCoordinates
import kotlin.math.abs
import kotlin.math.floor

/*
Expand Down Expand Up @@ -30,7 +29,7 @@ import kotlin.math.floor
* MGRS precision for the easting and northing.
* [MgrsCoordinate] stores everything in meter precision but can format with any of these precisions.
*/
enum class MgrsPrecision(val divisor: Int, val digits: Int) {
enum class MgrsPrecision(val meters: Int, val digits: Int) {
TEN_KM(10000,1),
ONE_KM(1000,2),
HUNDRED_M(100,3),
Expand Down Expand Up @@ -64,17 +63,17 @@ data class MgrsCoordinate(
* USNG is the human-readable version of MGRS which includes spaces.
*/
fun usng(precision: MgrsPrecision = MgrsPrecision.ONE_M): String {
val eastingStr = (easting / precision.divisor).toString().padStart(precision.digits, '0')
val northingStr = (northing / precision.divisor).toString().padStart(precision.digits, '0')
val eastingStr = (easting / precision.meters).toString().padStart(precision.digits, '0')
val northingStr = (northing / precision.meters).toString().padStart(precision.digits, '0')
return "$longitudeZone$latitudeZoneLetter $firstLetter$secondLetter $eastingStr $northingStr"
}

/**
* MGRS is the compact format without spaces.
*/
fun mgrs(precision: MgrsPrecision = MgrsPrecision.ONE_M): String {
val eastingStr = (easting / precision.divisor).toString().padStart(precision.digits, '0')
val northingStr = (northing / precision.divisor).toString().padStart(precision.digits, '0')
val eastingStr = (easting / precision.meters).toString().padStart(precision.digits, '0')
val northingStr = (northing / precision.meters).toString().padStart(precision.digits, '0')
return "$longitudeZone$latitudeZoneLetter$firstLetter$secondLetter$eastingStr$northingStr"
}
}
Expand Down Expand Up @@ -248,20 +247,13 @@ fun String.parseMgrs(): MgrsCoordinate? {
} else {
val mid = numbers.length / 2
val precision = MgrsPrecision.entries[mid - 1]
val easting = numbers.substring(0, mid).toInt() * precision.divisor
val northing = numbers.substring(mid).toInt() * precision.divisor
val easting = numbers.substring(0, mid).toInt() * precision.meters
val northing = numbers.substring(mid).toInt() * precision.meters
MgrsCoordinate(longitudeZone, latitudeZoneLetter, firstLetter, secondLetter, easting, northing)
}
}
}

private fun roundMGRS(value: Double): Long {
val ival = floor(value).toLong()
val fraction = value - ival
// double fraction = modf (value, &ivalue);
return if (fraction > 0.5 || fraction == 0.5 && ival % 2L == 1L) ival + 1 else ival
}

data class UpsConstant(
val latitudeZoneLetter: Char,
val chars: List<Char>,
Expand Down
29 changes: 19 additions & 10 deletions src/commonMain/kotlin/com/jillesvangurp/geo/utm.kt
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import com.jillesvangurp.geo.GeoGeometry.Companion.toRadians
import com.jillesvangurp.geojson.PointCoordinates
import com.jillesvangurp.geojson.latitude
import com.jillesvangurp.geojson.longitude
import com.jillesvangurp.geojson.normalize
import kotlin.math.*

/**
Expand Down Expand Up @@ -110,7 +111,7 @@ data class UtmCoordinate(
}
}

val UtmCoordinate.isUps get() = latitudeZoneLetter in listOf('A','B', 'Y', 'Z')
val UtmCoordinate.isUps get() = latitudeZoneLetter in listOf('A', 'B', 'Y', 'Z')
val UtmCoordinate.isUtm get() = !isUps

val UtmCoordinate.isSouth get() = latitudeZoneLetter < 'N'
Expand Down Expand Up @@ -218,7 +219,7 @@ private fun getLongitudeZone(latLong: PointCoordinates): Int {
// UPS longitude zones
val longitude = latLong.longitude
return if (isNorthPolar(latLong) || isSouthPolar(latLong)) {
if (longitude < 0.0) {
if (longitude < 0.0) {
30
} else {
31
Expand All @@ -227,30 +228,35 @@ private fun getLongitudeZone(latLong: PointCoordinates): Int {
val latitudeZone: Char = getLatitudeZoneLetter(latLong)
when {
latitudeZone == 'X' && longitude > 0.0 && longitude < 42.0 -> {
// X latitude exceptions
// X latitude exceptions
when {
longitude < 9.0 -> {
31
}

longitude < 21.0 -> {
33
}

longitude < 33.0 -> {
35
}

else -> {
37
}
}
}

latitudeZone == 'V' && longitude > 0.0 && longitude < 12.0 -> {
// V latitude exceptions
// V latitude exceptions
if (longitude < 3.0) {
31
} else {
32
}
}

else -> {
((longitude + 180) / 6).toInt() + 1
}
Expand Down Expand Up @@ -293,16 +299,16 @@ private fun getCentralMeridian(longitudeZone: Int, latitudeZone: Char): Double {
/**
* Converts to UTM or UPS and selects the coordinate system based on the latitude.
*/
fun PointCoordinates.toUtmOrUps() : UtmCoordinate {
fun PointCoordinates.toUtmOrUps(): UtmCoordinate {
return if (latitude < UTM_SOUTHERN_LIMIT || latitude > UTM_NORTHERN_LIMIT) {
toUpsCoordinate()
} else {
toUtmCoordinate()
}
}

fun UtmCoordinate.toPointCoordinates() : PointCoordinates {
return if(isUps) upsToPointCoordinates() else utmToPointCoordinates()
fun UtmCoordinate.toPointCoordinates(): PointCoordinates {
return if (isUps) upsToPointCoordinates() else utmToPointCoordinates()
}

fun PointCoordinates.toUtmCoordinate(): UtmCoordinate {
Expand Down Expand Up @@ -472,7 +478,10 @@ fun UtmCoordinate.utmToPointCoordinates(): PointCoordinates {
* (61.0 + 662.0 * tan2Phi + 1320.0 * tan4Phi + 720.0 * tan6Phi))
val latitude = phi - dE2 * t10 + dE4 * t11 - dE6 * t12 + dE8 * t13
val longitude = (lambda0 + dE * t14 - dE3 * t15 + dE5 * t16 - dE7 * t17)
return doubleArrayOf(fromRadians(longitude), fromRadians(latitude))
return doubleArrayOf(
fromRadians(longitude),
fromRadians(latitude)
).normalize()
}

/**
Expand All @@ -487,7 +496,7 @@ fun UtmCoordinate.utmToPointCoordinates(): PointCoordinates {
* - I've found and fixed several bugs in the UTM implementation where I did have access to those
*/
fun PointCoordinates.toUpsCoordinate(): UtmCoordinate {
if (latitude >= UTM_SOUTHERN_LIMIT && latitude <= UTM_NORTHERN_LIMIT) {
if (latitude in UTM_SOUTHERN_LIMIT..UTM_NORTHERN_LIMIT) {
error("$latitude is outside UPS supported latitude range of [-90 - $UTM_SOUTHERN_LIMIT] or [$UTM_NORTHERN_LIMIT - 90]. You should use UTM")
}

Expand Down Expand Up @@ -572,7 +581,7 @@ fun UtmCoordinate.upsToPointCoordinates(): PointCoordinates {
} else {
-phi
}
return doubleArrayOf(fromRadians(longitude), fromRadians(latitude))
return doubleArrayOf(fromRadians(longitude), fromRadians(latitude)).normalize()
}


Expand Down
23 changes: 23 additions & 0 deletions src/commonMain/kotlin/com/jillesvangurp/geojson/geojson.kt
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,29 @@ val PointCoordinates.longitude: Double
get() = this[0]
val PointCoordinates.x get() = longitude

fun PointCoordinates.normalize(): PointCoordinates {
return if (longitude < -180.0 || longitude > 180.0 || latitude < -90.0 || latitude > 90.0) {
doubleArrayOf(
// Longitude normalization
((longitude + 180.0) % 360.0 + 360.0) % 360.0 - 180.0,
// Latitude normalization with modulo to account for multiple rotations (edge case)
when (val lat = ((latitude + 90.0) % 360.0 + 360.0) % 360.0 - 90.0) {
in 90.0..180.0 -> {
180.0 - lat
}
in -180.0..-90.0 -> {
-180.0 - lat
}
else -> {
lat
}
}
)
} else {
this
}
}

enum class CompassDirection(val letter: Char) { East('E'), West('W'), South('S'), North('N') }

typealias Degree = Double
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -166,10 +166,14 @@ class UTMTest {
convertedBack.distanceTo(p) shouldBeLessThan 1.0
}
}

val toMgrs = toUTM.toMgrs()
withClue("${p.latitude},${p.longitude} $toMgrs") {
toMgrs.toString().parseMgrs()!!.toPointCoordinate().distanceTo(p) shouldBeLessThan 2.0
MgrsPrecision.entries.forEach {precision ->
withClue(precision) {
toMgrs.usng(precision).parseMgrs()!!.toPointCoordinate().distanceTo(p) shouldBeLessThan 2.0 * precision.meters
}
}
}

val newUtm = toMgrs.toUtm()
Expand Down

0 comments on commit 1ccb29a

Please sign in to comment.