From a8d7f21e91e2986a74e5354ea329cf0cb6955cbb Mon Sep 17 00:00:00 2001 From: SpaceOceanCorp Date: Wed, 11 Nov 2026 13:55:22 -0600 Subject: [PATCH] Compute the GARS cell from one 5 minute index GARS.from() found the cell by scaling band fractions in steps (x 2 for the quadrant, then x 3 for the keypad). Rounding in those steps put points on a 5 minute boundary in the cell below or to the left: for example 10 05' E, 10 05' N gave 381JJ38 (the cell 10 00' to 10 05' N) instead of 381JJ35. Of 1,000,000 random whole-minute boundary values (degrees + minutes / 60), 555,120 landed in the wrong cell. It also produced strings that parse() rejects: longitude 180 gave band 721 (e.g. 721HN37) and latitude 90 gave band RA (e.g. 361RA37). Compute the 5 minute cell index directly, multiplying before adding the offset as GeographicLib and GEOTRANS do, map longitude 180 to -180 (band 001), and keep latitude 90 in the top band QZ. With this change none of the 1,000,000 boundary values or 1,000,000 random points land in the wrong cell, and every result parses. testPointBounds expected 462RA49 at latitude 90; it now expects 462QZ23, the top cell of band QZ. --- src/main/java/mil/nga/gars/GARS.java | 37 ++++++++++--------- src/test/java/mil/nga/gars/GARSTest.java | 45 +++++++++++++++++++++++- 2 files changed, 64 insertions(+), 18 deletions(-) diff --git a/src/main/java/mil/nga/gars/GARS.java b/src/main/java/mil/nga/gars/GARS.java index 4d9e076..38b05d1 100644 --- a/src/main/java/mil/nga/gars/GARS.java +++ b/src/main/java/mil/nga/gars/GARS.java @@ -315,31 +315,34 @@ public static GARS from(Point point) { // Normalize the longitude if needed GeometryUtils.normalizeWGS84(point); + double longitude = point.getLongitude(); + if (longitude >= GARSConstants.MAX_LON) { + // 180 is the same meridian as -180, in band 001 + longitude -= 360.0; + } - double lon = GARSUtils.getLongitudeDecimalBand(point.getLongitude()); - double lat = GARSUtils.getLatitudeDecimalBandValue(point.getLatitude()); + // Index of the 5 minute cell, counted from 180 W and 90 S. Multiply + // before adding the offset so that values on a cell boundary land in + // the cell they start (scaling the band fractions in steps let + // rounding put them in the cell below or to the left). The top row + // includes latitude 90. + int lonIndex = (int) Math.floor(longitude * 12.0) + 180 * 12; + int latIndex = (int) Math.floor(point.getLatitude() * 12.0) + 90 * 12; + lonIndex = Math.max(0, Math.min(lonIndex, 360 * 12 - 1)); + latIndex = Math.max(0, Math.min(latIndex, 180 * 12 - 1)); - int lonInt = (int) lon; - int latInt = (int) lat; + int lonInt = lonIndex / 6 + 1; + int latInt = latIndex / 6 + 1; String latBand = GARSUtils.bandLetters(latInt); - double lonDecimal = lon - lonInt; - double latDecimal = lat - latInt; - - double quadrantColumn = lonDecimal * 2.0; - double quadrantRow = latDecimal * 2.0; - - int quadrantColumnInt = (int) quadrantColumn; - int quadrantRowInt = (int) quadrantRow; + int quadrantColumnInt = (lonIndex % 6) / 3; + int quadrantRowInt = (latIndex % 6) / 3; int quadrant = GARSUtils.quadrant(quadrantColumnInt, quadrantRowInt); - lonDecimal = quadrantColumn - quadrantColumnInt; - latDecimal = quadrantRow - quadrantRowInt; - - int keypadColumn = (int) (lonDecimal * 3.0); - int keypadRow = (int) (latDecimal * 3.0); + int keypadColumn = lonIndex % 3; + int keypadRow = latIndex % 3; int keypad = GARSUtils.keypad(keypadColumn, keypadRow); diff --git a/src/test/java/mil/nga/gars/GARSTest.java b/src/test/java/mil/nga/gars/GARSTest.java index e4fb015..7256041 100644 --- a/src/test/java/mil/nga/gars/GARSTest.java +++ b/src/test/java/mil/nga/gars/GARSTest.java @@ -290,7 +290,9 @@ public void testPointBounds() { // Max latitude tests - String gars = "462RA49"; + // Latitude 90 is in the top band, QZ (there is no band RA, and + // parse() rejects it) + String gars = "462QZ23"; String gars2 = "462QZ26"; double longitude = 50.920338; double latitudeBelow = 89.9; @@ -497,4 +499,45 @@ private void testCoordinate(Point point, String value) } + /** + * Test points on the antimeridian, at the north pole, and on whole + * minute cell boundaries + * + * @throws ParseException + * upon failure to parse + */ + @Test + public void testCellBoundaries() throws ParseException { + + // 180 is the same meridian as -180; latitude 90 is in the top row. + // These used to give 721HN37 and 361RA37, which parse() rejects. + assertEquals("001HN37", GARS.from(180, 0).coordinate()); + assertEquals("361QZ11", GARS.from(0, 90).coordinate()); + assertEquals("001QZ11", GARS.from(180, 90).coordinate()); + assertEquals("720QZ23", GARS.from(179.9999999, 89.9999999).coordinate()); + GARS.parse(GARS.from(180, 0).coordinate()); + GARS.parse(GARS.from(0, 90).coordinate()); + + // A point on a 5 minute boundary is in the cell it starts + // (10 05' E 10 05' N used to give 381JJ38, the cell to the south) + assertEquals("381JJ35", GARS.from(10 + 5 / 60.0, 10 + 5 / 60.0).coordinate()); + assertEquals("340GR23", + GARS.from(-(10 + 5 / 60.0), -(10 + 5 / 60.0)).coordinate()); + + for (int i = 0; i < 360 * 12; i += 7) { + for (int j = 0; j < 180 * 12; j += 5) { + int lonMinutes = i * 5 - 180 * 60; + int latMinutes = j * 5 - 90 * 60; + double lon = Math.signum(lonMinutes) * (Math.abs(lonMinutes) / 60 + + (Math.abs(lonMinutes) % 60) / 60.0); + double lat = Math.signum(latMinutes) * (Math.abs(latMinutes) / 60 + + (Math.abs(latMinutes) % 60) / 60.0); + Point southwest = GARS.from(lon, lat).toPoint(); + assertEquals(lon, southwest.getLongitude(), 1e-9); + assertEquals(lat, southwest.getLatitude(), 1e-9); + } + } + + } + }