UNPKG

s2-tools

Version:

A collection of geospatial tools primarily designed for WGS84, Web Mercator, and S2.

486 lines 18.8 kB
import { adjustLon } from './common'; import { DATUMS, HALF_PI, PJD_3PARAM, PJD_7PARAM, PJD_GRIDSHIFT, PJD_NODATUM, PJD_WGS84, SEC_TO_RAD, SRS_WGS84_ESQUARED, SRS_WGS84_SEMIMAJOR, SRS_WGS84_SEMIMINOR, } from './constants'; /** * Modifies projection parameters to properly define a datum * @param params - projection specific parameters to be adjusted * @param transformer - the projection transformer to potentially pull data from */ export function buildDatum(params, transformer) { if (params.datumCode === undefined || params.datumCode === 'none') { params.datumType = PJD_NODATUM; } else { params.datumType = PJD_WGS84; } // If datumParams is undefined, check against datum constants using datumCode if (params.datumParams === undefined) { const datum = DATUMS[params.datumCode?.toLowerCase() ?? '']; if (datum !== undefined) { // @ts-expect-error - this will be fixed in the next line params.datumParams = datum.datumParams; params.ellps = datum.ellipse; } } if (params.datumParams !== undefined) { if (params.datumParams[0] !== 0 || params.datumParams[1] !== 0 || params.datumParams[2] !== 0) { params.datumType = PJD_3PARAM; } if (params.datumParams.length > 3) { if (params.datumParams[3] !== 0 || params.datumParams[4] !== 0 || params.datumParams[5] !== 0 || params.datumParams[6] !== 0) { params.datumType = PJD_7PARAM; params.datumParams[3] *= SEC_TO_RAD; params.datumParams[4] *= SEC_TO_RAD; params.datumParams[5] *= SEC_TO_RAD; params.datumParams[6] = params.datumParams[6] / 1000000.0 + 1.0; } } } // Upgrade datumType if grids exists in params and pull in the grids we need if (params.nadgrids !== undefined) { params.datumType = PJD_GRIDSHIFT; params.grids = transformer.getGridsFromString(params.nadgrids); } } /** * Compares datums to see if they have equal datums * @param source - source projection starting from * @param dest - destination projection to end at * @returns true if projection datums are equal */ export function compareDatums(source, dest) { if (source.datumType !== dest.datumType) { return false; // false, datums are not equal } else if (source.a !== dest.a || Math.abs(source.es - dest.es) > 0.00000000005) { // the tolerance for es is to ensure that GRS80 and WGS84 // are considered identical return false; } else if (source.datumType === PJD_3PARAM) { return (source.datumParams[0] === dest.datumParams[0] && source.datumParams[1] === dest.datumParams[1] && source.datumParams[2] === dest.datumParams[2]); } else if (source.datumType === PJD_7PARAM) { return (source.datumParams[0] === dest.datumParams[0] && source.datumParams[1] === dest.datumParams[1] && source.datumParams[2] === dest.datumParams[2] && source.datumParams[3] === dest.datumParams[3] && source.datumParams[4] === dest.datumParams[4] && source.datumParams[5] === dest.datumParams[5] && source.datumParams[6] === dest.datumParams[6]); } 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) * @param p - lon-lat WGS84 point * @param es - eccentricity * @param a - semi-major axis */ export function geodeticToGeocentric(p, es, a) { const { sin, cos, sqrt, PI } = Math; let Longitude = p.x; let Latitude = p.y; const Height = p.z !== undefined ? p.z : 0; //Z value not always supplied /* ** 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 cos() and 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) { throw new Error('geocent:lat out of range:' + Latitude); } else if (Latitude > HALF_PI) { throw new Error('geocent:lat out of range:' + Latitude); } if (Longitude > PI) Longitude -= 2 * PI; const Sin_Lat = sin(Latitude); /* sin(Latitude) */ const Cos_Lat = cos(Latitude); /* cos(Latitude) */ const Sin2_Lat = Sin_Lat * Sin_Lat; /* Square of sin(Latitude) */ const Rn = a / sqrt(1.0 - es * Sin2_Lat); /* Earth radius at location */ p.x = (Rn + Height) * Cos_Lat * cos(Longitude); p.y = (Rn + Height) * Cos_Lat * sin(Longitude); p.z = (Rn * (1 - es) + Height) * Sin_Lat; } /** * converts a geocentric point to a geodetic point * @param p - Geocentric point * @param es - ellipsoid eccentricity * @param a - ellipsoid semimajor axis * @param b - ellipsoid semiminor axis */ export function geocentricToGeodetic(p, es, a, b) { const { abs, sqrt, atan2, atan } = Math; /* local defintions and variables */ /* end-criterium of loop, accuracy of sin(Latitude) */ const genau = 1e-12; const genau2 = genau * genau; const maxiter = 30; let RX; let RK; let RN; /* Earth radius at location */ let CPHI0; /* cos of start or old geodetic latitude in iterations */ let SPHI0; /* sin of start or old geodetic latitude in iterations */ let CPHI; /* cos of searched geodetic latitude */ let SPHI; /* sin of searched geodetic latitude */ let SDPHI; /* end-criterium: addition-theorem of sin(Latitude(iter)-Latitude(iter-1)) */ let iter; /* # of continous iteration, max. 30 is always enough (s.a.) */ const X = p.x; const Y = p.y; const Z = p.z !== undefined ? p.z : 0.0; //Z value not always supplied let Longitude; let Latitude; let Height; const P = sqrt(X * X + Y * Y); /* distance between semi-minor axis and location */ const RR = sqrt(X * X + Y * Y + Z * Z); /* distance between center and location */ /* special cases for latitude and longitude */ if (P / a < genau) { /* special case, if P=0. (X=0., Y=0.) */ 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 / a < genau) { Latitude = HALF_PI; Height = -b; return; } } else { /* ellipsoidal (geodetic) longitude * interval: -PI < Longitude <= +PI */ Longitude = 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. * -------------------------------------------------------------- */ const CT = Z / RR; /* sin of geocentric latitude */ const ST = P / RR; /* cos of geocentric latitude */ RX = 1.0 / sqrt(1.0 - es * (2.0 - es) * ST * ST); CPHI0 = ST * (1.0 - 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 = a / sqrt(1.0 - es * SPHI0 * SPHI0); /* ellipsoidal (geodetic) height */ Height = P * CPHI0 + Z * SPHI0 - RN * (1.0 - es * SPHI0 * SPHI0); RK = (es * RN) / (RN + Height); RX = 1.0 / 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 = atan(SPHI / abs(CPHI)); p.x = Longitude; p.y = Latitude; p.z = Height; } /** * pj_geocentic_to_wgs84( p ) * p = point to transform in geocentric coordinates (x,y,z) * 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. * @param p - Geocentric point * @param datumType - datum type * @param datumParams - datum parameters */ export function geocentricToWgs84(p, datumType, datumParams) { const z = p.z ?? 0; if (datumType === PJD_3PARAM) { // if( x[io] === HUGE_VAL ) // continue; p.x += datumParams[0]; p.y += datumParams[1]; p.z = z + datumParams[2]; } else if (datumType === PJD_7PARAM) { const Dx_BF = datumParams[0]; const Dy_BF = datumParams[1]; const Dz_BF = datumParams[2]; const Rx_BF = datumParams[3]; const Ry_BF = datumParams[4]; const Rz_BF = datumParams[5]; const M_BF = datumParams[6]; // if( x[io] === HUGE_VAL ) // continue; p.x = M_BF * (p.x - Rz_BF * p.y + Ry_BF * z) + Dx_BF; p.y = M_BF * (Rz_BF * p.x + p.y - Rx_BF * z) + Dy_BF; p.z = M_BF * (-Ry_BF * p.x + Rx_BF * p.y + z) + Dz_BF; } else { throw new Error(`geocentricToWgs84: unknown datum type: ${datumType}`); } } /** * pj_geocentic_from_wgs84() coordinate system definition, * point to transform in geocentric coordinates (x,y,z) * @param p - lon-lat WGS84 point * @param datumType - datum type * @param datumParams - datum parameters */ export function geocentricFromWgs84(p, datumType, datumParams) { const z = p.z ?? 0; if (datumType === PJD_3PARAM) { //if( x[io] === HUGE_VAL ) // continue; p.x -= datumParams[0]; p.y -= datumParams[1]; p.z = z - datumParams[2]; } else if (datumType === PJD_7PARAM) { const Dx_BF = datumParams[0]; const Dy_BF = datumParams[1]; const Dz_BF = datumParams[2]; const Rx_BF = datumParams[3]; const Ry_BF = datumParams[4]; const Rz_BF = datumParams[5]; const M_BF = datumParams[6]; const x_tmp = (p.x - Dx_BF) / M_BF; const y_tmp = (p.y - Dy_BF) / M_BF; const z_tmp = (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; } else { throw new Error(`geocentricToWgs84: unknown datum type: ${datumType}`); } } /** * check if 1 or 2 (3 or 7 parameter datum) * @param type - datum type * @returns - true if 1 or 2 (3 or 7 parameter datum) */ function checkParams(type) { return type === PJD_3PARAM || type === PJD_7PARAM; } /** * check if either of the projections are not WGS84 * @param source - source projection * @param dest - destination projection * @returns - true if either of the projections are not WGS84 */ export function checkNotWGS(source, dest) { return (((source.datumType === PJD_3PARAM || source.datumType === PJD_7PARAM || source.datumType === PJD_GRIDSHIFT) && dest.datumCode !== 'WGS84') || ((dest.datumType === PJD_3PARAM || dest.datumType === PJD_7PARAM || dest.datumType === PJD_GRIDSHIFT) && source.datumCode !== 'WGS84')); } /** * Transforms a point from one datum to another * @param point - lon-lat WGS84 point to mutate * @param source - source projection * @param dest - destination projection */ export function datumTransform(point, source, dest) { // Short cut if the datums are identical. if (compareDatums(source, dest)) return; // Explicitly skip datum transform by setting 'datum=none' as parameter for either source or dest if (source.datumType === PJD_NODATUM || dest.datumType === PJD_NODATUM) return; // If this datum requires grid shifts, then apply it to geodetic coordinates. let sourceA = source.a; let sourceEs = source.es; if (source.datumType === PJD_GRIDSHIFT) { // source applyGridShift(source, false, point); sourceA = SRS_WGS84_SEMIMAJOR; sourceEs = SRS_WGS84_ESQUARED; } let dest_a = dest.a; let dest_b = dest.b; let dest_es = dest.es; if (dest.datumType === PJD_GRIDSHIFT) { dest_a = SRS_WGS84_SEMIMAJOR; dest_b = SRS_WGS84_SEMIMINOR; dest_es = SRS_WGS84_ESQUARED; } // Do we need to go through geocentric coordinates? if (sourceEs === dest_es && sourceA === dest_a && !checkParams(source.datumType) && !checkParams(dest.datumType)) return; // Convert to geocentric coordinates. geodeticToGeocentric(point, sourceEs, sourceA); // Convert between datums if (checkParams(source.datumType)) geocentricToWgs84(point, source.datumType, source.datumParams); if (checkParams(dest.datumType)) geocentricFromWgs84(point, dest.datumType, dest.datumParams); // Convert back to geodetic coordinates. geocentricToGeodetic(point, dest_es, dest_a, dest_b); if (dest.datumType === PJD_GRIDSHIFT) applyGridShift(dest, true, point); } /** * Apply a grid shift given the source projection (assums a nadgrid has been set up) * @param source - source projection * @param inverse - if true, the grid shift is applied in reverse * @param point - the point to apply the grid shift */ export function applyGridShift(source, inverse, point) { const { abs } = Math; const { grids } = source; if (grids === undefined) throw new Error('Grid shift grids not found'); const input = { x: -point.x, y: point.y }; let output = { x: Number.NaN, y: Number.NaN }; const attemptedGrids = []; outer: for (const grid of grids) { attemptedGrids.push(grid.name); if (grid.isNull) { output = input; break; } if (grid.grid === undefined) { if (grid.mandatory) { console.warn(`Unable to find mandatory grid '${grid.name}'. Maybe have an incorrect result.`); } continue; } const subgrids = grid.grid.subgrids; for (let j = 0, jj = subgrids.length; j < jj; j++) { const subgrid = subgrids[j]; // skip tables that don't match our point at all const epsilon = (abs(subgrid.del.y) + abs(subgrid.del.x)) / 10000.0; const minX = subgrid.ll.x - epsilon; const minY = subgrid.ll.y - epsilon; const maxX = subgrid.ll.x + (subgrid.lim.x - 1) * subgrid.del.x + epsilon; const maxY = subgrid.ll.y + (subgrid.lim.y - 1) * subgrid.del.y + epsilon; if (minY > input.y || minX > input.x || maxY < input.y || maxX < input.x) { continue; } output = applySubgridShift(input, inverse, subgrid); if (!isNaN(output.x)) { break outer; } } } if (isNaN(output.x)) { return; // throw new Error( // `Failed to find a grid shift table for location '${-input.x * R2D} ${input.y * R2D}' tried: '${attemptedGrids}'`, // ); } point.x = -output.x; point.y = output.y; } /** * Apply a subgrid shift * @param pin - the point * @param inverse - if true, the grid shift is applied in reverse * @param ct - the subgrid * @returns - the new point */ function applySubgridShift(pin, inverse, ct) { const { abs, PI } = Math; const val = { x: Number.NaN, y: Number.NaN }; const tb = { x: pin.x, y: pin.y }; tb.x -= ct.ll.x; tb.y -= ct.ll.y; tb.x = adjustLon(tb.x - PI) + PI; const t = nadInterpolate(tb, ct); if (inverse) { if (isNaN(t.x)) return val; t.x = tb.x - t.x; t.y = tb.y - t.y; let i = 9; const tol = 1e-12; let dif, del; do { del = nadInterpolate(t, ct); if (isNaN(del.x)) { throw new Error('Inverse grid shift iteration failed, presumably at grid edge. Using first approximation.'); } dif = { x: tb.x - (del.x + t.x), y: tb.y - (del.y + t.y) }; t.x += dif.x; t.y += dif.y; } while (i-- > 0 && abs(dif.x) > tol && abs(dif.y) > tol); if (i < 0) throw new Error('Inverse grid shift iterator failed to converge.'); val.x = adjustLon(t.x + ct.ll.x); val.y = t.y + ct.ll.y; } else { if (!isNaN(t.x)) { val.x = pin.x + t.x; val.y = pin.y + t.y; } } return val; } /** * Interpolate between two points using the nad-subgrid * @param pin - the point * @param ct - the subgrid * @returns - the new point */ function nadInterpolate(pin, ct) { const { floor } = Math; const t = { x: pin.x / ct.del.x, y: pin.y / ct.del.y }; const indx = { x: floor(t.x), y: floor(t.y) }; const frct = { x: t.x - indx.x, y: t.y - indx.y }; const val = { x: NaN, y: NaN }; let inx; if (indx.x < 0 || indx.x >= ct.lim.x) return val; if (indx.y < 0 || indx.y >= ct.lim.y) return val; inx = indx.y * ct.lim.x + indx.x; const f00 = { x: ct.cvs[inx].x, y: ct.cvs[inx].y }; inx++; const f10 = { x: ct.cvs[inx].x, y: ct.cvs[inx].y }; inx += ct.lim.x; const f11 = { x: ct.cvs[inx].x, y: ct.cvs[inx].y }; inx--; const f01 = { x: ct.cvs[inx].x, y: ct.cvs[inx].y }; const m11 = frct.x * frct.y, m10 = frct.x * (1.0 - frct.y), m00 = (1.0 - frct.x) * (1.0 - frct.y), m01 = (1.0 - frct.x) * frct.y; val.x = m00 * f00.x + m10 * f10.x + m01 * f01.x + m11 * f11.x; val.y = m00 * f00.y + m10 * f10.y + m01 * f01.y + m11 * f11.y; return val; } //# sourceMappingURL=datum.js.map