Created
March 1, 2026 14:26
-
-
Save p34eu/0ea45b0ddb96cd69c782a08d5aaf4a9d to your computer and use it in GitHub Desktop.
Revisions
-
p34eu created this gist
Mar 1, 2026 .There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters. Learn more about bidirectional Unicode charactersOriginal file line number Diff line number Diff line change @@ -0,0 +1,196 @@ // HamGridSquare.js // @ts-nocheck // Copyright 2014 Paul Brewer KI6CQ // License: MIT License http://opensource.org/licenses/MIT // // Javascript routines to convert from lat-lon to Maidenhead Grid Squares // typically used in Ham Radio Satellite operations and VHF Contests // // Inspired in part by K6WRU Walter Underwood's python answer // http://ham.stackexchange.com/a/244 // to this stack overflow question: // How Can One Convert From Lat/Long to Grid Square // http://ham.stackexchange.com/questions/221/how-can-one-convert-from-lat-long-to-grid-square // const latLonToGridSquare = function (param1: any, param2?: any): string { var lat = -100.0; var lon = 0.0; var adjLat, adjLon, GLat, GLon, nLat, nLon, gLat, gLon, rLat, rLon; var U = 'ABCDEFGHIJKLMNOPQRSTUVWX'; var L = U.toLowerCase(); // NOTE (2026-03-01): Changes made for 8-character extended subsquare handling // - The original library attempted to use letters for positions 7-8 (a-x) // but that produced occasional out-of-range indices and "undefined" when // calculating the extended subsquare. To avoid that bug we use a digit-based // extended subsquare (0-9) for positions 7-8. This is a pragmatic, non-standard // 8-char extension: Maidenhead officially defines up to 6 characters (fields, // squares and subsquares). If strict Maidenhead compliance is required you // should revert to a letter-based 8-char extension (a-x) and ensure indices // are computed within 0..23. // support Chris Veness 2002-2012 LatLon library and // other objects with lat/lon properties // properties could be numbers, or strings function toNum(x: any): number { if (typeof x === 'number') return x; if (typeof x === 'string') return parseFloat(x); // dont call a function property here because of binding issue throw 'HamGridSquare -- toNum -- can not convert input: ' + x; } if (typeof param1 === 'object') { if (param1.length === 2) { lat = toNum(param1[0]); lon = toNum(param1[1]); } else if ('lat' in param1 && 'lon' in param1) { lat = typeof param1.lat === 'function' ? toNum(param1.lat()) : toNum(param1.lat); lon = typeof param1.lon === 'function' ? toNum(param1.lon()) : toNum(param1.lon); } else if ('latitude' in param1 && 'longitude' in param1) { lat = typeof param1.latitude === 'function' ? toNum(param1.latitude()) : toNum(param1.latitude); lon = typeof param1.longitude === 'function' ? toNum(param1.longitude()) : toNum(param1.longitude); } else { throw 'HamGridSquare -- can not convert object -- ' + param1; } } else { lat = toNum(param1); lon = toNum(param2); } if (isNaN(lat)) throw 'lat is NaN'; if (isNaN(lon)) throw 'lon is NaN'; if (Math.abs(lat) === 90.0) throw 'grid squares invalid at N/S poles'; if (Math.abs(lat) > 90) throw 'invalid latitude: ' + lat; if (Math.abs(lon) > 180) throw 'invalid longitude: ' + lon; adjLat = lat + 90; adjLon = lon + 180; GLat = U[Math.trunc(adjLat / 10)]; GLon = U[Math.trunc(adjLon / 20)]; nLat = '' + Math.trunc(adjLat % 10); nLon = '' + Math.trunc((adjLon / 2) % 10); rLat = (adjLat - Math.trunc(adjLat)) * 60; rLon = (adjLon - 2 * Math.trunc(adjLon / 2)) * 60; gLat = L[Math.trunc(rLat / 2.5)]; gLon = L[Math.trunc(rLon / 5)]; // Extended subsquare for 8-char locator (30" x 15" precision) // Implementation note: we emit digits (0-9) for the final two characters // (eLon,eLat). This yields 10 subdivisions per axis (vs 24 if using a-x) // and avoids the previous letter-index overflow bug. var rLat2 = (rLat - Math.trunc(rLat / 2.5) * 2.5); var rLon2 = (rLon - Math.trunc(rLon / 5) * 5); var eLat = '' + Math.trunc(rLat2 / 0.25); var eLon = '' + Math.trunc(rLon2 / 0.5); return GLon + GLat + nLon + nLat + gLon + gLat + eLon + eLat; }; // If a LatLon constructor exists in the global scope, the library will use it. // Declare it for TypeScript to avoid compile errors when it's not present. declare const LatLon: any; const gridSquareToLatLon = function (grid: string, obj?: any): any { var returnLatLonConstructor = typeof LatLon === 'function'; var returnObj = typeof obj === 'object'; var lat = 0.0, lon = 0.0, aNum = 'a'.charCodeAt(0), numA = 'A'.charCodeAt(0); function lat4(g: string): number { return 10 * (g.charCodeAt(1) - numA) + parseInt(g.charAt(3)) - 90; } function lon4(g: string): number { return 20 * (g.charCodeAt(0) - numA) + 2 * parseInt(g.charAt(2)) - 180; } if (grid.length != 4 && grid.length != 6 && grid.length != 8) throw 'gridSquareToLatLon: grid must be 4, 6, or 8 chars: ' + grid; // Parsing note: This implementation accepts 8-character grid squares where // the final pair (positions 7-8) are digits 0-9 (a pragmatic extension). // If you need an 8-char format that uses letters (a-x) in positions 7-8, // adjust the regex and conversion below accordingly. if (/^[A-X][A-X][0-9][0-9]$/.test(grid)) { lat = lat4(grid) + 0.5; lon = lon4(grid) + 1; } else if (/^[A-X][A-X][0-9][0-9][a-x][a-x]$/.test(grid)) { lat = lat4(grid) + (1.0 / 60.0) * 2.5 * (grid.charCodeAt(5) - aNum + 0.5); lon = lon4(grid) + (1.0 / 60.0) * 5 * (grid.charCodeAt(4) - aNum + 0.5); } else if (/^[A-X][A-X][0-9][0-9][a-x][a-x][0-9][0-9]$/.test(grid)) { // 8-char: extended subsquare with digits (30" x 15" precision) lat = lat4(grid) + (1.0 / 60.0) * 2.5 * (grid.charCodeAt(5) - aNum) + (1.0 / 60.0) * 0.25 * (parseInt(grid.charAt(7)) + 0.5); lon = lon4(grid) + (1.0 / 60.0) * 5 * (grid.charCodeAt(4) - aNum) + (1.0 / 60.0) * 0.5 * (parseInt(grid.charAt(6)) + 0.5); } else throw 'gridSquareToLatLon: invalid grid: ' + grid; if (returnLatLonConstructor) return new LatLon(lat, lon); if (returnObj) { obj.lat = lat; obj.lon = lon; return obj; } return [lat, lon]; }; const testGridSquare = function (): boolean { // First four test examples are from "Conversion Between Geodetic and Grid Locator Systems", // by Edmund T. Tyson N5JTY QST January 1989 // original test data in Python / citations by Walter Underwood K6WRU // last test and coding into Javascript from Python by Paul Brewer KI6CQ var testData = [ ['Munich', [48.14666, 11.60833], 'JN58td'], ['Montevideo', [[-34.91, -56.21166]], 'GF15vc'], ['Washington, DC', [{ lat: 38.92, lon: -77.065 }], 'FM18lw'], ['Wellington', [{ latitude: -41.28333, longitude: 174.745 }], 'RE78ir'], ['Newington, CT (W1AW)', [41.714775, -72.72726], 'FN31pr'], ['Palo Alto (K6WRU)', [[37.413708, -122.1073236]], 'CM87wj'], [ 'Chattanooga (KI6CQ/4)', [ { lat: function () { return '35.0542'; }, lon: function () { return '-85.1142'; }, }, ], 'EM75kb', ], ]; var i = 0, l = testData.length, result = '', result2, result3, thisPassed = 0, totalPassed = 0; for (i = 0; i < l; ++i) { result = latLonToGridSquare.apply({}, testData[i][1]); result2 = gridSquareToLatLon(result); result3 = latLonToGridSquare(result2); thisPassed = result === testData[i][2] && result3 === testData[i][2]; // console.log("test "+i+": "+testData[i][0]+" "+JSON.stringify(testData[i][1])+ // " result = "+result+" result2 = "+result2+" result3 = "+result3+" expected= "+testData[i][2]+ // " passed = "+thisPassed); totalPassed += thisPassed; } // console.log(totalPassed+" of "+l+" test passed"); return totalPassed === l; }; const HamGridSquare = { toLatLon: gridSquareToLatLon, fromLatLon: latLonToGridSquare, test: testGridSquare, }; // Exports for ES modules / TypeScript imports export default HamGridSquare; export const fromLatLon = latLonToGridSquare; export const toLatLon = gridSquareToLatLon;