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.

Revisions

  1. p34eu created this gist Mar 1, 2026.
    196 changes: 196 additions & 0 deletions latLonToGridSquare8Chars.js
    Original 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;