11package com.kylecorry.sol.science.geography
22
3- import com.kylecorry.sol.math.SolMath.power
4- import com.kylecorry.sol.math.SolMath.roundPlaces
5- import com.kylecorry.sol.shared.DecimalFormatter
6- import com.kylecorry.sol.shared.toDoubleCompat
3+ import com.kylecorry.sol.science.geography.formatting.*
74import com.kylecorry.sol.units.Coordinate
8- import gov.nasa.worldwind.avlist.AVKey
9- import gov.nasa.worldwind.geom.Angle
10- import gov.nasa.worldwind.geom.coords.MGRSCoordinateFormat
11- import gov.nasa.worldwind.geom.coords.UPSCoord
12- import gov.nasa.worldwind.geom.coords.UTMCoord
13- import uk.gov.dstl.geo.osgb.Constants
14- import uk.gov.dstl.geo.osgb.EastingNorthingConversion
15- import uk.gov.dstl.geo.osgb.NationalGrid
16- import uk.gov.dstl.geo.osgb.OSGB36
17- import java.util.*
18- import kotlin.math.abs
19- import kotlin.math.absoluteValue
205
216object CoordinateFormatter {
22- fun Coordinate.toDecimalDegrees (precision : Int = 6): String {
23- val formattedLatitude = DecimalFormatter .format(latitude, precision)
24- val formattedLongitude = DecimalFormatter .format(longitude, precision)
25- return " $formattedLatitude °, $formattedLongitude °"
26- }
27-
28- fun Coordinate.toDegreeDecimalMinutes (precision : Int = 3): String {
29- val latDir = if (latitude < 0 ) " S" else " N"
30- val lngDir = if (longitude < 0 ) " W" else " E"
31- return " ${ddmString(latitude, precision)}$latDir ${
32- ddmString(
33- longitude,
34- precision
35- )
36- }$lngDir "
37- }
38-
39- fun Coordinate.toDegreeMinutesSeconds (precision : Int = 1): String {
40- val latDir = if (latitude < 0 ) " S" else " N"
41- val lngDir = if (longitude < 0 ) " W" else " E"
42- return " ${dmsString(latitude, precision)}${latDir} ${
43- dmsString(
44- longitude,
45- precision
46- )
47- }${lngDir} "
48- }
49-
50- fun Coordinate.toUSNG (precision : Int = 5): String {
51- val mgrs = toMGRS(precision)
52- if (mgrs.length > 3 ) {
53- return mgrs.substring(0 , 3 ) + " " + mgrs.substring(3 )
54- }
55- return mgrs
56- }
57-
58- fun Coordinate.toMGRS (precision : Int = 5): String {
59- return try {
60- MGRSCoordinateFormat .getString(latitude, longitude, precision)
61- } catch (_: Exception ) {
62- " ?"
63- }
64- }
65-
66- fun Coordinate.toUTM (precision : Int = 7): String {
67- try {
68- val lat = Angle .fromDegreesLatitude(latitude)
69- val lng = Angle .fromDegreesLongitude(longitude)
70- val utm = UTMCoord .fromLatLon(lat, lng)
71-
72- val zone = utm.zone.toString().padStart(2 , ' 0' )
73-
74- val letter =
75- if (latitude < - 72 ) ' C' else if (latitude < - 64 ) ' D' else if (latitude < - 56 ) ' E' else if (latitude < - 48 ) ' F' else if (latitude < - 40 ) ' G' else if (latitude < - 32 ) ' H' else if (latitude < - 24 ) ' J' else if (latitude < - 16 ) ' K' else if (latitude < - 8 ) ' L' else if (latitude < 0 ) ' M' else if (latitude < 8 ) ' N' else if (latitude < 16 ) ' P' else if (latitude < 24 ) ' Q' else if (latitude < 32 ) ' R' else if (latitude < 40 ) ' S' else if (latitude < 48 ) ' T' else if (latitude < 56 ) ' U' else if (latitude < 64 ) ' V' else if (latitude < 72 ) ' W' else ' X'
76-
77-
78- val easting =
79- roundUTMPrecision(precision, utm.easting.toInt()).toString().padStart(7 , ' 0' ) + " E"
80- val northing =
81- roundUTMPrecision(precision, utm.northing.toInt()).toString().padStart(7 , ' 0' ) + " N"
82-
83- return " $zone$letter $easting $northing "
84- } catch (e: Exception ) {
85- return toUPS(precision)
86- }
87- }
88-
89- // TODO: Support precision
90- fun Coordinate.toOSGB (precision : Int = 5): String {
91- try {
92- val osgb36 = OSGB36 .fromWGS84(latitude, longitude)
93- val en = EastingNorthingConversion .fromLatLon(
94- osgb36,
95- Constants .ELLIPSOID_AIRY1830_MAJORAXIS ,
96- Constants .ELLIPSOID_AIRY1830_MINORAXIS ,
97- Constants .NATIONALGRID_N0 ,
98- Constants .NATIONALGRID_E0 ,
99- Constants .NATIONALGRID_F0 ,
100- Constants .NATIONALGRID_LAT0 ,
101- Constants .NATIONALGRID_LON0
102- )
103- val ng = NationalGrid .toNationalGrid(en)
104- if (ng != null ) {
105- return ng
106- }
107- } catch (e: Exception ) {
108- return " ?"
109- }
110- return " ?"
111- }
112-
113- private fun Coordinate.toUPS (precision : Int = 7): String {
114- try {
115- val lat = Angle .fromDegreesLatitude(latitude)
116- val lng = Angle .fromDegreesLongitude(longitude)
117- val ups = UPSCoord .fromLatLon(lat, lng)
118-
119- val easting =
120- roundUTMPrecision(precision, ups.easting.toInt()).toString().padStart(7 , ' 0' ) + " E"
121- val northing =
122- roundUTMPrecision(precision, ups.northing.toInt()).toString().padStart(7 , ' 0' ) + " N"
123-
124- val letter = if (isNorthernHemisphere) {
125- if (latitude == 90.0 || longitude >= 0 ) {
126- ' Z'
127- } else {
128- ' Y'
129- }
130- } else {
131- if (latitude == - 90.0 || longitude >= 0 ) {
132- ' B'
133- } else {
134- ' A'
135- }
136- }
137-
138- return " $letter $easting $northing "
139- } catch (e: Exception ) {
140- return " ?"
141- }
142- }
143-
144- private fun roundUTMPrecision (precision : Int , utmValue : Int ): Int {
145- return (utmValue / power(10.0 , 7 - precision)).toInt() * power(10.0 , 7 - precision).toInt()
146- }
147-
148- private fun ddmString (degrees : Double , precision : Int = 3): String {
149- val deg = abs(degrees.toInt())
150- val minutes = abs((degrees % 1 ) * 60 ).roundPlaces(precision)
151- return " $deg °$minutes '"
152- }
153-
154- private fun dmsString (degrees : Double , precision : Int = 1): String {
155- val deg = abs(degrees.toInt())
156- val minutes = abs((degrees % 1 ) * 60 )
157- val seconds = abs(((minutes % 1 ) * 60 ).roundPlaces(precision))
158- return " $deg °${minutes.toInt()} '$seconds \" "
159- }
7+ val formats = listOf (
8+ DecimalDegreesCoordinateFormat (),
9+ DegreesDecimalMinutesCoordinateFormat (),
10+ DegreesMinutesSecondsCoordinateFormat (),
11+ UTMCoordinateFormat (),
12+ MGRSCoordinateFormat (),
13+ USNGCoordinateFormat (),
14+ OSGBCoordinateFormat ()
15+ )
16016
16117 fun Coordinate.Companion.parse (
16218 location : String ,
163- format : CoordinateFormat ? = null
164- ): Coordinate ? {
165- if (format == null ) {
166- for (fmt in CoordinateFormat .values()) {
167- val parsed = parse(location, fmt)
168- if (parsed != null ) {
169- return parsed
170- }
171- }
172- return null
173- }
174-
175- return when (format) {
176- CoordinateFormat .DecimalDegrees -> fromDecimalDegrees(location)
177- CoordinateFormat .DegreesDecimalMinutes -> fromDegreesDecimalMinutes(location)
178- CoordinateFormat .DegreesMinutesSeconds -> fromDegreesMinutesSeconds(location)
179- CoordinateFormat .UTM -> fromUTM(location)
180- CoordinateFormat .MGRS -> fromMGRS(location)
181- CoordinateFormat .USNG -> fromMGRS(location)
182- CoordinateFormat .OSGB -> fromOSGB(location)
183- }
184- }
185-
186- private fun fromMGRS (location : String ): Coordinate ? {
187- return try {
188- return MGRSCoordinateFormat .fromString(location)
189- } catch (e: Exception ) {
190- null
191- }
192- }
193-
194- private fun fromOSGB (location : String ): Coordinate ? {
195- return try {
196- val eastingNorthing = try {
197- NationalGrid .fromNationalGrid(location)
198- } catch (e: Exception ) {
199- val split = location.split(" ," )
200- val en = split.mapNotNull { it.toDoubleCompat() }.toDoubleArray()
201- NationalGrid .toNationalGrid(en)
202- en
203- }
204- val latlonOSGB = EastingNorthingConversion .toLatLon(
205- eastingNorthing,
206- Constants .ELLIPSOID_AIRY1830_MAJORAXIS ,
207- Constants .ELLIPSOID_AIRY1830_MINORAXIS ,
208- Constants .NATIONALGRID_N0 ,
209- Constants .NATIONALGRID_E0 ,
210- Constants .NATIONALGRID_F0 ,
211- Constants .NATIONALGRID_LAT0 ,
212- Constants .NATIONALGRID_LON0
213- )
214- val latlonWGS84 = OSGB36 .toWGS84(latlonOSGB[0 ], latlonOSGB[1 ])
215- return Coordinate (latlonWGS84[0 ], latlonWGS84[1 ])
216- } catch (e: Exception ) {
217- null
218- }
219- }
220-
221- private fun fromDecimalDegrees (location : String ): Coordinate ? {
222- val regex = Regex (" ^(-?\\ d+(?:[.,]\\ d+)?)°?[,\\ s]+(-?\\ d+(?:[.,]\\ d+)?)°?\$ " )
223- val matches = regex.find(location.trim()) ? : return null
224- val latitude = matches.groupValues[1 ].toDoubleCompat() ? : return null
225- val longitude = matches.groupValues[2 ].toDoubleCompat() ? : return null
226-
227- if (isValidLatitude(latitude) && isValidLongitude(longitude)) {
228- return Coordinate (latitude, longitude)
229- }
230-
231- return null
232- }
233-
234- private fun fromDegreesDecimalMinutes (location : String ): Coordinate ? {
235- val ddmRegex =
236- Regex (" ^(\\ d+)°\\ s*(\\ d+(?:[.,]\\ d+)?)[′']\\ s*([nNsS])[,\\ s]+(\\ d+)°\\ s*(\\ d+(?:[.,]\\ d+)?)[′']\\ s*([wWeE])\$ " )
237- val matches = ddmRegex.find(location.trim()) ? : return null
238-
239- var latitudeDecimal = 0.0
240- latitudeDecimal + = matches.groupValues[1 ].toDouble()
241- latitudeDecimal + = (matches.groupValues[2 ].toDoubleCompat() ? : 0.0 ) / 60
242- latitudeDecimal * = if (matches.groupValues[3 ].lowercase(Locale .getDefault()) == " n" ) 1 else - 1
243-
244- var longitudeDecimal = 0.0
245- longitudeDecimal + = matches.groupValues[4 ].toDouble()
246- longitudeDecimal + = (matches.groupValues[5 ].toDoubleCompat() ? : 0.0 ) / 60
247- longitudeDecimal * = if (matches.groupValues[6 ].lowercase(Locale .getDefault()) == " e" ) 1 else - 1
248-
249- if (isValidLatitude(latitudeDecimal) && isValidLongitude(
250- longitudeDecimal
251- )
252- ) {
253- return Coordinate (latitudeDecimal, longitudeDecimal)
254- }
255-
256- return null
257- }
258-
259- private fun fromDegreesMinutesSeconds (location : String ): Coordinate ? {
260- val dmsRegex =
261- Regex (" ^(\\ d+)°\\ s*(\\ d+)[′']\\ s*(\\ d+(?:[.,]\\ d+)?)[″\" ]\\ s*([nNsS])[,\\ s]+(\\ d+)°\\ s*(\\ d+)[′']\\ s*(\\ d+(?:[.,]\\ d+)?)[″\" ]\\ s*([wWeE])\$ " )
262- val matches = dmsRegex.find(location.trim()) ? : return null
263-
264- var latitudeDecimal = 0.0
265- latitudeDecimal + = matches.groupValues[1 ].toDouble()
266- latitudeDecimal + = matches.groupValues[2 ].toDouble() / 60
267- latitudeDecimal + = (matches.groupValues[3 ].toDoubleCompat() ? : 0.0 ) / (60 * 60 )
268- latitudeDecimal * = if (matches.groupValues[4 ].lowercase(Locale .getDefault()) == " n" ) 1 else - 1
269-
270- var longitudeDecimal = 0.0
271- longitudeDecimal + = matches.groupValues[5 ].toDouble()
272- longitudeDecimal + = matches.groupValues[6 ].toDouble() / 60
273- longitudeDecimal + = (matches.groupValues[7 ].toDoubleCompat() ? : 0.0 ) / (60 * 60 )
274- longitudeDecimal * = if (matches.groupValues[8 ].lowercase(Locale .getDefault()) == " e" ) 1 else - 1
275-
276- if (isValidLatitude(latitudeDecimal) && isValidLongitude(
277- longitudeDecimal
278- )
279- ) {
280- return Coordinate (latitudeDecimal, longitudeDecimal)
281- }
282-
283- return null
284- }
285-
286- private fun fromUTM (utm : String ): Coordinate ? {
287- val regex =
288- Regex (" (\\ d*)\\ s*([a-z,A-Z^ioIO])\\ s*(\\ d+(?:[.,]\\ d+)?)[\\ smMeE]+(\\ d+(?:[.,]\\ d+)?)[\\ smMnN]*" )
289- val matches = regex.find(utm) ? : return null
290-
291- val zone = matches.groupValues[1 ].toIntOrNull() ? : 0
292- val letter = matches.groupValues[2 ].toCharArray().first()
293- val easting = matches.groupValues[3 ].toDoubleCompat() ? : 0.0
294- val northing = matches.groupValues[4 ].toDoubleCompat() ? : 0.0
295-
296- return fromUTM(zone, letter, easting, northing)
297- }
298-
299- private fun fromUTM (
300- zone : Int ,
301- letter : Char ,
302- easting : Double ,
303- northing : Double
19+ formats : List <CoordinateFormat > = CoordinateFormatter .formats
30420 ): Coordinate ? {
305- val polarLetters = listOf (' A' , ' B' , ' Y' , ' Z' )
306- return try {
307- if (polarLetters.contains(letter.uppercaseChar())) {
308- // Get it into the catch block
309- throw Exception ()
310- }
311- val latLng = UTMCoord .locationFromUTMCoord(
312- zone,
313- if (letter.uppercaseChar() <= ' M' ) AVKey .SOUTH else AVKey .NORTH ,
314- easting,
315- northing
316- )
317- Coordinate (latLng.latitude.degrees, latLng.longitude.degrees)
318- } catch (e: Exception ) {
319- val letters = listOf (' A' , ' B' , ' Y' , ' Z' )
320- if (zone != 0 || ! letters.contains(letter.uppercaseChar())) {
321- return null
322- }
323- try {
324- val latLng = UPSCoord .fromUPS(
325- if (letter.uppercaseChar() <= ' M' ) AVKey .SOUTH else AVKey .NORTH ,
326- easting,
327- northing
328- )
329- Coordinate (latLng.latitude.degrees, latLng.longitude.degrees)
330- } catch (e2: Exception ) {
331- null
332- }
333- }
334- }
335-
336- private fun isValidLongitude (longitude : Double ): Boolean {
337- return longitude.absoluteValue <= 180
21+ return formats.firstNotNullOfOrNull { it.parse(location) }
33822 }
339-
340- private fun isValidLatitude (latitude : Double ): Boolean {
341- return latitude.absoluteValue <= 90
342- }
343-
34423}
0 commit comments