Skip to content

Commit

Permalink
documentation and cleanup
Browse files Browse the repository at this point in the history
  • Loading branch information
jillesvangurp committed Nov 17, 2023
1 parent e31e78d commit 8789b36
Show file tree
Hide file tree
Showing 3 changed files with 62 additions and 53 deletions.
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@ You can find the latest version in the [releases section](https://github.com/jil
- **translate a wgs84 coordinate** by x & y meters along the latitude and longitude
- **rotate** a point around another point
- extension functions to convert to and from [UTM coordinates](https://en.wikipedia.org/wiki/Universal_Transverse_Mercator_coordinate_system) and [UPS coordinates](https://en.wikipedia.org/wiki/Universal_polar_stereographic_coordinate_system).
- Conversion to and from MGRS / USNG format

- GeoHashUtils class with methods that allow you to:
- **encode and decode** geo hashes; this functionality has been adapted from the original Apache Lucene implementation of this class.
Expand Down
93 changes: 52 additions & 41 deletions src/commonMain/kotlin/com/jillesvangurp/geo/mgrs.kt
Original file line number Diff line number Diff line change
@@ -1,14 +1,22 @@
package com.jillesvangurp.geo

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

/**
* This mgrs code is the result of me doing a bit of dead code archeology on various
* Java code repositories that appear abandoned.
*
* All of them had issues, and Java is just hard to read. Objectively, this looks a lot cleaner.
* All of them had issues, and Java is just hard to read for this stuff (verbose, many levels of indirection, etc.).
*
* This implementation does not handle UPS currently but should work for UTM coordinates in the supported latitudes for that
*
* With some tweaks and fixes, I think I've nailed most of them. But it's always possible that I
* missed an edge case.
*
* The simple test here is that any coordinate in the UTM range should convert from and back
* to UTM without ending up more than a few meters away. The UTMTest contains such a test that also tests
* the conversion to and from mgrs.
*/

enum class MgrsPrecision(val divisor: Int) {
Expand Down Expand Up @@ -42,28 +50,7 @@ data class MgrsCoordinate(

}

private val mgrsRegex = "([0-9]+)\\s*([A-Z])\\s*([A-Z])\\s*([A-Z])\\s*([0-9]{1,5}\\s*[0-9]{1,5})".toRegex()
fun String.parseMgrs(): MgrsCoordinate? {
return mgrsRegex.find(this)?.let { match ->
val groups = match.groups
val longitudeZone = groups[1]!!.value.toInt()
val latitudeZoneLetter = groups[2]!!.value[0]
val firstLetter = groups[3]!!.value[0]
val secondLetter = groups[4]!!.value[0]
val numbers = groups[5]!!.value.replace(" ","")
if (numbers.length % 2 != 0) {
null
} 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
MgrsCoordinate(longitudeZone, latitudeZoneLetter, firstLetter, secondLetter, easting, northing)
}
}
}

fun Int.setForZone(): Int {
private fun Int.setForZone(): Int {
return when (this % 6) {
0 -> 6
1 -> 1
Expand All @@ -75,9 +62,8 @@ fun Int.setForZone(): Int {
}
}

private const val BLOCK_SIZE = 100000


private const val GRID_SIZE_M = 100_000
private const val TWO_MILLION = 2_000_000
private fun Int.colLetters() = when (this) {
1 -> "ABCDEFGH"
2 -> "JKLMNPQR"
Expand All @@ -92,17 +78,18 @@ private fun Int.rowLetters() = if (this % 2 == 0) "FGHJKLMNPQRSTUVABCDE" else "A

fun UtmCoordinate.lookupGridLetters(): Pair<Char, Char> {
var row = 1
var n = northing.roundToInt()
while (n >= BLOCK_SIZE) {
n -= BLOCK_SIZE
// var n = northing.roundToInt()
var n = floor(northing).toInt()
while (n >= GRID_SIZE_M) {
n -= GRID_SIZE_M
row++
}
row %= 20

var col = 0
var e = easting.roundToInt()
while (e >= BLOCK_SIZE) {
e -= BLOCK_SIZE
var e = floor(easting).toInt()
while (e >= GRID_SIZE_M) {
e -= GRID_SIZE_M
col++
}
col %= 8
Expand All @@ -125,8 +112,8 @@ fun UtmCoordinate.lookupGridLetters(): Pair<Char, Char> {
fun UtmCoordinate.toMgrs(): MgrsCoordinate {
val (l1, l2) = lookupGridLetters()

val mgrsEasting = floor(easting % BLOCK_SIZE).toInt()
val mgrsNorthing = floor(northing % BLOCK_SIZE).toInt()
val mgrsEasting = floor(easting % GRID_SIZE_M).toInt()
val mgrsNorthing = floor(northing % GRID_SIZE_M).toInt()
// println("cols ${northing.toInt() / BLOCK_SIZE} $northing ${(northing / 1000).toInt()}")
return MgrsCoordinate(
longitudeZone,
Expand All @@ -138,7 +125,7 @@ fun UtmCoordinate.toMgrs(): MgrsCoordinate {
)
}

data class LatitudeBandConstants(
private data class LatitudeBandConstants(
val firstLetter: Char, // col
val minNorthing: Double,
val northLat: Double,
Expand Down Expand Up @@ -170,9 +157,7 @@ private val latitudeBandConstants = listOf(
LatitudeBandConstants('X', 7900000.0, 84.5, 72.0, 6000000.0)
).associateBy { it.firstLetter }

val eastingArray = listOf("", "AJS", "BKT", "CLU", "DMV", "ENW", "FPX", "GQY", "HRZ")

private const val TWO_MILLION = 2000000
private val eastingArray = listOf("", "AJS", "BKT", "CLU", "DMV", "ENW", "FPX", "GQY", "HRZ")

fun MgrsCoordinate.toUtm(): UtmCoordinate {

Expand All @@ -181,7 +166,7 @@ fun MgrsCoordinate.toUtm(): UtmCoordinate {
val utmEasting = eastingArray.withIndex().first { (i,letters) ->
firstLetter in letters
}.let { (i, letters) ->
i * BLOCK_SIZE + easting
(i * GRID_SIZE_M + easting).toDouble()
}

val setNumber = longitudeZone.setForZone()
Expand All @@ -193,7 +178,33 @@ fun MgrsCoordinate.toUtm(): UtmCoordinate {
while(utmNorthing < bandConstants.minNorthing) {
utmNorthing += TWO_MILLION
}

utmNorthing += northing

return UtmCoordinate(longitudeZone, latitudeZoneLetter, utmEasting.toDouble(), utmNorthing)
return UtmCoordinate(longitudeZone, latitudeZoneLetter, utmEasting, utmNorthing)
}

fun PointCoordinates.toMgrs() = toUtmCoordinate().toMgrs()
fun MgrsCoordinate.toPointCoordinate() = toUtm().toPointCoordinates()

private val mgrsRegex = "([0-9]+)\\s*([A-Z])\\s*([A-Z])\\s*([A-Z])\\s*([0-9]{1,5}\\s*[0-9]{1,5})".toRegex()
fun String.parseMgrs(): MgrsCoordinate? {
return mgrsRegex.find(this)?.let { match ->
val groups = match.groups
val longitudeZone = groups[1]!!.value.toInt()
val latitudeZoneLetter = groups[2]!!.value[0]
val firstLetter = groups[3]!!.value[0]
val secondLetter = groups[4]!!.value[0]
val numbers = groups[5]!!.value.replace(" ","")
if (numbers.length % 2 != 0) {
null
} 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
MgrsCoordinate(longitudeZone, latitudeZoneLetter, firstLetter, secondLetter, easting, northing)
}
}
}

21 changes: 9 additions & 12 deletions src/commonTest/kotlin/com/jillesvangurp/geogeometry/UTMTest.kt
Original file line number Diff line number Diff line change
Expand Up @@ -154,14 +154,11 @@ class UTMTest {
nextDouble(-80.0, 84.0).roundDecimals(4)
)
}

val letters = mutableSetOf<Char>()


assertSoftly {
repeat(1000000) {
repeat(100000) {
Random.supportedUtmCoordinate().let { p ->
val toUTM = p.toUtmCoordinate()
letters.add(toUTM.latitudeZoneLetter)

runCatching {
val convertedBack = toUTM.utmToPointCoordinates()
Expand All @@ -171,10 +168,12 @@ class UTMTest {
}

// conversion to mgrs and back to utm should not deviate
// FIXME small amount of coordinates that fail this test
// withClue(toUTM) {
// toUTM.toMgrs().toUtm().toPointCoordinates().distanceTo(p) shouldBeLessThan 2.0
// }
val toMgrs = toUTM.toMgrs()
val newUtm = toMgrs.toUtm()
withClue("${p.latitude},${p.longitude} $toUTM | $toMgrs | $newUtm") {
// rounding errors can add up to 2 meters but close enough
newUtm.toPointCoordinates().distanceTo(p) shouldBeLessThan 2.0
}
}
}
}
Expand Down Expand Up @@ -203,9 +202,7 @@ class UTMTest {
runCatching {
val convertedBack = toUTM.upsToPointCoordinates()
withClue("${p.stringify()} -> ${convertedBack.stringify()} - $toUTM") {
convertedBack.let { out ->
out.distanceTo(p) shouldBeLessThan 1.0
}
convertedBack.distanceTo(p) shouldBeLessThan 1.0
}
}
}
Expand Down

0 comments on commit 8789b36

Please sign in to comment.