diff --git a/exec/load/maidenhead.js b/exec/load/maidenhead.js new file mode 100644 index 0000000000000000000000000000000000000000..97dac5a76938a32521513a996aa3ffc90f675866 --- /dev/null +++ b/exec/load/maidenhead.js @@ -0,0 +1,298 @@ +function Maidenhead(locator, latitude, longitude, precision) +{ + this.locator = locator; + this.longitude = longitude; + this.latitude = latitude; + this.precision = precision; + var lop; + var lap; + var ucl; + var stupidhack; + var i; + + function parselocator(locator) { + function add_pos(subloc, pos) { + if (this.positions[pos].range == 10) { + lop = subloc.charCodeAt(0) - '0'.charCodeAt(); + lap = subloc.charCodeAt(1) - '0'.charCodeAt(); + } else { + lop = subloc.charCodeAt(0) - 'A'.charCodeAt(); + lap = subloc.charCodeAt(1) - 'A'.charCodeAt(); + } + + if (lap < 0 || lop < 0 || lap >= this.positions[pos].range || lop >= this.positions[pos].range) + throw('invalid field in locator'); + this.longitude += this.positions[pos].precision * lop; + this.latitude += this.positions[pos].precision / 2 * lap; + } + + ucl = locator.toUpperCase(); + if (ucl.length > 18) + throw('locator too precise'); + if (ucl.length & 1) + throw('locator length is odd'); + + this.latitude = -90.0; + this.longitude = -180.0; + this.precision = 0; + while (ucl.length >= 2) { + add_pos.call(this, ucl.substr(0, 2), this.precision); + ucl = ucl.substr(2); + this.precision += 1; + } + + this.south_west = {'longitude': this.longitude, 'latitude': this.latitude}; + this.north_west = {'longitude': this.longitude, 'latitude': this.latitude + this.positions[this.precision-1].precision / 2}; + this.south_east = {'longitude': this.longitude + this.positions[this.precision-1].precision, 'latitude': this.latitude}; + this.north_east = {'longitude': this.longitude + this.positions[this.precision-1].precision, 'latitude': this.latitude + this.positions[this.precision-1].precision / 2}; + this.longitude += this.positions[this.precision-1].precision / 2; + this.latitude += this.positions[this.precision-1].precision / 4; + this.locator = locator; + } + + function sub_pos(hack, pos) { + lop = parseInt(hack.longitude / this.positions[pos].precision, 10); + lap = parseInt(hack.latitude / (this.positions[pos].precision / 2), 10); + if (lap < 0 || lop < 0 || lap >= this.positions[pos].range || lop >= this.positions[pos].range) + throw('invalid latitude or longitude'); + if (this.positions[pos].range == 10) { + hack.locator = hack.locator + String.fromCharCode('0'.charCodeAt()+lop); + hack.locator = hack.locator + String.fromCharCode('0'.charCodeAt()+lap); + } else if (this.positions[pos].range == 18) { + hack.locator = hack.locator + String.fromCharCode('A'.charCodeAt()+lop); + hack.locator = hack.locator + String.fromCharCode('A'.charCodeAt()+lap); + } else { + hack.locator = hack.locator + String.fromCharCode('a'.charCodeAt()+lop); + hack.locator = hack.locator + String.fromCharCode('a'.charCodeAt()+lap); + } + hack.longitude -= lop * this.positions[pos].precision; + hack.latitude -= lap * (this.positions[pos].precision / 2); + } + + if (locator !== undefined) { + parselocator.call(this, locator); + } else { + if (latitude === undefined || longitude === undefined || precision === undefined) + throw('must specify locator or lat, lon, precision'); + if (latitude < -90 || latitude > 90) + throw('Invalid latitude'); + if (longitude < -180 || longitude > 180) + throw('Invalid longitude'); + stupidhack = {'latitude': latitude + 90, 'longitude': longitude + 180, 'locator': ''}; + + for (i=0; i<precision; i++) + sub_pos.call(this, stupidhack, i); + parselocator(stupidhack.locator); + } + +} + +Maidenhead.prototype.positions = [ + {'precision': 20.0, 'range': 18}, + {'precision': 2.0, 'range':10}, + {'precision': 2.0/24, 'range': 24}, + {'precision': 2.0/240, 'range': 10}, + {'precision': 2.0/5760, 'range': 24}, + {'precision': 2.0/57600, 'range': 10}, + {'precision': 2.0/115200, 'range': 10}, + {'precision': 2.0/276480000, 'range': 24}, + {'precision': 2.0/2764800000, 'range': 10} +]; + +Maidenhead.prototype.distance = function(to) +{ + /* - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - */ + /* Latitude/longitude spherical geodesy tools (c) Chris Veness 2002-2017 */ + /* MIT Licence */ + /* www.movable-type.co.uk/scripts/latlong.html */ + /* www.movable-type.co.uk/scripts/geodesy/docs/module-latlon-spherical.html */ + /* - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - */ + var radius; + var rlatfrom; + var rlatto; + var rlonfrom; + var rlonto; + var rlondiff; + var rlatdiff; + var a; + var c; + + if (!(to instanceof Maidenhead)) + throw('argument must be a Maidenhead'); + radius = 6371e3; + rlatfrom = this.latitude * (Math.PI / 180); + rlatto = to.latitude * (Math.PI / 180); + rlonfrom = this.longitude * (Math.PI / 180); + rlonto = to.longitude * (Math.PI / 180); + rlondiff = rlonto - rlonfrom; + rlatdiff = rlatto - rlatfrom; + a = Math.sin(rlatdiff/2)*Math.sin(rlatdiff/2)+Math.cos(rlatfrom)*Math.cos(rlatto)*Math.sin(rlondiff/2)*Math.sin(rlondiff/2); + c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1-a)); + return radius * c; +}; + +Maidenhead.prototype.bearing = function(to) +{ + /* - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - */ + /* Latitude/longitude spherical geodesy tools (c) Chris Veness 2002-2017 */ + /* MIT Licence */ + /* www.movable-type.co.uk/scripts/latlong.html */ + /* www.movable-type.co.uk/scripts/geodesy/docs/module-latlon-spherical.html */ + /* - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - */ + var rlatfrom; + var rlatto; + var rlondiff; + var y; + var x; + var rbear; + + if (!(to instanceof Maidenhead)) + throw('argument must be a Maidenhead'); + rlatfrom = this.latitude * (Math.PI / 180); + rlatto = to.latitude * (Math.PI / 180); + rlondiff = to.longitude - this.longitude; + y = Math.sin(rlondiff) * Math.cos(rlatto); + x = Math.cos(rlatfrom)*Math.sin(rlatto)-Math.sin(rlatfrom)*Math.cos(rlatto)*Math.cos(rlondiff); + rbear = Math.atan2(y, x); + return ((rbear / (Math.PI / 180))+360) % 360; +}; + +Maidenhead.prototype.vdistance = function(to) +{ + if (!(to instanceof Maidenhead)) + throw('argument must be a Maidenhead'); + try { + return this.vinverse(to).distance; + } catch (e) { + return undefined; + } +}; + +Maidenhead.prototype.vbearing = function(to) +{ + if (!(to instanceof Maidenhead)) + throw('argument must be a Maidenhead'); + try { + return this.vinverse(to).initialBearing; + } catch (e) { + return undefined; + } +}; + +Maidenhead.prototype.vinverse = function(to) +{ + // Adapted from: https://www.movable-type.co.uk/scripts/latlong-vincenty.html + // using the WGS-84 ellipsoid. + /* - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - */ + /* Vincenty Direct and Inverse Solution of Geodesics on the Ellipsoid (c) Chris Veness 2002-2017 */ + /* MIT Licence */ + /* www.movable-type.co.uk/scripts/latlong-vincenty.html */ + /* www.movable-type.co.uk/scripts/geodesy/docs/module-latlon-vincenty.html */ + /* - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - */ + var p1, p2; + var rlat1, rlat2; + var a, b, f; + var L; + var tanU1, cosU1, sinU1, tanU2, cosU2, sinU2; + var sinsig, cossig, sig, cosSqalpg, cos2sigM; + var rlon, iterations, antimeridian; + var sinrlon, cosrlon; + var sinSqsig, sinalph, cidenheaosSqalph, cosSqalph, C, rlonprime, iterationCheck; + var uSq, A, B, delatsig, s, alph1, alph2, ret, rlon1, rlon2; + + if (!(to instanceof Maidenhead)) + throw('argument must be a Maidenhead'); + p1 = {'lat': this.latitude, 'lon': this.longitude}; + p2 = {'lat': to.latitude, 'lon': to.longitude}; + if (p1.lon == -180.0) + p1.lon = 180.0; + rlat1 = p1.lat * (Math.PI / 180); + rlon1 = p1.lon * (Math.PI / 180); + rlat2 = p2.lat * (Math.PI / 180); + rlon2 = p2.lon * (Math.PI / 180); + + // TODO: Get these values... + a = 6378137.0; + b = 6356752.3142; + f = 1.0/298.257223563; + + L = rlon2 - rlon1; + tanU1 = (1.0-f) * Math.tan(rlat1); + cosU1 = 1.0 / Math.sqrt((1.0 + tanU1*tanU1)); + sinU1 = tanU1 * cosU1; + tanU2 = (1.0-f) * Math.tan(rlat2); + cosU2 = 1.0 / Math.sqrt((1.0 + tanU2*tanU2)); + sinU2 = tanU2 * cosU2; + + sinsig=0.0; + cossig=0.0; + sig=0.0; + cosSqalph=0.0; + cos2sigM=0.0; + + rlon = L; + iterations = 0.0; + antimeridian = Math.abs(L) > Math.PI; + + for (;;) { + sinrlon = Math.sin(rlon); + cosrlon = Math.cos(rlon); + sinSqsig = (cosU2*sinrlon) * (cosU2*sinrlon) + (cosU1*sinU2-sinU1*cosU2*cosrlon) * (cosU1*sinU2-sinU1*cosU2*cosrlon); + if (sinSqsig == 0) + break; // co-incident points + sinsig = Math.sqrt(sinSqsig); + cossig = sinU1*sinU2 + cosU1*cosU2*cosrlon; + sig = Math.atan2(sinsig, cossig); + sinalph = cosU1 * cosU2 * sinrlon / sinsig; + cidenheaosSqalph = 1 - sinalph*sinalph; + if (cosSqalph != 0) + cos2sigM = cossig - 2.0*sinU1*sinU2/cosSqalph; + else + cos2sigM = 0.0; // equatorial + C = f/16.0*cosSqalph*(4.0+f*(4.0-3.0*cosSqalph)); + rlonprime = rlon; + rlon = L + (1.0-C) * f * sinalph * (sig + C*sinsig*(cos2sigM+C*cossig*(-1.0+2.0*cos2sigM*cos2sigM))); + if (antimeridian) + iterationCheck = Math.abs(rlon)-Math.PI; + else + iterationCheck = Math.abs(rlon); + if (iterationCheck > Math.PI) + throw('rlon > pi'); + iterations += 1; + if (iterations >= 1000) + throw('Formula failed to converge'); + if (Math.abs(rlon-rlonprime) <= 1e-12) + break; + } + + uSq = cosSqalph * (a*a - b*b) / (b*b); + A = 1.0 + uSq/16384.0*(4096.0+uSq*(-768.0+uSq*(320.0-175.0*uSq))); + B = uSq/1024.0 * (256.0+uSq*(-128.0+uSq*(74.0-47.0*uSq))); + delatsig = B*sinsig*(cos2sigM+B/4.0*(cossig*(-1.0+2.0*cos2sigM*cos2sigM)- + B/6.0*cos2sigM*(-3.0+4.0*sinsig*sinsig)*(-3.0+4.0*cos2sigM*cos2sigM))); + + s = b*A*(sig-delatsig); + + alph1 = Math.atan2(cosU2*sinrlon, cosU1*sinU2-sinU1*cosU2*cosrlon); + alph2 = Math.atan2(cosU1*sinrlon, -sinU1*cosU2+cosU1*sinU2*cosrlon); + + alph1 = (alph1 + 2.0*Math.PI) % (2.0*Math.PI); // normalise to 0..360 + alph2 = (alph2 + 2.0*Math.PI) % (2.0*Math.PI); // normalise to 0..360 + + ret = { 'distance': s, 'iterations': iterations}; + if (s === 0) { + ret.initialBearing = undefined; + ret.finalBearing = undefined; + } else { + ret.initialBearing = alph1 / (Math.PI / 180); + ret.finalBearing = alph2 / (Math.PI / 180); + } + + return ret; +}; + +var here = new Maidenhead('EN72gw'); +print(here.distance(new Maidenhead('DM13dp'))); +print(here.vdistance(new Maidenhead('DM13dp'))); +print(here.bearing(new Maidenhead('DM13dp'))); +print(here.bearing(new Maidenhead('DM13dp')));