UNPKG

usng-gmaps

Version:

USNG for Google Maps V3

807 lines (691 loc) 30 kB
import Proj4js from 'proj4'; /* eslint-disable */ export const USNG2 = function() { // Note: grid locations are the SW corner of the grid square (because easting and northing are always positive) // 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 x 100,000m northing var NSLetters135 = ['A','B','C','D','E','F','G','H','J','K','L','M','N','P','Q','R','S','T','U','V']; var NSLetters246 = ['F','G','H','J','K','L','M','N','P','Q','R','S','T','U','V','A','B','C','D','E']; // 1 2 3 4 5 6 7 8 x 100,000m easting var EWLetters14 = ['A','B','C','D','E','F','G','H']; var EWLetters25 = ['J','K','L','M','N','P','Q','R']; var EWLetters36 = ['S','T','U','V','W','X','Y','Z']; // -80 -72 -64 -56 -48 -40 -32 -24 -16 -8 0 8 16 24 32 40 48 56 64 72 (*Latitude) // Handle oddball zone 80-84 var GridZones = ['C', 'D', 'E', 'F', 'G', 'H', 'J', 'K', 'L', 'M', 'N', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'X']; var GridZonesDeg = [-80, -72, -64, -56, -48, -40, -32, -24, -16, -8, 0, 8, 16, 24, 32, 40, 48, 58, 64, 72, 80]; // TODO: This is approximate and actually depends on longitude too. var GridZonesNorthing = new Array(20); for(var i = 0 ; i < 20; i++) { GridZonesNorthing[i] = 110946.259 * GridZonesDeg[i]; // == 2 * PI * 6356752.3 * (latitude / 360.0) } // Grid Letters for UPS // 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 var XLetters = [ 'A', 'B', 'C', 'F', 'G', 'H', 'J', 'K', 'L', 'P', 'Q', 'R', 'S', 'T', 'U', 'X', 'Y', 'Z' ]; var YNLetters = [ 'H', 'J', 'K', 'L', 'M', 'N', 'P', 'A', 'B', 'C', 'D', 'E', 'F', 'G' ]; var YSLetters = [ 'N', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z', 'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'J', 'K', 'L', 'M' ]; // http://en.wikipedia.org/wiki/Great-circle_distance // http://en.wikipedia.org/wiki/Vincenty%27s_formulae this.llDistance = function(ll_start, ll_end) { var lat_s = ll_start.lat * Math.PI / 180; var lat_f = ll_end.lat * Math.PI / 180; var d_lon = (ll_end.lon - ll_start.lon) * Math.PI / 180; return( Math.atan2( Math.sqrt( Math.pow(Math.cos(lat_f) * Math.sin(d_lon),2) + Math.pow(Math.cos(lat_s)*Math.sin(lat_f) - Math.sin(lat_s)*Math.cos(lat_f)*Math.cos(d_lon),2)) , Math.sin(lat_s)*Math.sin(lat_f) + Math.cos(lat_s)*Math.cos(lat_f)*Math.cos(d_lon) ) ); } /* Returns a USNG String for a UTM point, and zone id's, and precision * utm_zone => 15 ; grid_zone => 'T' (calculated from latitude); * utm_easting => 491000, utm_northing => 49786000; precision => 2 */ this.fromUTM = function(utm_zone, grid_zone, utm_easting, utm_northing, precision) { var utm_zone; var grid_zone; var grid_square; var grid_easting; var grid_northing; var precision; var grid_square_set = utm_zone % 6; var ew_idx = Math.floor(utm_easting / 100000) - 1; // should be [100000, 900000] var ns_idx = Math.floor((utm_northing % 2000000) / 100000); // should [0, 10000000) => [0, 2000000) if(ns_idx < 0) { /* handle southern hemisphere */ ns_idx += 20; } switch(grid_square_set) { case 1: grid_square = EWLetters14[ew_idx] + NSLetters135[ns_idx]; break; case 2: grid_square = EWLetters25[ew_idx] + NSLetters246[ns_idx]; break; case 3: grid_square = EWLetters36[ew_idx] + NSLetters135[ns_idx]; break; case 4: grid_square = EWLetters14[ew_idx] + NSLetters246[ns_idx]; break; case 5: grid_square = EWLetters25[ew_idx] + NSLetters135[ns_idx]; break; case 0: // Calculates as zero, but is technically 6 */ grid_square = EWLetters36[ew_idx] + NSLetters246[ns_idx]; break; default: throw("USNG: can't get here"); } // Calc Easting and Northing integer to 100,000s place var easting = Math.floor(utm_easting % 100000).toString(); var northing = Math.floor(utm_northing % 100000); if(northing < 0) { // TODO: Does this switch to southing or is 1m south of the equator 99999? northing += 100000; //northing = -northing; } northing = northing.toString(); // Pad up to meter precision (5 digits) while(easting.length < 5) easting = '0' + easting; while(northing.length < 5) northing = '0' + northing; if(precision > 5) { // Calculate the fractional meter parts var digits = precision - 5; grid_easting = easting + (utm_easting % 1).toFixed(digits).substr(2,digits); grid_northing = northing + (utm_northing % 1).toFixed(digits).substr(2,digits); } else { // Remove unnecessary digits grid_easting = easting.substr(0, precision); grid_northing = northing.substr(0, precision); } var usng_string = String(utm_zone) + grid_zone + " " + grid_square + " " + grid_easting + " " + grid_northing; return(usng_string); } // Calculate UTM easting and northing from full, parsed USNG coordinate this.toUTMFromFullParsedUSNG = function(utm_zone, grid_zone, grid_square, grid_easting, grid_northing, precision, strict) { var utm_easting = 0; var utm_northing = 0; var grid_square_set = utm_zone % 6; var ns_grid; var ew_grid; switch(grid_square_set) { case 1: ns_grid = NSLetters135; ew_grid = EWLetters14; break; case 2: ns_grid = NSLetters246; ew_grid = EWLetters25; break; case 3: ns_grid = NSLetters135; ew_grid = EWLetters36; break; case 4: ns_grid = NSLetters246; ew_grid = EWLetters14; break; case 5: ns_grid = NSLetters135; ew_grid = EWLetters25; break; case 0: // grid_square_set will == 0, but it is technically group 6 ns_grid = NSLetters246; ew_grid = EWLetters36; break; default: throw("Can't get here"); } var ew_idx = ew_grid.indexOf(grid_square[0]); var ns_idx = ns_grid.indexOf(grid_square[1]); if(ew_idx == -1 || ns_idx == -1) throw("USNG: Invalid USNG 100km grid designator for UTM zone " + utm_zone + "."); //throw(RangeError("USNG: Invalid USNG 100km grid designator.")); utm_easting = ((ew_idx + 1) * 100000) + grid_easting; // Should be [100,000, 900,000] utm_northing = ((ns_idx + 0) * 100000) + grid_northing; // Should be [0, 2,000,000) // TODO: this really depends on easting too... // At this point know UTM zone, Grid Zone (min latitude), and easting // Right now this is look up table returns a max number based on lon == utm zone center var min_northing = GridZonesNorthing[GridZones.indexOf(grid_zone)]; // Unwrap northing to ~ [0, 10000000] utm_northing += 2000000 * Math.ceil((min_northing - utm_northing) / 2000000); // Check that the coordinate is within the utm zone and grid zone specified: var ll = utm_proj.invProj(utm_zone, utm_easting, utm_northing); var ll_utm_zone = Math.floor((ll.lon - (-180.0)) / 6.0) + 1; var ll_grid_zone = GridZones[Math.floor((ll.lat - (-80.0)) / 8)]; // If error from the above TODO mattered... then need to move north a grid if( ll_grid_zone != grid_zone) { utm_northing -= 2000000; ll = utm_proj.invProj(utm_zone, utm_easting, utm_northing); ll_utm_zone = Math.floor((ll.lon - (-180.0)) / 6.0) + 1; ll_grid_zone = GridZones[Math.floor((ll.lat - (-80.0)) / 8)]; } if(strict) { if(ll.lat > 84.0 || ll.lat < -80.0) throw("USNG: Latitude " + ll.lat + " outside valid UTM range."); if(ll_utm_zone != utm_zone) throw("USNG: calculated coordinate not in correct UTM zone! Supplied: "+utm_zone+grid_zone+" Calculated: "+ll_utm_zone+ll_grid_zone); if(ll_grid_zone != grid_zone) throw("USNG: calculated coordinate not in correct grid zone! Supplied: "+utm_zone+grid_zone+" Calculated: "+ll_utm_zone+ll_grid_zone); } else { // Loosen requirements to allow for grid extensions that don't introduce ambiguity. // "The UTM grid extends to 80°30'S and 84°30'N, providing a 30-minute overlap with the UPS grid." // -- http://earth-info.nga.mil/GandG/publications/tm8358.1/tr83581b.html Section 2-6.3.1 if(ll.lat > 84.5 || ll.lat < -79.5) throw("USNG: Latitude " + ll.lat + " outside valid UTM range."); // 100km grids E-W unique +/- 2 UTM zones of the correct UTM zone. // 100km grids unique for 800,000m in one UTM zone. // Thus, two limiting conditions for uniqueness: // UTM zone max width = 665,667m at equator => 800,000m is 1.2 UTM 6* zones wide at 0*N. => 67000m outside zone. // => utm_easting in [100,000, 900,000] (800,000m wide centered at 500,000m (false easting) // UTM zone min width = 63,801m at 84.5* N. => 12 UTM 6* zones. => 2 UTM zones. if(utm_easting < 100000 || utm_easting > 900000) throw("USNG: calculated coordinate not in correct UTM zone! Supplied: "+utm_zone+grid_zone+" Calculated: "+ll_utm_zone+ll_grid_zone); var utm_zone_diff = Math.abs(ll_utm_zone - utm_zone); if(utm_zone_diff > 2 && utm_zone_diff < 58) // utm_zone wraps 1..60,1 throw("USNG: calculated coordinate not in correct UTM zone! Supplied: "+utm_zone+grid_zone+" Calculated: "+ll_utm_zone+ll_grid_zone); // 100km grids N-S unique +/- 2,000,000 meters // A grid zone is roughly 887,570 meters N-S // => unique +/- 1 grid zone. var ll_idx = NSLetters135.indexOf(ll_grid_zone); // 135 or 246 doesn't matter var gz_idx = NSLetters135.indexOf(grid_zone); // letters in same order and circular subtraction. var gz_diff = Math.abs(ll_idx - gz_idx); if(gz_diff > 1 && gz_diff < 19) throw("USNG: calculated coordinate not in correct grid zone! Supplied: "+utm_zone+grid_zone+" Calculated: "+ll_utm_zone+ll_grid_zone); } var usng_string = String(utm_zone) + grid_zone + " " + grid_square + " " + grid_easting + " " + grid_northing; return { zone : utm_zone, easting : utm_easting, northing : utm_northing, precision : precision, usng: usng_string }; } /* Method to convert a USNG coordinate string into a NAD83/WGS84 LonLat Point * First parameter: usng = A valid USNG coordinate string (possibly truncated) * Possible cases: * Full USNG: 14TPU3467 * Truncated: TPU3467 * Truncated: PU3467 * Truncated: 3467 * Truncated: 14TPU * Truncated: 14T * Truncated: PU * Second parameter: a LonLat point to use to disambiguate a truncated USNG point * Returns: The LonLat point */ this.toUTM = function(usng, initial_lonlat, strict) { // Parse USNG into component parts var easting = 0; var northing = 0; var precision = 0; var digits = ""; /* don't really need this if using call to parsed... */ var grid_square = null; var grid_zone = null; var utm_zone = null; // Remove Whitespace (shouldn't be any) usng = usng.replace(/ /g, ""); // Strip Coordinate values off of end, if any // This will be any trailing digits. re = new RegExp("([0-9]+)$"); fields = re.exec(usng); if(fields) { digits = fields[0]; precision = digits.length / 2; // TODO: throw an error if #digits is odd. var scale_factor = Math.pow(10, (5 - precision)); // 1 digit => 10k place, 2 digits => 1k ... easting = Number(digits.substr(0, precision)) * scale_factor; northing = Number(digits.substr(precision, precision)) * scale_factor; } usng = usng.substr(0, usng.length-(precision*2)); // Get 100km Grid Designator, if any var re = new RegExp("([A-Z][A-Z]$)"); var fields = re.exec(usng); if(fields) { grid_square = fields[0]; } usng = usng.substr(0, usng.length - 2); // Get UTM and Grid Zone re = new RegExp("([0-9]+)([A-Z])"); fields = re.exec(usng); if(fields) { utm_zone = fields[1]; grid_zone = fields[2]; } // Allow the number-less A,B,Y,Z UPS grid zones if(!utm_zone) { re = new RegExp("([A-Z])"); fields = re.exec(usng); if(fields) grid_zone = fields[1]; } // Use lonlat Point as approx Location to fill in missing prefix info // Note: actual prefix need not be the same as that of the llPoint (we could cross 100km grid squares, utm zones, etc.) // Our job is to find the closest point to the llPoint given what we know about the USNG point. // Calculate the UTM zone, easting and northing from what we know /* Method: we can only guess missing prefix information so our cases are: * We have everything (14TPU) * We are missing the UTM zone (PU) * We are missing the UTM zone and the grid designator * TODO: Need to throw an exception if utm_zone and no grid_zone as invalid * TODO: Also need to throw an exception if don't have at least one of grid_zone and coordinate...maybe * TODO: Error if grid_zone is not in GridZones */ if(utm_zone && grid_zone && grid_square) { // We have everything so there is nothing more to do, UTM. } else if((grid_zone == "A" || grid_zone == "B" || grid_zone == "Y" || grid_zone == "Z") && grid_square) { // We have everything so there is nothing more to do, UPS. } else if(grid_square && initial_lonlat) { // We need to find the utm_zone and grid_zone // We know the grid zone so first we need to find the closest matching grid zone // to the initial point. Then add in the easting and northing (if any). //throw("USNG: Truncated coordinate support not implemented"); // Linear search all possible points (TODO: try to put likely guesses near top of list) var min_arc_distance = 1000; var min_utm_zone = null; var min_grid_zone = null; var ll_utm_zone = Math.floor((initial_lonlat.lon - (-180.0)) / 6.0) + 1; var ll_grid_zone_idx = Math.floor((initial_lonlat.lat - (-80.0)) / 8); // Check the min ranges that need to be searched based on the spec. // Need to wrap UTM zones mod 60 for(utm_zone = ll_utm_zone - 1; utm_zone <= ll_utm_zone+1; utm_zone++) { // still true at 80*? for(var grid_zone_idx = 0; grid_zone_idx < 20; grid_zone_idx++) { grid_zone = GridZones[grid_zone_idx]; try { var result = this.toLonLat((utm_zone%60) + grid_zone + grid_square + digits, null, true); // usng should be [A-Z][A-Z][0-9]+ var arc_distance = this.llDistance(initial_lonlat, result); //console.log(utm_zone + grid_zone + grid_square + digits + " " + arc_distance); if(arc_distance < min_arc_distance) { min_arc_distance = arc_distance; min_utm_zone = utm_zone % 60; min_grid_zone = grid_zone; } } catch(e) { //console.log("USNG: upstream: "+e); // catch range errors and ignore } } } // Search UPS zones var ups_zones; if(initial_lonlat.lat > 0) ups_zones = ['Y', 'Z']; else ups_zones = ['A', 'B']; for(var grid_zone_idx in ups_zones) { grid_zone = ups_zones[grid_zone_idx]; try { var result = this.toLonLat(grid_zone + grid_square + digits, null, true); // usng should be [A-Z][A-Z][0-9]+ var arc_distance = this.llDistance(initial_lonlat, result); //console.log(grid_zone + grid_square + digits + " " + arc_distance); if(arc_distance < min_arc_distance) { min_arc_distance = arc_distance; min_utm_zone = null; min_grid_zone = grid_zone; } } catch(e) { //console.log("USNG: upstream: "+e); // catch range errors and ignore } } if(min_grid_zone) { utm_zone = min_utm_zone; grid_zone = min_grid_zone; } else { throw("USNG: Couldn't find a match"); } } else if(initial_lonlat) { // We need to find the utm_zone, grid_zone and 100km grid designator // Find the closest grid zone within the specified easting and northing // Note: may cross UTM zone boundaries! // Linear search all possible points (TODO: try to put likely guesses near top of list) var min_arc_distance = 1000; var min_utm_zone = null; var min_grid_zone = null; var min_grid_square = null; var ll_utm_zone = Math.floor((initial_lonlat.lon - (-180.0)) / 6.0) + 1; var ll_grid_zone_idx = Math.floor((initial_lonlat.lat - (-80.0)) / 8); // Check the min ranges that need to be searched based on the spec. for(utm_zone = ll_utm_zone-1; utm_zone <= ll_utm_zone+1; utm_zone++) { // still true at 80*? for(var grid_zone_idx = ll_grid_zone_idx - 1; grid_zone_idx <= ll_grid_zone_idx + 1; grid_zone_idx++) { grid_zone = GridZones[grid_zone_idx]; var grid_square_set = utm_zone % 6; var ns_grid; var ew_grid; switch(grid_square_set) { case 1: ns_grid = NSLetters135; ew_grid = EWLetters14; break; case 2: ns_grid = NSLetters246; ew_grid = EWLetters25; break; case 3: ns_grid = NSLetters135; ew_grid = EWLetters36; break; case 4: ns_grid = NSLetters246; ew_grid = EWLetters14; break; case 5: ns_grid = NSLetters135; ew_grid = EWLetters25; break; case 0: // grid_square_set will == 0, but it is technically group 6 ns_grid = NSLetters246; ew_grid = EWLetters36; break; default: throw("Can't get here"); } //console.log(utm_zone + grid_zone); for(var ns_idx = 0; ns_idx < 20; ns_idx++) { for(var ew_idx = 0; ew_idx < 8; ew_idx++) { try { grid_square = ew_grid[ew_idx]+ns_grid[ns_idx]; var result = this.toLonLat((utm_zone%60) + grid_zone + grid_square + digits, null, true); // usng should be [A-Z][A-Z][0-9]+ var arc_distance = this.llDistance(initial_lonlat, result); //console.log(utm_zone + grid_zone + grid_square + digits + " " + arc_distance); if(arc_distance < min_arc_distance) { min_arc_distance = arc_distance; min_utm_zone = utm_zone % 60; min_grid_zone = grid_zone; min_grid_square = grid_square; } } catch(e) { //console.log("USNG: upstream: "+e); // catch range errors and ignore } } } } } // Search UPS zones var ups_zones; var y_zones; var y_max; if(initial_lonlat.lat > 0) { ups_zones = ['Y', 'Z']; y_zones = YNLetters; y_max = 14; } else { ups_zones = ['A', 'B']; y_zones = YSLetters; y_max = 24; } for(var grid_zone_idx in ups_zones) { grid_zone = ups_zones[grid_zone_idx]; for(var y_idx = 0; y_idx < y_max; y_idx++) { for(var x_idx = 0; x_idx < 18; x_idx++) { try { grid_square = XLetters[x_idx]+y_zones[y_idx]; var result = this.toLonLat(grid_zone + grid_square + digits, null, true); // usng should be [A-Z][A-Z][0-9]+ var arc_distance = this.llDistance(initial_lonlat, result); //console.log(grid_zone + grid_square + digits + " " + arc_distance); if(arc_distance < min_arc_distance) { min_arc_distance = arc_distance; min_utm_zone = null; min_grid_zone = grid_zone; min_grid_square = grid_square; } } catch(e) { //console.log("USNG: upstream: "+e); // catch range errors and ignore } } } } if(min_grid_zone) { utm_zone = min_utm_zone; grid_zone = min_grid_zone; grid_square = min_grid_square; } else { throw("USNG: Couldn't find a match"); } } else { throw("USNG: Not enough information to locate point."); } if(grid_zone == "A" || grid_zone == "B" || grid_zone == "Y" || grid_zone == "Z") return(this.toUPSFromFullParsedUSNG(grid_zone, grid_square, easting, northing, precision)); else return(this.toUTMFromFullParsedUSNG(utm_zone, grid_zone, grid_square, easting, northing, precision, strict)); } this.fromUPS = function(grid_zone, ups_x, ups_y, precision) { if(! ((grid_zone == "A") || (grid_zone == "B") || (grid_zone == "Y") || (grid_zone == "Z"))) throw( "UPS only valid in zones A, B, Y, and Z" ); var grid_square; var grid_square_x_idx = Math.floor((ups_x - 2000000) / 100000); var grid_square_y_idx = Math.floor((ups_y - 2000000) / 100000); if(grid_square_x_idx < 0) grid_square_x_idx += 18; if(grid_zone == "A" || grid_zone == "B") { // south if(grid_square_y_idx < 0) grid_square_y_idx += 24; grid_square = XLetters[grid_square_x_idx] + YSLetters[grid_square_y_idx]; } else { // north if(grid_square_y_idx < 0) grid_square_y_idx += 14; grid_square = XLetters[grid_square_x_idx] + YNLetters[grid_square_y_idx]; } // Calc X and Y integer to 100,000s place var x = Math.floor(ups_x % 100000).toString(); var y = Math.floor(ups_y % 100000).toString(); // Pad up to meter precision (5 digits) while(x.length < 5) x = '0' + x; while(y.length < 5) y = '0' + y; let grid_x, grid_y; if(precision > 5) { // Calculate the fractional meter parts var digits = precision - 5; grid_x = x + (ups_x % 1).toFixed(digits).substr(2,digits); grid_y = y + (ups_y % 1).toFixed(digits).substr(2,digits); } else { // Remove unnecessary digits grid_x = x.substr(0, precision); grid_y = y.substr(0, precision); } return grid_zone + " " + grid_square + " " + grid_x + " " + grid_y; } this.toUPSFromFullParsedUSNG = function(grid_zone, grid_square, grid_x, grid_y, precision) { if(!Proj4js) throw("USNG: Zones A,B,Y, and Z require Proj4js."); /* Start at the pole */ var ups_x = 2000000; var ups_y = 2000000; /* Offset based on 100km grid square */ var x_idx = XLetters.indexOf(grid_square[0]); if(x_idx < 0) throw("USNG: Invalid grid square."); var y_idx; switch(grid_zone) { case 'A': // South West half-hemisphere x_idx = x_idx - 18; case 'B': // South East half-hemisphere y_idx = YSLetters.indexOf(grid_square[1]); if(x_idx < -12 || x_idx > 11 || y_idx < 0) throw("USNG: Invalid grid square."); if(y_idx > 11) y_idx = y_idx - 24; break; case 'Y': // North West half-hemisphere x_idx = x_idx - 18; case 'Z': // North East half-hemisphere y_idx = YNLetters.indexOf(grid_square[1]); if(x_idx < -7 || x_idx > 6 || y_idx < 0) throw("USNG: Invalid grid square."); if(y_idx > 6) y_idx = y_idx - 14; break; default: throw( "UPS only valid in zones A, B, Y, and Z" ); } //console.log(x_idx, y_idx); ups_x += x_idx * 100000; ups_y += y_idx * 100000; /* Offset based on grid_x,y */ ups_x += grid_x; ups_y += grid_y; // Check that the coordinate is within the ups zone and grid zone specified: var ll = { x: ups_x, y: ups_y }; if(grid_zone == "A" || grid_zone == "B") { Proj4js.transform(south_proj, ll_proj, ll); if(ll.y > -80.0) throw("USNG: Grid Zone A or B but Latitude > -80."); } else { Proj4js.transform(north_proj, ll_proj, ll); if(ll.y < 84.0) throw("USNG: Grid Zone Y or Z but Latitude < 84."); } var usng_string = grid_zone + " " + grid_square + " " + grid_x + " " + grid_y; return { grid_zone : grid_zone, x : ups_x, y : ups_y, precision : precision, usng: usng_string }; } // Converts a lat, lon point (NAD83) into a USNG coordinate string // of precision where precision indicates the number of digits used // per coordinate (0 = 100,000m, 1 = 10km, 2 = 1km, 3 = 100m, 4 = 10m, ...) this.fromLonLat = function(lonlat, precision) { var lon = lonlat.lon; var lat = lonlat.lat; // Normalize Latitude and Longitude while(lon < -180) { lon += 180; } while(lon > 180) { lon -= 180; } // Calculate UTM Zone number from Longitude // -180 = 180W is grid 1... increment every 6 degrees going east // Note [-180, -174) is in grid 1, [-174,-168) is 2, [174, 180) is 60 var utm_zone = Math.floor((lon - (-180.0)) / 6.0) + 1; // Calculate USNG Grid Zone Designation from Latitude // Starts at -80 degrees and is in 8 degree increments if(! ((lat > -80) && (lat < 84) )) { if(!north_proj) throw("USNG: Latitude must be between -80 and 84. (Zones A,B,Y, and Z require Proj4js.)"); var grid_zone; var ups_pt = new Proj4js.Point( lon, lat ); if( lat > 0 ) { Proj4js.transform( ll_proj, north_proj, ups_pt ); grid_zone = (lon < 0) ? "Y":"Z"; } else { Proj4js.transform( ll_proj, south_proj, ups_pt ); grid_zone = (lon < 0) ? "A":"B"; } return this.fromUPS(grid_zone, ups_pt.x, ups_pt.y, precision); } var grid_zone = GridZones[Math.floor((lat - (-80.0)) / 8)]; var utm_pt = utm_proj.proj(utm_zone, lon, lat); return this.fromUTM(utm_zone, grid_zone, utm_pt.utm_easting, utm_pt.utm_northing, precision); } this.toLonLat = function(usng, initial_lonlat, strict) { var result = this.toUTM(usng, initial_lonlat, strict); var grid_zone = result.grid_zone; var ll; //console.log(result); if(south_proj && (grid_zone == "A" || grid_zone == "B")) { var pt = {x: result.x, y: result.y}; Proj4js.transform( south_proj, ll_proj, pt ); ll = { lon: pt.x, lat: pt.y, precision: result.precision, usng: result.usng }; } else if(north_proj && (grid_zone == "Y" || grid_zone == "Z")) { var pt = {x: result.x, y: result.y}; Proj4js.transform( north_proj, ll_proj, pt ); ll = { lon: pt.x, lat: pt.y, precision: result.precision, usng: result.usng }; } else { ll = utm_proj.invProj(result.zone, result.easting, result.northing); ll.precision = result.precision; ll.usng = result.usng; } return (ll); } this.UTM = function() { // Functions to convert between lat,lon and utm. Derived from visual basic // routines from Craig Perault. This assumes a NAD83 datum. // constants var MajorAxis = 6378137.0; var MinorAxis = 6356752.3; var Ecc = (MajorAxis * MajorAxis - MinorAxis * MinorAxis) / (MajorAxis * MajorAxis); var Ecc2 = Ecc / (1.0 - Ecc); var K0 = 0.9996; var E4 = Ecc * Ecc; var E6 = Ecc * E4; var degrees2radians = Math.PI / 180.0; // Computes the meridian distance for the GRS-80 Spheroid. // See equation 3-22, USGS Professional Paper 1395. function meridianDist(lat) { var c1 = MajorAxis * (1 - Ecc / 4 - 3 * E4 / 64 - 5 * E6 / 256); var c2 = -MajorAxis * (3 * Ecc / 8 + 3 * E4 / 32 + 45 * E6 / 1024); var c3 = MajorAxis * (15 * E4 / 256 + 45 * E6 / 1024); var c4 = -MajorAxis * 35 * E6 / 3072; return(c1 * lat + c2 * Math.sin(lat * 2) + c3 * Math.sin(lat * 4) + c4 * Math.sin(lat * 6)); } // Convert lat/lon (given in decimal degrees) to UTM, given a particular UTM zone. this.proj = function(zone, in_lon, in_lat) { var centeralMeridian = -((30 - zone) * 6 + 3) * degrees2radians; var lat = in_lat * degrees2radians; var lon = in_lon * degrees2radians; var latSin = Math.sin(lat); var latCos = Math.cos(lat); var latTan = latSin / latCos; var latTan2 = latTan * latTan; var latTan4 = latTan2 * latTan2; var N = MajorAxis / Math.sqrt(1 - Ecc * (latSin*latSin)); var c = Ecc2 * latCos*latCos; var a = latCos * (lon - centeralMeridian); var m = meridianDist(lat); var temp5 = 1.0 - latTan2 + c; var temp6 = 5.0 - 18.0 * latTan2 + latTan4 + 72.0 * c - 58.0 * Ecc2; var temp11 = Math.pow(a, 5); var x = K0 * N * (a + (temp5 * Math.pow(a, 3)) / 6.0 + temp6 * temp11 / 120.0) + 500000; var temp7 = (5.0 - latTan2 + 9.0 * c + 4.0 * (c*c)) * Math.pow(a,4) / 24.0; var temp8 = 61.0 - 58.0 * latTan2 + latTan4 + 600.0 * c - 330.0 * Ecc2; var temp9 = temp11 * a / 720.0; var y = K0 * (m + N * latTan * ((a * a) / 2.0 + temp7 + temp8 * temp9)) return( { utm_zone: zone, utm_easting : x, utm_northing : y } ); } // Convert UTM coordinates (given in meters) to Lat/Lon (in decimal degrees), given a particular UTM zone. this.invProj = function(zone, easting, northing) { var centeralMeridian = -((30 - zone) * 6 + 3) * degrees2radians; var temp = Math.sqrt(1.0 - Ecc); var ecc1 = (1.0 - temp) / (1.0 + temp); var ecc12 = ecc1 * ecc1; var ecc13 = ecc1 * ecc12; var ecc14 = ecc12 * ecc12; easting = easting - 500000.0; var m = northing / K0; var um = m / (MajorAxis * (1.0 - (Ecc / 4.0) - 3.0 * (E4 / 64.0) - 5.0 * (E6 / 256.0))); var temp8 = (1.5 * ecc1) - (27.0 / 32.0) * ecc13; var temp9 = ((21.0 / 16.0) * ecc12) - ((55.0 / 32.0) * ecc14); var latrad1 = um + temp8 * Math.sin(2 * um) + temp9 * Math.sin(4 * um) + (151.0 * ecc13 / 96.0) * Math.sin(6.0 * um); var latsin1 = Math.sin(latrad1); var latcos1 = Math.cos(latrad1); var lattan1 = latsin1 / latcos1; var n1 = MajorAxis / Math.sqrt(1.0 - Ecc * latsin1 * latsin1); var t2 = lattan1 * lattan1; var c1 = Ecc2 * latcos1 * latcos1; var temp20 = (1.0 - Ecc * latsin1 * latsin1); var r1 = MajorAxis * (1.0 - Ecc) / Math.sqrt(temp20 * temp20 * temp20); var d1 = easting / (n1*K0); var d2 = d1 * d1; var d3 = d1 * d2; var d4 = d2 * d2; var d5 = d1 * d4; var d6 = d3 * d3; var t12 = t2 * t2; var c12 = c1 * c1; var temp1 = n1 * lattan1 / r1; var temp2 = 5.0 + 3.0 * t2 + 10.0 * c1 - 4.0 * c12 - 9.0 * Ecc2; var temp4 = 61.0 + 90.0 * t2 + 298.0 * c1 + 45.0 * t12 - 252.0 * Ecc2 - 3.0 * c12; var temp5 = (1.0 + 2.0 * t2 + c1) * d3 / 6.0; var temp6 = 5.0 - 2.0 * c1 + 28.0 * t2 - 3.0 * c12 + 8.0 * Ecc2 + 24.0 * t12; var lat = (latrad1 - temp1 * (d2 / 2.0 - temp2 * (d4 / 24.0) + temp4 * d6 / 720.0)) * 180 / Math.PI; var lon = (centeralMeridian + (d1 - temp5 + temp6 * d5 / 120.0) / latcos1) * 180 / Math.PI; //easting = easting + 500000.0; return ({ lon: lon, lat: lat}); } } var utm_proj = new this.UTM(); // Use Proj4JS for Universal Polar Stereographic if available. var north_proj; var south_proj; var ll_proj; if(typeof Proj4js == "object") { Proj4js.defs["EPSG:32661"] = "+proj=stere +lat_0=90 +lat_ts=90 +lon_0=0 +k=0.994 +x_0=2000000 +y_0=2000000 +ellps=WGS84 +datum=WGS84 +units=m +no_defs"; Proj4js.defs["EPSG:32761"] = "+proj=stere +lat_0=-90 +lat_ts=-90 +lon_0=0 +k=0.994 +x_0=2000000 +y_0=2000000 +ellps=WGS84 +datum=WGS84 +units=m +no_defs"; Proj4js.defs["EPSG:4326"] = "+proj=longlat +ellps=WGS84 +datum=WGS84 +no_defs"; north_proj = new Proj4js.Proj('EPSG:32661'); south_proj = new Proj4js.Proj('EPSG:32761'); ll_proj = new Proj4js.Proj('EPSG:4326'); } }