Skip to content

Commit 1ccb29a

Browse files
committed
add mgrs test for different precisions
normalize pointcoordinates (there were some issues with longitudes on the dateline)
1 parent 7d74ea6 commit 1ccb29a

File tree

4 files changed

+55
-27
lines changed

4 files changed

+55
-27
lines changed

src/commonMain/kotlin/com/jillesvangurp/geo/mgrs.kt

Lines changed: 7 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
package com.jillesvangurp.geo
22

33
import com.jillesvangurp.geojson.PointCoordinates
4-
import kotlin.math.abs
54
import kotlin.math.floor
65

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

7271
/**
7372
* MGRS is the compact format without spaces.
7473
*/
7574
fun mgrs(precision: MgrsPrecision = MgrsPrecision.ONE_M): String {
76-
val eastingStr = (easting / precision.divisor).toString().padStart(precision.digits, '0')
77-
val northingStr = (northing / precision.divisor).toString().padStart(precision.digits, '0')
75+
val eastingStr = (easting / precision.meters).toString().padStart(precision.digits, '0')
76+
val northingStr = (northing / precision.meters).toString().padStart(precision.digits, '0')
7877
return "$longitudeZone$latitudeZoneLetter$firstLetter$secondLetter$eastingStr$northingStr"
7978
}
8079
}
@@ -248,20 +247,13 @@ fun String.parseMgrs(): MgrsCoordinate? {
248247
} else {
249248
val mid = numbers.length / 2
250249
val precision = MgrsPrecision.entries[mid - 1]
251-
val easting = numbers.substring(0, mid).toInt() * precision.divisor
252-
val northing = numbers.substring(mid).toInt() * precision.divisor
250+
val easting = numbers.substring(0, mid).toInt() * precision.meters
251+
val northing = numbers.substring(mid).toInt() * precision.meters
253252
MgrsCoordinate(longitudeZone, latitudeZoneLetter, firstLetter, secondLetter, easting, northing)
254253
}
255254
}
256255
}
257256

258-
private fun roundMGRS(value: Double): Long {
259-
val ival = floor(value).toLong()
260-
val fraction = value - ival
261-
// double fraction = modf (value, &ivalue);
262-
return if (fraction > 0.5 || fraction == 0.5 && ival % 2L == 1L) ival + 1 else ival
263-
}
264-
265257
data class UpsConstant(
266258
val latitudeZoneLetter: Char,
267259
val chars: List<Char>,

src/commonMain/kotlin/com/jillesvangurp/geo/utm.kt

Lines changed: 19 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ import com.jillesvangurp.geo.GeoGeometry.Companion.toRadians
1818
import com.jillesvangurp.geojson.PointCoordinates
1919
import com.jillesvangurp.geojson.latitude
2020
import com.jillesvangurp.geojson.longitude
21+
import com.jillesvangurp.geojson.normalize
2122
import kotlin.math.*
2223

2324
/**
@@ -110,7 +111,7 @@ data class UtmCoordinate(
110111
}
111112
}
112113

113-
val UtmCoordinate.isUps get() = latitudeZoneLetter in listOf('A','B', 'Y', 'Z')
114+
val UtmCoordinate.isUps get() = latitudeZoneLetter in listOf('A', 'B', 'Y', 'Z')
114115
val UtmCoordinate.isUtm get() = !isUps
115116

116117
val UtmCoordinate.isSouth get() = latitudeZoneLetter < 'N'
@@ -218,7 +219,7 @@ private fun getLongitudeZone(latLong: PointCoordinates): Int {
218219
// UPS longitude zones
219220
val longitude = latLong.longitude
220221
return if (isNorthPolar(latLong) || isSouthPolar(latLong)) {
221-
if (longitude < 0.0) {
222+
if (longitude < 0.0) {
222223
30
223224
} else {
224225
31
@@ -227,30 +228,35 @@ private fun getLongitudeZone(latLong: PointCoordinates): Int {
227228
val latitudeZone: Char = getLatitudeZoneLetter(latLong)
228229
when {
229230
latitudeZone == 'X' && longitude > 0.0 && longitude < 42.0 -> {
230-
// X latitude exceptions
231+
// X latitude exceptions
231232
when {
232233
longitude < 9.0 -> {
233234
31
234235
}
236+
235237
longitude < 21.0 -> {
236238
33
237239
}
240+
238241
longitude < 33.0 -> {
239242
35
240243
}
244+
241245
else -> {
242246
37
243247
}
244248
}
245249
}
250+
246251
latitudeZone == 'V' && longitude > 0.0 && longitude < 12.0 -> {
247-
// V latitude exceptions
252+
// V latitude exceptions
248253
if (longitude < 3.0) {
249254
31
250255
} else {
251256
32
252257
}
253258
}
259+
254260
else -> {
255261
((longitude + 180) / 6).toInt() + 1
256262
}
@@ -293,16 +299,16 @@ private fun getCentralMeridian(longitudeZone: Int, latitudeZone: Char): Double {
293299
/**
294300
* Converts to UTM or UPS and selects the coordinate system based on the latitude.
295301
*/
296-
fun PointCoordinates.toUtmOrUps() : UtmCoordinate {
302+
fun PointCoordinates.toUtmOrUps(): UtmCoordinate {
297303
return if (latitude < UTM_SOUTHERN_LIMIT || latitude > UTM_NORTHERN_LIMIT) {
298304
toUpsCoordinate()
299305
} else {
300306
toUtmCoordinate()
301307
}
302308
}
303309

304-
fun UtmCoordinate.toPointCoordinates() : PointCoordinates {
305-
return if(isUps) upsToPointCoordinates() else utmToPointCoordinates()
310+
fun UtmCoordinate.toPointCoordinates(): PointCoordinates {
311+
return if (isUps) upsToPointCoordinates() else utmToPointCoordinates()
306312
}
307313

308314
fun PointCoordinates.toUtmCoordinate(): UtmCoordinate {
@@ -472,7 +478,10 @@ fun UtmCoordinate.utmToPointCoordinates(): PointCoordinates {
472478
* (61.0 + 662.0 * tan2Phi + 1320.0 * tan4Phi + 720.0 * tan6Phi))
473479
val latitude = phi - dE2 * t10 + dE4 * t11 - dE6 * t12 + dE8 * t13
474480
val longitude = (lambda0 + dE * t14 - dE3 * t15 + dE5 * t16 - dE7 * t17)
475-
return doubleArrayOf(fromRadians(longitude), fromRadians(latitude))
481+
return doubleArrayOf(
482+
fromRadians(longitude),
483+
fromRadians(latitude)
484+
).normalize()
476485
}
477486

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

@@ -572,7 +581,7 @@ fun UtmCoordinate.upsToPointCoordinates(): PointCoordinates {
572581
} else {
573582
-phi
574583
}
575-
return doubleArrayOf(fromRadians(longitude), fromRadians(latitude))
584+
return doubleArrayOf(fromRadians(longitude), fromRadians(latitude)).normalize()
576585
}
577586

578587

src/commonMain/kotlin/com/jillesvangurp/geojson/geojson.kt

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,29 @@ val PointCoordinates.longitude: Double
7979
get() = this[0]
8080
val PointCoordinates.x get() = longitude
8181

82+
fun PointCoordinates.normalize(): PointCoordinates {
83+
return if (longitude < -180.0 || longitude > 180.0 || latitude < -90.0 || latitude > 90.0) {
84+
doubleArrayOf(
85+
// Longitude normalization
86+
((longitude + 180.0) % 360.0 + 360.0) % 360.0 - 180.0,
87+
// Latitude normalization with modulo to account for multiple rotations (edge case)
88+
when (val lat = ((latitude + 90.0) % 360.0 + 360.0) % 360.0 - 90.0) {
89+
in 90.0..180.0 -> {
90+
180.0 - lat
91+
}
92+
in -180.0..-90.0 -> {
93+
-180.0 - lat
94+
}
95+
else -> {
96+
lat
97+
}
98+
}
99+
)
100+
} else {
101+
this
102+
}
103+
}
104+
82105
enum class CompassDirection(val letter: Char) { East('E'), West('W'), South('S'), North('N') }
83106

84107
typealias Degree = Double

src/commonTest/kotlin/com/jillesvangurp/geogeometry/UTMTest.kt

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -166,10 +166,14 @@ class UTMTest {
166166
convertedBack.distanceTo(p) shouldBeLessThan 1.0
167167
}
168168
}
169-
169+
170170
val toMgrs = toUTM.toMgrs()
171171
withClue("${p.latitude},${p.longitude} $toMgrs") {
172-
toMgrs.toString().parseMgrs()!!.toPointCoordinate().distanceTo(p) shouldBeLessThan 2.0
172+
MgrsPrecision.entries.forEach {precision ->
173+
withClue(precision) {
174+
toMgrs.usng(precision).parseMgrs()!!.toPointCoordinate().distanceTo(p) shouldBeLessThan 2.0 * precision.meters
175+
}
176+
}
173177
}
174178

175179
val newUtm = toMgrs.toUtm()

0 commit comments

Comments
 (0)