/** * UTM (Universal Transverse Mercator) coordinate conversion utilities * Converts WGS84 coordinates (latitude/longitude) to UTM coordinates */ export class UTMConverter { // WGS84 ellipsoid parameters static WGS84_A = 6378137.0; // semi-major axis in meters static WGS84_F = 1 / 298.257223563; // flattening static WGS84_E = 0.081819191; // eccentricity static WGS84_ESQ = 0.00669438; // eccentricity squared // UTM parameters static UTM_SCALE_FACTOR = 0.9996; static UTM_FALSE_EASTING = 500000; // in meters static UTM_FALSE_NORTHING_N = 0; // for northern hemisphere static UGM_FALSE_NORTHING_S = 10000000; // for southern hemisphere /** * Convert WGS84 coordinates to UTM * @param {number} longitude - Longitude in decimal degrees * @param {number} latitude - Latitude in decimal degrees * @returns {Object} UTM coordinates with zone information */ static toUTM(longitude, latitude) { // Validate input coordinates if (longitude < -180 || longitude > 180) { throw new Error(`Longitude ${longitude} is out of valid range [-180, 180]`); } if (latitude < -90 || latitude > 90) { throw new Error(`Latitude ${latitude} is out of valid range [-90, 90]`); } const zone = UTMConverter.getUTMZone(longitude, latitude); const hemisphere = latitude >= 0 ? "N" : "S"; // Convert to radians const latRad = (latitude * Math.PI) / 180; const lonRad = (longitude * Math.PI) / 180; // Central meridian for the zone (in radians) const lonOrigin = (((zone - 1) * 6 - 180 + 3) * Math.PI) / 180; const dLon = lonRad - lonOrigin; // Ellipsoid constants const e = UTMConverter.WGS84_E; const e2 = UTMConverter.WGS84_ESQ; const e4 = e2 * e2; const e6 = e4 * e2; const N = UTMConverter.WGS84_A / Math.sqrt(1 - e2 * Math.sin(latRad) * Math.sin(latRad)); const T = Math.tan(latRad) * Math.tan(latRad); const C = (e2 * Math.cos(latRad) * Math.cos(latRad)) / (1 - e2); const A = Math.cos(latRad) * dLon; const M = UTMConverter.WGS84_A * ((1 - e2 / 4 - (3 * e4) / 64 - (5 * e6) / 256) * latRad - ((3 * e2) / 8 + (3 * e4) / 32 + (45 * e6) / 1024) * Math.sin(2 * latRad) + ((15 * e4) / 256 + (45 * e6) / 1024) * Math.sin(4 * latRad) - ((35 * e6) / 3072) * Math.sin(6 * latRad)); // Calculate easting const easting = UTMConverter.UTM_SCALE_FACTOR * N * (A + ((1 - T + C) * A * A * A) / 6 + ((5 - 18 * T + T * T + 72 * C - 58 * e2) * A * A * A * A * A) / 120) + UTMConverter.UTM_FALSE_EASTING; // Calculate northing const northing = UTMConverter.UTM_SCALE_FACTOR * (M + N * Math.tan(latRad) * ((A * A) / 2 + ((5 - T + 9 * C + 4 * C * C) * A * A * A * A) / 24 + ((61 - 58 * T + T * T + 600 * C - 330 * e2) * A * A * A * A * A * A) / 720)); // Adjust northing for southern hemisphere const adjustedNorthing = hemisphere === "S" ? northing + UTMConverter.UGM_FALSE_NORTHING_S : northing; return { datum: "WGS84", easting: Math.round(easting * 100) / 100, // Round to 2 decimal places hemisphere: hemisphere, northing: Math.round(adjustedNorthing * 100) / 100, zone: zone, }; } /** * Determine UTM zone from longitude and latitude * @param {number} longitude - Longitude in decimal degrees * @param {number} latitude - Latitude in decimal degrees * @returns {number} UTM zone number */ static getUTMZone(longitude, latitude) { // Special zones for Norway and Svalbard if (latitude >= 56.0 && latitude < 64.0 && longitude >= 3.0 && longitude < 12.0) { return 32; } // Special zones for Svalbard if (latitude >= 72.0 && latitude < 84.0) { if (longitude >= 0.0 && longitude < 9.0) return 31; if (longitude >= 9.0 && longitude < 21.0) return 33; if (longitude >= 21.0 && longitude < 33.0) return 35; if (longitude >= 33.0 && longitude < 42.0) return 37; } // Standard zone calculation let zone = Math.floor((longitude + 180) / 6) + 1; // Handle zones 31-37 for northern Europe special cases if (latitude >= 72.0 && latitude < 84.0 && longitude >= 0.0 && longitude < 42.0) { if (longitude < 9.0) zone = 31; else if (longitude < 21.0) zone = 33; else if (longitude < 33.0) zone = 35; else zone = 37; } return Math.min(Math.max(zone, 1), 60); } /** * Convert UTM coordinates back to WGS84 * @param {number} easting - UTM easting in meters * @param {number} northing - UTM northing in meters * @param {number} zone - UTM zone number * @param {string} hemisphere - 'N' for northern, 'S' for southern * @returns {Object} WGS84 coordinates {longitude, latitude} */ static fromUTM(easting, northing, zone, hemisphere = "N") { if (zone < 1 || zone > 60) { throw new Error(`UTM zone ${zone} is out of valid range [1, 60]`); } // Adjust northing for southern hemisphere const adjustedNorthing = hemisphere === "S" ? northing - UTMConverter.UGM_FALSE_NORTHING_S : northing; const e = UTMConverter.WGS84_E; const e2 = UTMConverter.WGS84_ESQ; const e4 = e2 * e2; const e6 = e4 * e2; // Remove false easting and scale factor const x = (easting - UTMConverter.UTM_FALSE_EASTING) / UTMConverter.UTM_SCALE_FACTOR; const y = adjustedNorthing / UTMConverter.UTM_SCALE_FACTOR; // Footprint latitude const M = y; const mu = M / (UTMConverter.WGS84_A * (1 - e2 / 4 - (3 * e4) / 64 - (5 * e6) / 256)); const e1 = (1 - Math.sqrt(1 - e2)) / (1 + Math.sqrt(1 - e2)); const fp = mu + ((3 * e1) / 2 - (27 * e1 * e1 * e1) / 32) * Math.sin(2 * mu) + ((21 * e1 * e1) / 16 - (55 * e1 * e1 * e1 * e1) / 32) * Math.sin(4 * mu) + ((151 * e1 * e1 * e1) / 96) * Math.sin(6 * mu); // Calculate latitude and longitude const C1 = (e2 * Math.cos(fp) * Math.cos(fp)) / (1 - e2); const T1 = Math.tan(fp) * Math.tan(fp); const N1 = UTMConverter.WGS84_A / Math.sqrt(1 - e2 * Math.sin(fp) * Math.sin(fp)); const R1 = (UTMConverter.WGS84_A * (1 - e2)) / (1 - e2 * Math.sin(fp) * Math.sin(fp)) ** 1.5; const D = x / N1; const lat = fp - ((N1 * Math.tan(fp)) / R1) * ((D * D) / 2 - ((5 + 3 * T1 + 10 * C1 - 4 * C1 * C1 - 9 * e2) * D * D * D * D) / 24 + ((61 + 90 * T1 + 298 * C1 + 45 * T1 * T1 - 252 * e2 - 3 * C1 * C1) * D * D * D * D * D * D) / 720); const lonOrigin = (((zone - 1) * 6 - 180 + 3) * Math.PI) / 180; const lon = lonOrigin + (D - ((1 + 2 * T1 + C1) * D * D * D) / 6 + ((5 - 2 * C1 + 28 * T1 - 3 * C1 * C1 + 8 * e2 + 24 * T1 * T1) * D * D * D * D * D) / 120) / Math.cos(fp); return { latitude: (lat * 180) / Math.PI, longitude: (lon * 180) / Math.PI, }; } /** * Convert Point geometry to UTM coordinates * @param {Point} point - Point geometry * @returns {Object} UTM coordinates */ static pointToUTM(point) { return UTMConverter.toUTM(point.lng, point.lat); } /** * Convert multiple coordinates to UTM * @param {Coordinate[]} coordinates - Array of [longitude, latitude] pairs * @returns {Object[]} Array of UTM coordinates */ static coordinatesToUTM(coordinates) { return coordinates.map((coord) => UTMConverter.toUTM(coord[0], coord[1])); } } // Convenience function for direct coordinate conversion /** * Convert WGS84 to UTM coordinates * @param {number} lng - Longitude * @param {number} lat - Latitude * @returns {Object} UTM coordinates {easting, northing, zone, hemisphere} */ export function wgs84ToUTM(lng, lat) { return UTMConverter.toUTM(lng, lat); } /** * Convert UTM to WGS84 coordinates * @param {number} easting - UTM easting * @param {number} northing - UTM northing * @param {number} zone - UTM zone * @param {string} hemisphere - 'N' or 'S' * @returns {Object} WGS84 coordinates {longitude, latitude} */ export function utmToWGS84(easting, northing, zone, hemisphere = "N") { return UTMConverter.fromUTM(easting, northing, zone, hemisphere); }