// QSC projection rewritten from the original PROJ4 // https://github.com/OSGeo/proj.4/blob/master/src/PJ_qsc.c import {EPSLN, TWO_PI, SPI, HALF_PI, FORTPI} from '../constants/values'; /* constants */ var FACE_ENUM = { FRONT: 1, RIGHT: 2, BACK: 3, LEFT: 4, TOP: 5, BOTTOM: 6 }; var AREA_ENUM = { AREA_0: 1, AREA_1: 2, AREA_2: 3, AREA_3: 4 }; export function init() { this.x0 = this.x0 || 0; this.y0 = this.y0 || 0; this.lat0 = this.lat0 || 0; this.long0 = this.long0 || 0; this.lat_ts = this.lat_ts || 0; this.title = this.title || "Quadrilateralized Spherical Cube"; /* Determine the cube face from the center of projection. */ if (this.lat0 >= HALF_PI - FORTPI / 2.0) { this.face = FACE_ENUM.TOP; } else if (this.lat0 <= -(HALF_PI - FORTPI / 2.0)) { this.face = FACE_ENUM.BOTTOM; } else if (Math.abs(this.long0) <= FORTPI) { this.face = FACE_ENUM.FRONT; } else if (Math.abs(this.long0) <= HALF_PI + FORTPI) { this.face = this.long0 > 0.0 ? FACE_ENUM.RIGHT : FACE_ENUM.LEFT; } else { this.face = FACE_ENUM.BACK; } /* Fill in useful values for the ellipsoid <-> sphere shift * described in [LK12]. */ if (this.es !== 0) { this.one_minus_f = 1 - (this.a - this.b) / this.a; this.one_minus_f_squared = this.one_minus_f * this.one_minus_f; } } // QSC forward equations--mapping lat,long to x,y // ----------------------------------------------------------------- export function forward(p) { var xy = {x: 0, y: 0}; var lat, lon; var theta, phi; var t, mu; /* nu; */ var area = {value: 0}; // move lon according to projection's lon p.x -= this.long0; /* Convert the geodetic latitude to a geocentric latitude. * This corresponds to the shift from the ellipsoid to the sphere * described in [LK12]. */ if (this.es !== 0) {//if (P->es != 0) { lat = Math.atan(this.one_minus_f_squared * Math.tan(p.y)); } else { lat = p.y; } /* Convert the input lat, lon into theta, phi as used by QSC. * This depends on the cube face and the area on it. * For the top and bottom face, we can compute theta and phi * directly from phi, lam. For the other faces, we must use * unit sphere cartesian coordinates as an intermediate step. */ lon = p.x; //lon = lp.lam; if (this.face === FACE_ENUM.TOP) { phi = HALF_PI - lat; if (lon >= FORTPI && lon <= HALF_PI + FORTPI) { area.value = AREA_ENUM.AREA_0; theta = lon - HALF_PI; } else if (lon > HALF_PI + FORTPI || lon <= -(HALF_PI + FORTPI)) { area.value = AREA_ENUM.AREA_1; theta = (lon > 0.0 ? lon - SPI : lon + SPI); } else if (lon > -(HALF_PI + FORTPI) && lon <= -FORTPI) { area.value = AREA_ENUM.AREA_2; theta = lon + HALF_PI; } else { area.value = AREA_ENUM.AREA_3; theta = lon; } } else if (this.face === FACE_ENUM.BOTTOM) { phi = HALF_PI + lat; if (lon >= FORTPI && lon <= HALF_PI + FORTPI) { area.value = AREA_ENUM.AREA_0; theta = -lon + HALF_PI; } else if (lon < FORTPI && lon >= -FORTPI) { area.value = AREA_ENUM.AREA_1; theta = -lon; } else if (lon < -FORTPI && lon >= -(HALF_PI + FORTPI)) { area.value = AREA_ENUM.AREA_2; theta = -lon - HALF_PI; } else { area.value = AREA_ENUM.AREA_3; theta = (lon > 0.0 ? -lon + SPI : -lon - SPI); } } else { var q, r, s; var sinlat, coslat; var sinlon, coslon; if (this.face === FACE_ENUM.RIGHT) { lon = qsc_shift_lon_origin(lon, +HALF_PI); } else if (this.face === FACE_ENUM.BACK) { lon = qsc_shift_lon_origin(lon, +SPI); } else if (this.face === FACE_ENUM.LEFT) { lon = qsc_shift_lon_origin(lon, -HALF_PI); } sinlat = Math.sin(lat); coslat = Math.cos(lat); sinlon = Math.sin(lon); coslon = Math.cos(lon); q = coslat * coslon; r = coslat * sinlon; s = sinlat; if (this.face === FACE_ENUM.FRONT) { phi = Math.acos(q); theta = qsc_fwd_equat_face_theta(phi, s, r, area); } else if (this.face === FACE_ENUM.RIGHT) { phi = Math.acos(r); theta = qsc_fwd_equat_face_theta(phi, s, -q, area); } else if (this.face === FACE_ENUM.BACK) { phi = Math.acos(-q); theta = qsc_fwd_equat_face_theta(phi, s, -r, area); } else if (this.face === FACE_ENUM.LEFT) { phi = Math.acos(-r); theta = qsc_fwd_equat_face_theta(phi, s, q, area); } else { /* Impossible */ phi = theta = 0; area.value = AREA_ENUM.AREA_0; } } /* Compute mu and nu for the area of definition. * For mu, see Eq. (3-21) in [OL76], but note the typos: * compare with Eq. (3-14). For nu, see Eq. (3-38). */ mu = Math.atan((12 / SPI) * (theta + Math.acos(Math.sin(theta) * Math.cos(FORTPI)) - HALF_PI)); t = Math.sqrt((1 - Math.cos(phi)) / (Math.cos(mu) * Math.cos(mu)) / (1 - Math.cos(Math.atan(1 / Math.cos(theta))))); /* Apply the result to the real area. */ if (area.value === AREA_ENUM.AREA_1) { mu += HALF_PI; } else if (area.value === AREA_ENUM.AREA_2) { mu += SPI; } else if (area.value === AREA_ENUM.AREA_3) { mu += 1.5 * SPI; } /* Now compute x, y from mu and nu */ xy.x = t * Math.cos(mu); xy.y = t * Math.sin(mu); xy.x = xy.x * this.a + this.x0; xy.y = xy.y * this.a + this.y0; p.x = xy.x; p.y = xy.y; return p; } // QSC inverse equations--mapping x,y to lat/long // ----------------------------------------------------------------- export function inverse(p) { var lp = {lam: 0, phi: 0}; var mu, nu, cosmu, tannu; var tantheta, theta, cosphi, phi; var t; var area = {value: 0}; /* de-offset */ p.x = (p.x - this.x0) / this.a; p.y = (p.y - this.y0) / this.a; /* Convert the input x, y to the mu and nu angles as used by QSC. * This depends on the area of the cube face. */ nu = Math.atan(Math.sqrt(p.x * p.x + p.y * p.y)); mu = Math.atan2(p.y, p.x); if (p.x >= 0.0 && p.x >= Math.abs(p.y)) { area.value = AREA_ENUM.AREA_0; } else if (p.y >= 0.0 && p.y >= Math.abs(p.x)) { area.value = AREA_ENUM.AREA_1; mu -= HALF_PI; } else if (p.x < 0.0 && -p.x >= Math.abs(p.y)) { area.value = AREA_ENUM.AREA_2; mu = (mu < 0.0 ? mu + SPI : mu - SPI); } else { area.value = AREA_ENUM.AREA_3; mu += HALF_PI; } /* Compute phi and theta for the area of definition. * The inverse projection is not described in the original paper, but some * good hints can be found here (as of 2011-12-14): * http://fits.gsfc.nasa.gov/fitsbits/saf.93/saf.9302 * (search for "Message-Id: <9302181759.AA25477 at fits.cv.nrao.edu>") */ t = (SPI / 12) * Math.tan(mu); tantheta = Math.sin(t) / (Math.cos(t) - (1 / Math.sqrt(2))); theta = Math.atan(tantheta); cosmu = Math.cos(mu); tannu = Math.tan(nu); cosphi = 1 - cosmu * cosmu * tannu * tannu * (1 - Math.cos(Math.atan(1 / Math.cos(theta)))); if (cosphi < -1) { cosphi = -1; } else if (cosphi > +1) { cosphi = +1; } /* Apply the result to the real area on the cube face. * For the top and bottom face, we can compute phi and lam directly. * For the other faces, we must use unit sphere cartesian coordinates * as an intermediate step. */ if (this.face === FACE_ENUM.TOP) { phi = Math.acos(cosphi); lp.phi = HALF_PI - phi; if (area.value === AREA_ENUM.AREA_0) { lp.lam = theta + HALF_PI; } else if (area.value === AREA_ENUM.AREA_1) { lp.lam = (theta < 0.0 ? theta + SPI : theta - SPI); } else if (area.value === AREA_ENUM.AREA_2) { lp.lam = theta - HALF_PI; } else /* area.value == AREA_ENUM.AREA_3 */ { lp.lam = theta; } } else if (this.face === FACE_ENUM.BOTTOM) { phi = Math.acos(cosphi); lp.phi = phi - HALF_PI; if (area.value === AREA_ENUM.AREA_0) { lp.lam = -theta + HALF_PI; } else if (area.value === AREA_ENUM.AREA_1) { lp.lam = -theta; } else if (area.value === AREA_ENUM.AREA_2) { lp.lam = -theta - HALF_PI; } else /* area.value == AREA_ENUM.AREA_3 */ { lp.lam = (theta < 0.0 ? -theta - SPI : -theta + SPI); } } else { /* Compute phi and lam via cartesian unit sphere coordinates. */ var q, r, s; q = cosphi; t = q * q; if (t >= 1) { s = 0; } else { s = Math.sqrt(1 - t) * Math.sin(theta); } t += s * s; if (t >= 1) { r = 0; } else { r = Math.sqrt(1 - t); } /* Rotate q,r,s into the correct area. */ if (area.value === AREA_ENUM.AREA_1) { t = r; r = -s; s = t; } else if (area.value === AREA_ENUM.AREA_2) { r = -r; s = -s; } else if (area.value === AREA_ENUM.AREA_3) { t = r; r = s; s = -t; } /* Rotate q,r,s into the correct cube face. */ if (this.face === FACE_ENUM.RIGHT) { t = q; q = -r; r = t; } else if (this.face === FACE_ENUM.BACK) { q = -q; r = -r; } else if (this.face === FACE_ENUM.LEFT) { t = q; q = r; r = -t; } /* Now compute phi and lam from the unit sphere coordinates. */ lp.phi = Math.acos(-s) - HALF_PI; lp.lam = Math.atan2(r, q); if (this.face === FACE_ENUM.RIGHT) { lp.lam = qsc_shift_lon_origin(lp.lam, -HALF_PI); } else if (this.face === FACE_ENUM.BACK) { lp.lam = qsc_shift_lon_origin(lp.lam, -SPI); } else if (this.face === FACE_ENUM.LEFT) { lp.lam = qsc_shift_lon_origin(lp.lam, +HALF_PI); } } /* Apply the shift from the sphere to the ellipsoid as described * in [LK12]. */ if (this.es !== 0) { var invert_sign; var tanphi, xa; invert_sign = (lp.phi < 0 ? 1 : 0); tanphi = Math.tan(lp.phi); xa = this.b / Math.sqrt(tanphi * tanphi + this.one_minus_f_squared); lp.phi = Math.atan(Math.sqrt(this.a * this.a - xa * xa) / (this.one_minus_f * xa)); if (invert_sign) { lp.phi = -lp.phi; } } lp.lam += this.long0; p.x = lp.lam; p.y = lp.phi; return p; } /* Helper function for forward projection: compute the theta angle * and determine the area number. */ function qsc_fwd_equat_face_theta(phi, y, x, area) { var theta; if (phi < EPSLN) { area.value = AREA_ENUM.AREA_0; theta = 0.0; } else { theta = Math.atan2(y, x); if (Math.abs(theta) <= FORTPI) { area.value = AREA_ENUM.AREA_0; } else if (theta > FORTPI && theta <= HALF_PI + FORTPI) { area.value = AREA_ENUM.AREA_1; theta -= HALF_PI; } else if (theta > HALF_PI + FORTPI || theta <= -(HALF_PI + FORTPI)) { area.value = AREA_ENUM.AREA_2; theta = (theta >= 0.0 ? theta - SPI : theta + SPI); } else { area.value = AREA_ENUM.AREA_3; theta += HALF_PI; } } return theta; } /* Helper function: shift the longitude. */ function qsc_shift_lon_origin(lon, offset) { var slon = lon + offset; if (slon < -SPI) { slon += TWO_PI; } else if (slon > +SPI) { slon -= TWO_PI; } return slon; } export var names = ["Quadrilateralized Spherical Cube", "Quadrilateralized_Spherical_Cube", "qsc"]; export default { init: init, forward: forward, inverse: inverse, names: names };