proj4
Version:
Proj4js is a JavaScript library to transform point coordinates from one coordinate system to another, including datum transformations.
372 lines (329 loc) • 10.6 kB
JavaScript
import adjust_lon from '../common/adjust_lon';
import { D2R, R2D, HALF_PI, EPSLN } from '../constants/values';
import Proj from '../Proj';
import { names as longLatNames } from './longlat';
/**
Original projection implementation:
https://github.com/OSGeo/PROJ/blob/46c47e9adf6376ae06afabe5d24a0016a05ced82/src/projections/ob_tran.cpp
Documentation:
https://proj.org/operations/projections/ob_tran.html
References/Formulas:
https://pubs.usgs.gov/pp/1395/report.pdf
Examples:
+proj=ob_tran +o_proj=moll +o_lat_p=45 +o_lon_p=-90
+proj=ob_tran +o_proj=moll +o_lat_p=45 +o_lon_p=-90 +lon_0=60
+proj=ob_tran +o_proj=moll +o_lat_p=45 +o_lon_p=-90 +lon_0=-90
*/
const projectionType = {
OBLIQUE: {
forward: forwardOblique,
inverse: inverseOblique
},
TRANSVERSE: {
forward: forwardTransverse,
inverse: inverseTransverse
}
};
/**
* @typedef {Object} LocalThis
* @property {number} lamp
* @property {number} cphip
* @property {number} sphip
* @property {Object} projectionType
* @property {string | undefined} o_proj
* @property {string | undefined} o_lon_p
* @property {string | undefined} o_lat_p
* @property {string | undefined} o_alpha
* @property {string | undefined} o_lon_c
* @property {string | undefined} o_lat_c
* @property {string | undefined} o_lon_1
* @property {string | undefined} o_lat_1
* @property {string | undefined} o_lon_2
* @property {string | undefined} o_lat_2
* @property {number | undefined} oLongP
* @property {number | undefined} oLatP
* @property {number | undefined} oAlpha
* @property {number | undefined} oLongC
* @property {number | undefined} oLatC
* @property {number | undefined} oLong1
* @property {number | undefined} oLat1
* @property {number | undefined} oLong2
* @property {number | undefined} oLat2
* @property {boolean} isIdentity
* @property {import('..').Converter} obliqueProjection
*
*/
/**
* Parameters can be from the following sets:
* New pole --> o_lat_p, o_lon_p
* Rotate about point --> o_alpha, o_lon_c, o_lat_c
* New equator points --> lon_1, lat_1, lon_2, lat_2
*
* Per the original source code, the parameter sets are
* checked in the order of the object below.
*/
const paramSets = {
ROTATE: {
o_alpha: 'oAlpha',
o_lon_c: 'oLongC',
o_lat_c: 'oLatC'
},
NEW_POLE: {
o_lat_p: 'oLatP',
o_lon_p: 'oLongP'
},
NEW_EQUATOR: {
o_lon_1: 'oLong1',
o_lat_1: 'oLat1',
o_lon_2: 'oLong2',
o_lat_2: 'oLat2'
}
};
/** @this {import('../defs.js').ProjectionDefinition & LocalThis} */
export function init() {
this.x0 = this.x0 || 0;
this.y0 = this.y0 || 0;
this.long0 = this.long0 || 0;
this.title = this.title || 'General Oblique Transformation';
this.isIdentity = longLatNames.includes(this.o_proj);
/** Verify required parameters exist */
if (!this.o_proj) {
throw new Error('Missing parameter: o_proj');
}
if (this.o_proj === `ob_tran`) {
throw new Error('Invalid value for o_proj: ' + this.o_proj);
}
const newProjStr = this.projStr.replace('+proj=ob_tran', '').replace('+o_proj=', '+proj=').trim();
/** @type {import('../defs.js').ProjectionDefinition} */
const oProj = Proj(newProjStr);
if (!oProj) {
throw new Error('Invalid parameter: o_proj. Unknown projection ' + this.o_proj);
}
oProj.long0 = 0; // we handle long0 before/after forward/inverse
this.obliqueProjection = oProj;
let matchedSet;
const paramSetsKeys = Object.keys(paramSets);
/**
* parse strings, convert to radians, throw on NaN
* @param {string} name
* @returns {number | undefined}
*/
const parseParam = (name) => {
if (typeof this[name] === `undefined`) {
return undefined;
}
const val = parseFloat(this[name]) * D2R;
if (isNaN(val)) {
throw new Error('Invalid value for ' + name + ': ' + this[name]);
}
return val;
};
for (let i = 0; i < paramSetsKeys.length; i++) {
const setKey = paramSetsKeys[i];
const set = paramSets[setKey];
const params = Object.entries(set);
const setHasParams = params.some(
([p]) => typeof this[p] !== 'undefined'
);
if (!setHasParams) {
continue;
}
matchedSet = set;
for (let ii = 0; ii < params.length; ii++) {
const [inputParam, param] = params[ii];
const val = parseParam(inputParam);
if (typeof val === 'undefined') {
throw new Error('Missing parameter: ' + inputParam + '.');
}
this[param] = val;
}
break;
}
if (!matchedSet) {
throw new Error('No valid parameters provided for ob_tran projection.');
}
const { lamp, phip } = createRotation(this, matchedSet);
this.lamp = lamp;
if (Math.abs(phip) > EPSLN) {
this.cphip = Math.cos(phip);
this.sphip = Math.sin(phip);
this.projectionType = projectionType.OBLIQUE;
} else {
this.projectionType = projectionType.TRANSVERSE;
}
}
// ob_tran forward equations--mapping (lat,long) to (x,y)
// transverse (90 degrees from normal orientation) - forwardTransverse
// or oblique (arbitrary angle) used based on parameters - forwardOblique
// -----------------------------------------------------------------
/** @this {import('../defs.js').ProjectionDefinition & LocalThis} */
export function forward(p) {
return this.projectionType.forward(this, p);
}
// inverse equations--mapping (x,y) to (lat,long)
// transverse: inverseTransverse
// oblique: inverseOblique
// -----------------------------------------------------------------
/** @this {import('../defs.js').ProjectionDefinition & LocalThis} */
export function inverse(p) {
return this.projectionType.inverse(this, p);
}
/**
* @param {import('../defs.js').ProjectionDefinition & LocalThis} params - Initialized projection definition
* @param {Object} how - Transformation method
* @returns {{phip: number, lamp: number}}
*/
function createRotation(params, how) {
let phip, lamp;
if (how === paramSets.ROTATE) {
let lamc = params.oLongC;
let phic = params.oLatC;
let alpha = params.oAlpha;
if (Math.abs(Math.abs(phic) - HALF_PI) <= EPSLN) {
throw new Error('Invalid value for o_lat_c: ' + params.o_lat_c + ' should be < 90°');
}
lamp = lamc + Math.atan2(-1 * Math.cos(alpha), -1 * Math.sin(alpha) * Math.sin(phic));
phip = Math.asin(Math.cos(phic) * Math.sin(alpha));
} else if (how === paramSets.NEW_POLE) {
lamp = params.oLongP;
phip = params.oLatP;
} else {
let lam1 = params.oLong1;
let phi1 = params.oLat1;
let lam2 = params.oLong2;
let phi2 = params.oLat2;
let con = Math.abs(phi1);
if (Math.abs(phi1) > HALF_PI - EPSLN) {
throw new Error('Invalid value for o_lat_1: ' + params.o_lat_1 + ' should be < 90°');
}
if (Math.abs(phi2) > HALF_PI - EPSLN) {
throw new Error('Invalid value for o_lat_2: ' + params.o_lat_2 + ' should be < 90°');
}
if (Math.abs(phi1 - phi2) < EPSLN) {
throw new Error('Invalid value for o_lat_1 and o_lat_2: o_lat_1 should be different from o_lat_2');
}
if (con < EPSLN) {
throw new Error('Invalid value for o_lat_1: o_lat_1 should be different from zero');
}
lamp = Math.atan2(
(Math.cos(phi1) * Math.sin(phi2) * Math.cos(lam1))
- (Math.sin(phi1) * Math.cos(phi2) * Math.cos(lam2)),
(Math.sin(phi1) * Math.cos(phi2) * Math.sin(lam2))
- (Math.cos(phi1) * Math.sin(phi2) * Math.sin(lam1))
);
phip = Math.atan(-1 * Math.cos(lamp - lam1) / Math.tan(phi1));
}
return { lamp, phip };
}
/**
* Forward (lng, lat) to (x, y) for oblique case
* @param {import('../defs.js').ProjectionDefinition & LocalThis} self
* @param {{x: number, y: number}} lp - lambda, phi
*/
function forwardOblique(self, lp) {
let { x: lam, y: phi } = lp;
lam += self.long0;
const coslam = Math.cos(lam);
const sinphi = Math.sin(phi);
const cosphi = Math.cos(phi);
lp.x = adjust_lon(
Math.atan2(
cosphi * Math.sin(lam),
(self.sphip * cosphi * coslam) + (self.cphip * sinphi)
) + self.lamp
);
lp.y = Math.asin(
(self.sphip * sinphi) - (self.cphip * cosphi * coslam)
);
const result = self.obliqueProjection.forward(lp);
if (self.isIdentity) {
result.x *= R2D;
result.y *= R2D;
}
return result;
}
/**
* Forward (lng, lat) to (x, y) for transverse case
* @param {import('../defs.js').ProjectionDefinition & LocalThis} self
* @param {{x: number, y: number}} lp - lambda, phi
*/
function forwardTransverse(self, lp) {
let { x: lam, y: phi } = lp;
lam += self.long0;
const cosphi = Math.cos(phi);
const coslam = Math.cos(lam);
lp.x = adjust_lon(
Math.atan2(
cosphi * Math.sin(lam),
Math.sin(phi)
) + self.lamp
);
lp.y = Math.asin(-1 * cosphi * coslam);
const result = self.obliqueProjection.forward(lp);
if (self.isIdentity) {
result.x *= R2D;
result.y *= R2D;
}
return result;
}
/**
* Inverse (x, y) to (lng, lat) for oblique case
* @param {import('../defs.js').ProjectionDefinition & LocalThis} self
* @param {{x: number, y: number}} lp - lambda, phi
*/
function inverseOblique(self, lp) {
if (self.isIdentity) {
lp.x *= D2R;
lp.y *= D2R;
}
const innerLp = self.obliqueProjection.inverse(lp);
let { x: lam, y: phi } = innerLp;
if (lam < Number.MAX_VALUE) {
lam -= self.lamp;
const coslam = Math.cos(lam);
const sinphi = Math.sin(phi);
const cosphi = Math.cos(phi);
lp.x = Math.atan2(
cosphi * Math.sin(lam),
(self.sphip * cosphi * coslam) - (self.cphip * sinphi)
);
lp.y = Math.asin(
(self.sphip * sinphi) + (self.cphip * cosphi * coslam)
);
}
lp.x = adjust_lon(lp.x + self.long0);
return lp;
}
/**
* Inverse (x, y) to (lng, lat) for transverse case
* @param {import('../defs.js').ProjectionDefinition & LocalThis} self
* @param {{x: number, y: number}} lp - lambda, phi
*/
function inverseTransverse(self, lp) {
if (self.isIdentity) {
lp.x *= D2R;
lp.y *= D2R;
}
const innerLp = self.obliqueProjection.inverse(lp);
let { x: lam, y: phi } = innerLp;
if (lam < Number.MAX_VALUE) {
const cosphi = Math.cos(phi);
lam -= self.lamp;
lp.x = Math.atan2(
cosphi * Math.sin(lam),
-1 * Math.sin(phi)
);
lp.y = Math.asin(
cosphi * Math.cos(lam)
);
}
lp.x = adjust_lon(lp.x + self.long0);
return lp;
}
export var names = ['General Oblique Transformation', 'General_Oblique_Transformation', 'ob_tran'];
export default {
init: init,
forward: forward,
inverse: inverse,
names: names
};