// // GEOGRAPHIC UTILITY FUNCTIONS // Great Circle calculation, Maidenhead grid calcs, etc. // // Calculate great circle bearing between two lat/lon points. function calcBearing(lat1, lon1, lat2, lon2) { lat1 *= Math.PI / 180; lon1 *= Math.PI / 180; lat2 *= Math.PI / 180; lon2 *= Math.PI / 180; var lonDelta = lon2 - lon1; var y = Math.sin(lonDelta) * Math.cos(lat2); var x = Math.cos(lat1) * Math.sin(lat2) - Math.sin(lat1) * Math.cos(lat2) * Math.cos(lonDelta); var bearing = Math.atan2(y, x); bearing = bearing * (180 / Math.PI); if ( bearing < 0 ) { bearing += 360; } return bearing; } // Convert a Maidenhead grid reference of arbitrary precision to the lat/long of the centre point of the square. // Returns null if the grid format is invalid. function latLonForGridCentre(grid) { let [lat, lon, latCellSize, lonCellSize] = latLonForGridSWCornerPlusSize(grid); if (lat != null && lon != null && latCellSize != null && lonCellSize != null) { return [lat + latCellSize / 2.0, lon + lonCellSize / 2.0]; } else { return null; } } // Convert a Maidenhead grid reference of arbitrary precision to lat/long, including in the result the size of the // lowest grid square. This is a utility method used by the main methods that return the centre, southwest, and // northeast coordinates of a grid square. // The return type is always an array of size 4. The elements in it are null if the grid format is invalid. function latLonForGridSWCornerPlusSize(grid) { // Make sure we are in upper case so our maths works. Case is arbitrary for Maidenhead references grid = grid.toUpperCase(); // Return null if our Maidenhead string is invalid or too short let len = grid.length; if (len <= 0 || (len % 2) !== 0) { return [null, null, null, null]; } let lat = 0.0; // aggregated latitude let lon = 0.0; // aggregated longitude let latCellSize = 10; // Size in degrees latitude of the current cell. Starts at 20 and gets smaller as the calculation progresses let lonCellSize = 20; // Size in degrees longitude of the current cell. Starts at 20 and gets smaller as the calculation progresses let latCellNo; // grid latitude cell number this time let lonCellNo; // grid longitude cell number this time // Iterate through blocks (two-character sections) for (let block = 0; block * 2 < len; block += 1) { if (block % 2 === 0) { // Letters in this block lonCellNo = grid.charCodeAt(block * 2) - 'A'.charCodeAt(0); latCellNo = grid.charCodeAt(block * 2 + 1) - 'A'.charCodeAt(0); // Bail if the values aren't in range. Allowed values are A-R (0-17) for the first letter block, or // A-X (0-23) thereafter. let maxCellNo = (block === 0) ? 17 : 23; if (latCellNo < 0 || latCellNo > maxCellNo || lonCellNo < 0 || lonCellNo > maxCellNo) { return [null, null, null, null]; } } else { // Numbers in this block lonCellNo = parseInt(grid.charAt(block * 2)); latCellNo = parseInt(grid.charAt(block * 2 + 1)); // Bail if the values aren't in range 0-9.. if (latCellNo < 0 || latCellNo > 9 || lonCellNo < 0 || lonCellNo > 9) { return [null, null, null, null]; } } // Aggregate the angles lat += latCellNo * latCellSize; lon += lonCellNo * lonCellSize; // Reduce the cell size for the next block, unless we are on the last cell. if (block * 2 < len - 2) { // Still have more work to do, so reduce the cell size if (block % 2 === 0) { // Just dealt with letters, next block will be numbers so cells will be 1/10 the current size latCellSize = latCellSize / 10.0; lonCellSize = lonCellSize / 10.0; } else { // Just dealt with numbers, next block will be letters so cells will be 1/24 the current size latCellSize = latCellSize / 24.0; lonCellSize = lonCellSize / 24.0; } } } // Offset back to (-180, -90) where the grid starts lon -= 180.0; lat -= 90.0; // Return nulls on maths errors if (isNaN(lat) || isNaN(lon) || isNaN(latCellSize) || isNaN(lonCellSize)) { return [null, null, null, null]; } return [lat, lon, latCellSize, lonCellSize]; }