Skip to content

Instantly share code, notes, and snippets.

@p34eu
Created March 1, 2026 14:26
Show Gist options
  • Select an option

  • Save p34eu/0ea45b0ddb96cd69c782a08d5aaf4a9d to your computer and use it in GitHub Desktop.

Select an option

Save p34eu/0ea45b0ddb96cd69c782a08d5aaf4a9d to your computer and use it in GitHub Desktop.
Changes made for 8-character extended subsquare handling
// 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;
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment