var HALF_PI = Math.PI/2; var PJD_3PARAM = 1; var PJD_7PARAM = 2; var PJD_GRIDSHIFT = 3; var PJD_WGS84 = 4; // WGS84 or equivalent var PJD_NODATUM = 5; // WGS84 or equivalent var SEC_TO_RAD = 4.84813681109535993589914102357e-6; var AD_C = 1.0026000; var COS_67P5 = 0.38268343236508977; var datum = function(proj) { if (!(this instanceof datum)) { return new datum(proj); } this.datum_type = PJD_WGS84; //default setting if (!proj) { return; } if (proj.datumCode && proj.datumCode === 'none') { this.datum_type = PJD_NODATUM; } if (proj.datum_params) { this.datum_params = proj.datum_params.map(parseFloat); if (this.datum_params[0] !== 0 || this.datum_params[1] !== 0 || this.datum_params[2] !== 0) { this.datum_type = PJD_3PARAM; } if (this.datum_params.length > 3) { if (this.datum_params[3] !== 0 || this.datum_params[4] !== 0 || this.datum_params[5] !== 0 || this.datum_params[6] !== 0) { this.datum_type = PJD_7PARAM; this.datum_params[3] *= SEC_TO_RAD; this.datum_params[4] *= SEC_TO_RAD; this.datum_params[5] *= SEC_TO_RAD; this.datum_params[6] = (this.datum_params[6] / 1000000.0) + 1.0; } } } // DGR 2011-03-21 : nadgrids support this.datum_type = proj.grids ? PJD_GRIDSHIFT : this.datum_type; this.a = proj.a; //datum object also uses these values this.b = proj.b; this.es = proj.es; this.ep2 = proj.ep2; if (this.datum_type === PJD_GRIDSHIFT) { this.grids = proj.grids; } }; datum.prototype = { /****************************************************************/ // cs_compare_datums() // Returns TRUE if the two datums match, otherwise FALSE. compare_datums: function(dest) { if (this.datum_type !== dest.datum_type) { return false; // false, datums are not equal } else if (this.a !== dest.a || Math.abs(this.es - dest.es) > 0.000000000050) { // the tolerence for es is to ensure that GRS80 and WGS84 // are considered identical return false; } else if (this.datum_type === PJD_3PARAM) { return (this.datum_params[0] === dest.datum_params[0] && this.datum_params[1] === dest.datum_params[1] && this.datum_params[2] === dest.datum_params[2]); } else if (this.datum_type === PJD_7PARAM) { return (this.datum_params[0] === dest.datum_params[0] && this.datum_params[1] === dest.datum_params[1] && this.datum_params[2] === dest.datum_params[2] && this.datum_params[3] === dest.datum_params[3] && this.datum_params[4] === dest.datum_params[4] && this.datum_params[5] === dest.datum_params[5] && this.datum_params[6] === dest.datum_params[6]); } else if (this.datum_type === PJD_GRIDSHIFT || dest.datum_type === PJD_GRIDSHIFT) { //alert("ERROR: Grid shift transformations are not implemented."); //return false //DGR 2012-07-29 lazy ... return this.nadgrids === dest.nadgrids; } else { return true; // datums are equal } }, // cs_compare_datums() /* * The function Convert_Geodetic_To_Geocentric converts geodetic coordinates * (latitude, longitude, and height) to geocentric coordinates (X, Y, Z), * according to the current ellipsoid parameters. * * Latitude : Geodetic latitude in radians (input) * Longitude : Geodetic longitude in radians (input) * Height : Geodetic height, in meters (input) * X : Calculated Geocentric X coordinate, in meters (output) * Y : Calculated Geocentric Y coordinate, in meters (output) * Z : Calculated Geocentric Z coordinate, in meters (output) * */ geodetic_to_geocentric: function(p) { var Longitude = p.x; var Latitude = p.y; var Height = p.z ? p.z : 0; //Z value not always supplied var X; // output var Y; var Z; var Error_Code = 0; // GEOCENT_NO_ERROR; var Rn; /* Earth radius at location */ var Sin_Lat; /* Math.sin(Latitude) */ var Sin2_Lat; /* Square of Math.sin(Latitude) */ var Cos_Lat; /* Math.cos(Latitude) */ /* ** Don't blow up if Latitude is just a little out of the value ** range as it may just be a rounding issue. Also removed longitude ** test, it should be wrapped by Math.cos() and Math.sin(). NFW for PROJ.4, Sep/2001. */ if (Latitude < -HALF_PI && Latitude > -1.001 * HALF_PI) { Latitude = -HALF_PI; } else if (Latitude > HALF_PI && Latitude < 1.001 * HALF_PI) { Latitude = HALF_PI; } else if ((Latitude < -HALF_PI) || (Latitude > HALF_PI)) { /* Latitude out of range */ //..reportError('geocent:lat out of range:' + Latitude); return null; } if (Longitude > Math.PI) { Longitude -= (2 * Math.PI); } Sin_Lat = Math.sin(Latitude); Cos_Lat = Math.cos(Latitude); Sin2_Lat = Sin_Lat * Sin_Lat; Rn = this.a / (Math.sqrt(1.0e0 - this.es * Sin2_Lat)); X = (Rn + Height) * Cos_Lat * Math.cos(Longitude); Y = (Rn + Height) * Cos_Lat * Math.sin(Longitude); Z = ((Rn * (1 - this.es)) + Height) * Sin_Lat; p.x = X; p.y = Y; p.z = Z; return Error_Code; }, // cs_geodetic_to_geocentric() geocentric_to_geodetic: function(p) { /* local defintions and variables */ /* end-criterium of loop, accuracy of sin(Latitude) */ var genau = 1e-12; var genau2 = (genau * genau); var maxiter = 30; var P; /* distance between semi-minor axis and location */ var RR; /* distance between center and location */ var CT; /* sin of geocentric latitude */ var ST; /* cos of geocentric latitude */ var RX; var RK; var RN; /* Earth radius at location */ var CPHI0; /* cos of start or old geodetic latitude in iterations */ var SPHI0; /* sin of start or old geodetic latitude in iterations */ var CPHI; /* cos of searched geodetic latitude */ var SPHI; /* sin of searched geodetic latitude */ var SDPHI; /* end-criterium: addition-theorem of sin(Latitude(iter)-Latitude(iter-1)) */ var At_Pole; /* indicates location is in polar region */ var iter; /* # of continous iteration, max. 30 is always enough (s.a.) */ var X = p.x; var Y = p.y; var Z = p.z ? p.z : 0.0; //Z value not always supplied var Longitude; var Latitude; var Height; At_Pole = false; P = Math.sqrt(X * X + Y * Y); RR = Math.sqrt(X * X + Y * Y + Z * Z); /* special cases for latitude and longitude */ if (P / this.a < genau) { /* special case, if P=0. (X=0., Y=0.) */ At_Pole = true; Longitude = 0.0; /* if (X,Y,Z)=(0.,0.,0.) then Height becomes semi-minor axis * of ellipsoid (=center of mass), Latitude becomes PI/2 */ if (RR / this.a < genau) { Latitude = HALF_PI; Height = -this.b; return; } } else { /* ellipsoidal (geodetic) longitude * interval: -PI < Longitude <= +PI */ Longitude = Math.atan2(Y, X); } /* -------------------------------------------------------------- * Following iterative algorithm was developped by * "Institut for Erdmessung", University of Hannover, July 1988. * Internet: www.ife.uni-hannover.de * Iterative computation of CPHI,SPHI and Height. * Iteration of CPHI and SPHI to 10**-12 radian resp. * 2*10**-7 arcsec. * -------------------------------------------------------------- */ CT = Z / RR; ST = P / RR; RX = 1.0 / Math.sqrt(1.0 - this.es * (2.0 - this.es) * ST * ST); CPHI0 = ST * (1.0 - this.es) * RX; SPHI0 = CT * RX; iter = 0; /* loop to find sin(Latitude) resp. Latitude * until |sin(Latitude(iter)-Latitude(iter-1))| < genau */ do { iter++; RN = this.a / Math.sqrt(1.0 - this.es * SPHI0 * SPHI0); /* ellipsoidal (geodetic) height */ Height = P * CPHI0 + Z * SPHI0 - RN * (1.0 - this.es * SPHI0 * SPHI0); RK = this.es * RN / (RN + Height); RX = 1.0 / Math.sqrt(1.0 - RK * (2.0 - RK) * ST * ST); CPHI = ST * (1.0 - RK) * RX; SPHI = CT * RX; SDPHI = SPHI * CPHI0 - CPHI * SPHI0; CPHI0 = CPHI; SPHI0 = SPHI; } while (SDPHI * SDPHI > genau2 && iter < maxiter); /* ellipsoidal (geodetic) latitude */ Latitude = Math.atan(SPHI / Math.abs(CPHI)); p.x = Longitude; p.y = Latitude; p.z = Height; return p; }, // cs_geocentric_to_geodetic() /** Convert_Geocentric_To_Geodetic * The method used here is derived from 'An Improved Algorithm for * Geocentric to Geodetic Coordinate Conversion', by Ralph Toms, Feb 1996 */ geocentric_to_geodetic_noniter: function(p) { var X = p.x; var Y = p.y; var Z = p.z ? p.z : 0; //Z value not always supplied var Longitude; var Latitude; var Height; var W; /* distance from Z axis */ var W2; /* square of distance from Z axis */ var T0; /* initial estimate of vertical component */ var T1; /* corrected estimate of vertical component */ var S0; /* initial estimate of horizontal component */ var S1; /* corrected estimate of horizontal component */ var Sin_B0; /* Math.sin(B0), B0 is estimate of Bowring aux variable */ var Sin3_B0; /* cube of Math.sin(B0) */ var Cos_B0; /* Math.cos(B0) */ var Sin_p1; /* Math.sin(phi1), phi1 is estimated latitude */ var Cos_p1; /* Math.cos(phi1) */ var Rn; /* Earth radius at location */ var Sum; /* numerator of Math.cos(phi1) */ var At_Pole; /* indicates location is in polar region */ X = parseFloat(X); // cast from string to float Y = parseFloat(Y); Z = parseFloat(Z); At_Pole = false; if (X !== 0.0) { Longitude = Math.atan2(Y, X); } else { if (Y > 0) { Longitude = HALF_PI; } else if (Y < 0) { Longitude = -HALF_PI; } else { At_Pole = true; Longitude = 0.0; if (Z > 0.0) { /* north pole */ Latitude = HALF_PI; } else if (Z < 0.0) { /* south pole */ Latitude = -HALF_PI; } else { /* center of earth */ Latitude = HALF_PI; Height = -this.b; return; } } } W2 = X * X + Y * Y; W = Math.sqrt(W2); T0 = Z * AD_C; S0 = Math.sqrt(T0 * T0 + W2); Sin_B0 = T0 / S0; Cos_B0 = W / S0; Sin3_B0 = Sin_B0 * Sin_B0 * Sin_B0; T1 = Z + this.b * this.ep2 * Sin3_B0; Sum = W - this.a * this.es * Cos_B0 * Cos_B0 * Cos_B0; S1 = Math.sqrt(T1 * T1 + Sum * Sum); Sin_p1 = T1 / S1; Cos_p1 = Sum / S1; Rn = this.a / Math.sqrt(1.0 - this.es * Sin_p1 * Sin_p1); if (Cos_p1 >= COS_67P5) { Height = W / Cos_p1 - Rn; } else if (Cos_p1 <= -COS_67P5) { Height = W / -Cos_p1 - Rn; } else { Height = Z / Sin_p1 + Rn * (this.es - 1.0); } if (At_Pole === false) { Latitude = Math.atan(Sin_p1 / Cos_p1); } p.x = Longitude; p.y = Latitude; p.z = Height; return p; }, // geocentric_to_geodetic_noniter() /****************************************************************/ // pj_geocentic_to_wgs84( p ) // p = point to transform in geocentric coordinates (x,y,z) geocentric_to_wgs84: function(p) { if (this.datum_type === PJD_3PARAM) { // if( x[io] === HUGE_VAL ) // continue; p.x += this.datum_params[0]; p.y += this.datum_params[1]; p.z += this.datum_params[2]; } else if (this.datum_type === PJD_7PARAM) { var Dx_BF = this.datum_params[0]; var Dy_BF = this.datum_params[1]; var Dz_BF = this.datum_params[2]; var Rx_BF = this.datum_params[3]; var Ry_BF = this.datum_params[4]; var Rz_BF = this.datum_params[5]; var M_BF = this.datum_params[6]; // if( x[io] === HUGE_VAL ) // continue; var x_out = M_BF * (p.x - Rz_BF * p.y + Ry_BF * p.z) + Dx_BF; var y_out = M_BF * (Rz_BF * p.x + p.y - Rx_BF * p.z) + Dy_BF; var z_out = M_BF * (-Ry_BF * p.x + Rx_BF * p.y + p.z) + Dz_BF; p.x = x_out; p.y = y_out; p.z = z_out; } }, // cs_geocentric_to_wgs84 /****************************************************************/ // pj_geocentic_from_wgs84() // coordinate system definition, // point to transform in geocentric coordinates (x,y,z) geocentric_from_wgs84: function(p) { if (this.datum_type === PJD_3PARAM) { //if( x[io] === HUGE_VAL ) // continue; p.x -= this.datum_params[0]; p.y -= this.datum_params[1]; p.z -= this.datum_params[2]; } else if (this.datum_type === PJD_7PARAM) { var Dx_BF = this.datum_params[0]; var Dy_BF = this.datum_params[1]; var Dz_BF = this.datum_params[2]; var Rx_BF = this.datum_params[3]; var Ry_BF = this.datum_params[4]; var Rz_BF = this.datum_params[5]; var M_BF = this.datum_params[6]; var x_tmp = (p.x - Dx_BF) / M_BF; var y_tmp = (p.y - Dy_BF) / M_BF; var z_tmp = (p.z - Dz_BF) / M_BF; //if( x[io] === HUGE_VAL ) // continue; p.x = x_tmp + Rz_BF * y_tmp - Ry_BF * z_tmp; p.y = -Rz_BF * x_tmp + y_tmp + Rx_BF * z_tmp; p.z = Ry_BF * x_tmp - Rx_BF * y_tmp + z_tmp; } //cs_geocentric_from_wgs84() } }; /** point object, nothing fancy, just allows values to be passed back and forth by reference rather than by value. Other point classes may be used as long as they have x and y properties, which will get modified in the transform method. */ module.exports = datum;