diff --git a/LICENSE b/LICENSE index d3347208..68b04a1a 100755 --- a/LICENSE +++ b/LICENSE @@ -300,3 +300,129 @@ Copyright (c) 2014, Mike Adair, Richard Greenwood, Didier Richard, Stephen Irons LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +--- + +Some code has been repurposed from + +Creative Commons Legal Code + +CC0 1.0 Universal + + CREATIVE COMMONS CORPORATION IS NOT A LAW FIRM AND DOES NOT PROVIDE + LEGAL SERVICES. DISTRIBUTION OF THIS DOCUMENT DOES NOT CREATE AN + ATTORNEY-CLIENT RELATIONSHIP. CREATIVE COMMONS PROVIDES THIS + INFORMATION ON AN "AS-IS" BASIS. CREATIVE COMMONS MAKES NO WARRANTIES + REGARDING THE USE OF THIS DOCUMENT OR THE INFORMATION OR WORKS + PROVIDED HEREUNDER, AND DISCLAIMS LIABILITY FOR DAMAGES RESULTING FROM + THE USE OF THIS DOCUMENT OR THE INFORMATION OR WORKS PROVIDED + HEREUNDER. + +Statement of Purpose + +The laws of most jurisdictions throughout the world automatically confer +exclusive Copyright and Related Rights (defined below) upon the creator +and subsequent owner(s) (each and all, an "owner") of an original work of +authorship and/or a database (each, a "Work"). + +Certain owners wish to permanently relinquish those rights to a Work for +the purpose of contributing to a commons of creative, cultural and +scientific works ("Commons") that the public can reliably and without fear +of later claims of infringement build upon, modify, incorporate in other +works, reuse and redistribute as freely as possible in any form whatsoever +and for any purposes, including without limitation commercial purposes. +These owners may contribute to the Commons to promote the ideal of a free +culture and the further production of creative, cultural and scientific +works, or to gain reputation or greater distribution for their Work in +part through the use and efforts of others. + +For these and/or other purposes and motivations, and without any +expectation of additional consideration or compensation, the person +associating CC0 with a Work (the "Affirmer"), to the extent that he or she +is an owner of Copyright and Related Rights in the Work, voluntarily +elects to apply CC0 to the Work and publicly distribute the Work under its +terms, with knowledge of his or her Copyright and Related Rights in the +Work and the meaning and intended legal effect of CC0 on those rights. + +1. Copyright and Related Rights. A Work made available under CC0 may be +protected by copyright and related or neighboring rights ("Copyright and +Related Rights"). Copyright and Related Rights include, but are not +limited to, the following: + + i. the right to reproduce, adapt, distribute, perform, display, + communicate, and translate a Work; + ii. moral rights retained by the original author(s) and/or performer(s); +iii. publicity and privacy rights pertaining to a person's image or + likeness depicted in a Work; + iv. rights protecting against unfair competition in regards to a Work, + subject to the limitations in paragraph 4(a), below; + v. rights protecting the extraction, dissemination, use and reuse of data + in a Work; + vi. database rights (such as those arising under Directive 96/9/EC of the + European Parliament and of the Council of 11 March 1996 on the legal + protection of databases, and under any national implementation + thereof, including any amended or successor version of such + directive); and +vii. other similar, equivalent or corresponding rights throughout the + world based on applicable law or treaty, and any national + implementations thereof. + +2. Waiver. To the greatest extent permitted by, but not in contravention +of, applicable law, Affirmer hereby overtly, fully, permanently, +irrevocably and unconditionally waives, abandons, and surrenders all of +Affirmer's Copyright and Related Rights and associated claims and causes +of action, whether now known or unknown (including existing as well as +future claims and causes of action), in the Work (i) in all territories +worldwide, (ii) for the maximum duration provided by applicable law or +treaty (including future time extensions), (iii) in any current or future +medium and for any number of copies, and (iv) for any purpose whatsoever, +including without limitation commercial, advertising or promotional +purposes (the "Waiver"). Affirmer makes the Waiver for the benefit of each +member of the public at large and to the detriment of Affirmer's heirs and +successors, fully intending that such Waiver shall not be subject to +revocation, rescission, cancellation, termination, or any other legal or +equitable action to disrupt the quiet enjoyment of the Work by the public +as contemplated by Affirmer's express Statement of Purpose. + +3. Public License Fallback. Should any part of the Waiver for any reason +be judged legally invalid or ineffective under applicable law, then the +Waiver shall be preserved to the maximum extent permitted taking into +account Affirmer's express Statement of Purpose. In addition, to the +extent the Waiver is so judged Affirmer hereby grants to each affected +person a royalty-free, non transferable, non sublicensable, non exclusive, +irrevocable and unconditional license to exercise Affirmer's Copyright and +Related Rights in the Work (i) in all territories worldwide, (ii) for the +maximum duration provided by applicable law or treaty (including future +time extensions), (iii) in any current or future medium and for any number +of copies, and (iv) for any purpose whatsoever, including without +limitation commercial, advertising or promotional purposes (the +"License"). The License shall be deemed effective as of the date CC0 was +applied by Affirmer to the Work. Should any part of the License for any +reason be judged legally invalid or ineffective under applicable law, such +partial invalidity or ineffectiveness shall not invalidate the remainder +of the License, and in such case Affirmer hereby affirms that he or she +will not (i) exercise any of his or her remaining Copyright and Related +Rights in the Work or (ii) assert any associated claims and causes of +action with respect to the Work, in either case contrary to Affirmer's +express Statement of Purpose. + +4. Limitations and Disclaimers. + + a. No trademark or patent rights held by Affirmer are waived, abandoned, + surrendered, licensed or otherwise affected by this document. + b. Affirmer offers the Work as-is and makes no representations or + warranties of any kind concerning the Work, express, implied, + statutory or otherwise, including without limitation warranties of + title, merchantability, fitness for a particular purpose, non + infringement, or the absence of latent or other defects, accuracy, or + the present or absence of errors, whether or not discoverable, all to + the greatest extent permissible under applicable law. + c. Affirmer disclaims responsibility for clearing rights of other persons + that may apply to the Work or any use thereof, including without + limitation any person's Copyright and Related Rights in the Work. + Further, Affirmer disclaims responsibility for obtaining any necessary + consents, permissions or other rights required for any use of the + Work. + d. Affirmer understands and acknowledges that Creative Commons is not a + party to this document and has no duty or obligation with respect to + this CC0 or use of the Work. diff --git a/bun.lockb b/bun.lockb index 8711be9d..54ad599b 100755 Binary files a/bun.lockb and b/bun.lockb differ diff --git a/eslint.config.mjs b/eslint.config.mjs index ae3ba659..9978147f 100644 --- a/eslint.config.mjs +++ b/eslint.config.mjs @@ -24,6 +24,11 @@ export default tseslint.config( jsdoc.configs['flat/recommended-typescript'], { rules: { + // ensure explicit comparisons + eqeqeq: ['error', 'always'], + 'no-implicit-coercion': ['error', { boolean: false }], + 'no-extra-boolean-cast': 'error', + 'no-constant-condition': ['error', { checkLoops: false }], // console logs 'no-console': ['error', { allow: ['info', 'warn', 'error'] }], // https://github.com/gajus/eslint-plugin-jsdoc diff --git a/package.json b/package.json index 811b7fc7..96ad5363 100755 --- a/package.json +++ b/package.json @@ -64,6 +64,7 @@ "@skypack/package-check": "^0.2.2", "@types/bun": "^1.1.9", "@types/node": "^22.5.5", + "@types/tmp": "^0.2.6", "eslint": "^9.10.0", "eslint-config-prettier": "^9.1.0", "eslint-plugin-jsdoc": "^50.2.3", @@ -72,6 +73,7 @@ "nextafter": "^1.0.0", "prettier": "^3.3.3", "robust-orientation": "^1.2.1", + "tmp": "^0.2.3", "typedoc": "^0.26.7", "typedoc-plugin-coverage": "^3.3.0", "typescript": "^5.6.2", diff --git a/proj4js-master/lib/Proj.js b/proj4js-master/lib/Proj.js index e1d504a6..885f4bb5 100644 --- a/proj4js-master/lib/Proj.js +++ b/proj4js-master/lib/Proj.js @@ -17,6 +17,7 @@ function Projection(srsCode,callback) { } }; var json = parseCode(srsCode); + // console.log('json', json) if(typeof json !== 'object'){ callback('Could not parse to valid json: ' + srsCode); return; @@ -38,10 +39,8 @@ function Projection(srsCode,callback) { json.axis = json.axis || 'enu'; json.ellps = json.ellps || 'wgs84'; json.lat1 = json.lat1 || json.lat0; // Lambert_Conformal_Conic_1SP, for example, needs this - console.log('json begin', json) var sphere_ = dc_sphere(json.a, json.b, json.rf, json.ellps, json.sphere); - console.log('SPHERE', sphere_) var ecc = dc_eccentricity(sphere_.a, sphere_.b, sphere_.rf, json.R_A); var nadgrids = getNadgrids(json.nadgrids); var datumObj = json.datum || datum(json.datumCode, json.datum_params, sphere_.a, sphere_.b, ecc.es, ecc.ep2, diff --git a/proj4js-master/lib/common/pj_inv_mlfn.js b/proj4js-master/lib/common/pj_inv_mlfn.js index 7f919319..9b4d5077 100644 --- a/proj4js-master/lib/common/pj_inv_mlfn.js +++ b/proj4js-master/lib/common/pj_inv_mlfn.js @@ -4,6 +4,7 @@ import {EPSLN} from '../constants/values'; var MAX_ITER = 20; export default function(arg, es, en) { + console.log('pjInvMlfn', arg, es, en); var k = 1 / (1 - es); var phi = arg; for (var i = MAX_ITER; i; --i) { /* rarely goes over 2 iterations */ diff --git a/proj4js-master/lib/constants/values.js b/proj4js-master/lib/constants/values.js index 2e897562..df4335e4 100644 --- a/proj4js-master/lib/constants/values.js +++ b/proj4js-master/lib/constants/values.js @@ -7,7 +7,7 @@ export var SRS_WGS84_SEMIMAJOR = 6378137.0; // only used in grid shift transfor export var SRS_WGS84_SEMIMINOR = 6356752.314; // only used in grid shift transforms export var SRS_WGS84_ESQUARED = 0.0066943799901413165; // only used in grid shift transforms export var SEC_TO_RAD = 4.84813681109535993589914102357e-6; -export var HALF_PI = Math.PI/2; +export var HALF_PI = Math.PI / 2; // ellipoid pj_set_ell.c export var SIXTH = 0.1666666666666666667; /* 1/6 */ diff --git a/proj4js-master/lib/datumUtils.js b/proj4js-master/lib/datumUtils.js index 5eb07c66..a064db7c 100644 --- a/proj4js-master/lib/datumUtils.js +++ b/proj4js-master/lib/datumUtils.js @@ -72,6 +72,7 @@ export function geodeticToGeocentric(p, es, a) { } // cs_geodetic_to_geocentric() export function geocentricToGeodetic(p, es, a, b) { + console.log('geocentricToGeodetic', p, es, a, b) /* local defintions and variables */ /* end-criterium of loop, accuracy of sin(Latitude) */ var genau = 1e-12; @@ -162,6 +163,7 @@ export function geocentricToGeodetic(p, es, a, b) { /* ellipsoidal (geodetic) latitude */ Latitude = Math.atan(SPHI / Math.abs(CPHI)); + console.log('LATITUDE A 1', SPHI, CPHI, Latitude); return { x: Longitude, y: Latitude, diff --git a/proj4js-master/lib/datum_transform.js b/proj4js-master/lib/datum_transform.js index d7b4a191..37c39641 100644 --- a/proj4js-master/lib/datum_transform.js +++ b/proj4js-master/lib/datum_transform.js @@ -55,14 +55,19 @@ export default function(source, dest, point) { // Convert to geocentric coordinates. point = geodeticToGeocentric(point, source_es, source_a); + console.log('DATUM 1 A: ', point) // Convert between datums + console.log('source.datum_params', source.datum_params) if (checkParams(source.datum_type)) { point = geocentricToWgs84(point, source.datum_type, source.datum_params); } + console.log('DATUM 1 B: ', point) if (checkParams(dest.datum_type)) { point = geocentricFromWgs84(point, dest.datum_type, dest.datum_params); } + console.log('DATUM 1 C: ', point) point = geocentricToGeodetic(point, dest_es, dest_a, dest_b); + console.log('DATUM 1 D: ', point) if (dest.datum_type === PJD_GRIDSHIFT) { var destGridShiftResult = applyGridShift(dest, true, point); diff --git a/proj4js-master/lib/deriveConstants.js b/proj4js-master/lib/deriveConstants.js index 8483b501..fd8b382a 100644 --- a/proj4js-master/lib/deriveConstants.js +++ b/proj4js-master/lib/deriveConstants.js @@ -22,7 +22,6 @@ export function eccentricity(a, b, rf, R_A) { }; } export function sphere(a, b, rf, ellps, sphere) { - console.log('SPHERE INPUT', a, b, rf, ellps, sphere) if (!a) { // do we have an ellipsoid? var ellipse = match(Ellipsoid, ellps); if (!ellipse) { diff --git a/proj4js-master/lib/includedProjections.js b/proj4js-master/lib/includedProjections.js index 3c2fdb6b..17cb595c 100644 --- a/proj4js-master/lib/includedProjections.js +++ b/proj4js-master/lib/includedProjections.js @@ -28,6 +28,7 @@ import tpers from './projections/tpers'; import geos from './projections/geos'; import eqearth from "./projections/eqearth"; import bonne from "./projections/bonne"; +import gstmerc from "./projections/gstmerc"; var projs = [ tmerc, @@ -59,7 +60,8 @@ var projs = [ tpers, geos, eqearth, - bonne + bonne, + gstmerc ]; export default function (proj4) { diff --git a/proj4js-master/lib/projString.js b/proj4js-master/lib/projString.js index 7f52a73b..53e96730 100644 --- a/proj4js-master/lib/projString.js +++ b/proj4js-master/lib/projString.js @@ -140,5 +140,6 @@ export default function(defData) { if(typeof self.datumCode === 'string' && self.datumCode !== "WGS84"){ self.datumCode = self.datumCode.toLowerCase(); } + return self; } diff --git a/proj4js-master/lib/projections/bonne.js b/proj4js-master/lib/projections/bonne.js index a32ff1ef..90b4343f 100644 --- a/proj4js-master/lib/projections/bonne.js +++ b/proj4js-master/lib/projections/bonne.js @@ -15,6 +15,7 @@ export function init() { if (Math.abs(this.phi1) < EPS10) { throw new Error(); } + console.log('BONNE INIT', this) if (this.es) { this.en = pj_enfn(this.es); this.m1 = pj_mlfn(this.phi1, this.am1 = Math.sin(this.phi1), diff --git a/proj4js-master/lib/projections/geos.js b/proj4js-master/lib/projections/geos.js index 9a440956..5c11a435 100644 --- a/proj4js-master/lib/projections/geos.js +++ b/proj4js-master/lib/projections/geos.js @@ -49,6 +49,7 @@ function forward(p) { v_z = r * Math.sin(lat); if (((this.radius_g - v_x) * v_x - v_y * v_y - v_z * v_z * this.radius_p_inv2) < 0.0) { + console.log('HEHREHREHRHERHEHRHERH ERRRROOOOOORRRRR!!!!!!!') p.x = Number.NaN; p.y = Number.NaN; return p; diff --git a/proj4js-master/lib/projections/merc.js b/proj4js-master/lib/projections/merc.js index 8fbf17b3..0117b2c7 100644 --- a/proj4js-master/lib/projections/merc.js +++ b/proj4js-master/lib/projections/merc.js @@ -5,7 +5,7 @@ import tsfnz from '../common/tsfnz'; import phi2z from '../common/phi2z'; import {FORTPI, R2D, EPSLN, HALF_PI} from '../constants/values'; export function init() { - console.log('INIIIIIIIIIIIIIIIIT', this) + // console.log('INIIIIIIIIIIIIIIIIT', this) var con = this.b / this.a; this.es = 1 - con * con; if(!('x0' in this)){ @@ -46,8 +46,6 @@ export function forward(p) { return null; } - console.log('FIIIIIIIIIIIIIIIIIRST 2', x, y, this); - var x, y; if (Math.abs(Math.abs(lat) - HALF_PI) <= EPSLN) { return null; @@ -77,8 +75,6 @@ export function inverse(p) { var y = p.y - this.y0; var lon, lat; - console.log('FIIIIIIIIIIIIIIIIIRST', x, y, this); - if (this.sphere) { lat = HALF_PI - 2 * Math.atan(Math.exp(-y / (this.a * this.k0))); } diff --git a/proj4js-master/lib/projections/omerc.js b/proj4js-master/lib/projections/omerc.js index 7c47e3ee..72627bf9 100644 --- a/proj4js-master/lib/projections/omerc.js +++ b/proj4js-master/lib/projections/omerc.js @@ -41,6 +41,7 @@ export function init() { if (gam) { gamma = (this.rectified_grid_angle * D2R); } + console.log('GAMMA START', gamma) if (alp || gam) { lamc = this.longc; @@ -117,6 +118,8 @@ export function init() { gamma0 = Math.atan(2 * Math.sin(this.B * adjust_lon(lam1 - this.lam0)) / (F - 1 / F)); gamma = alpha_c = Math.asin(D * Math.sin(gamma0)); } + + console.log('gamma', gamma) this.singam = Math.sin(gamma0); this.cosgam = Math.cos(gamma0); diff --git a/proj4js-master/lib/projections/stere.js b/proj4js-master/lib/projections/stere.js index 55bc1d6e..2118f9a1 100644 --- a/proj4js-master/lib/projections/stere.js +++ b/proj4js-master/lib/projections/stere.js @@ -19,10 +19,13 @@ export function init() { this.lat0 = this.lat0 || 0; this.long0 = this.long0 || 0; + console.log('BEGIN!!!!!!!!', this.k0) + this.coslat0 = Math.cos(this.lat0); this.sinlat0 = Math.sin(this.lat0); if (this.sphere) { if (this.k0 === 1 && !isNaN(this.lat_ts) && Math.abs(this.coslat0) <= EPSLN) { + console.log('BEGIN CASE A!') this.k0 = 0.5 * (1 + sign(this.lat0) * Math.sin(this.lat_ts)); } } @@ -43,6 +46,7 @@ export function init() { if (this.k0 === 1 && !isNaN(this.lat_ts) && Math.abs(this.coslat0) <= EPSLN && Math.abs(Math.cos(this.lat_ts)) > EPSLN) { // When k0 is 1 (default value) and lat_ts is a vaild number and lat0 is at a pole and lat_ts is not at a pole // Recalculate k0 using formula 21-35 from p161 of Snyder, 1987 + console.log('BEGIN CASE B!') this.k0 = 0.5 * this.cons * msfnz(this.e, Math.sin(this.lat_ts), Math.cos(this.lat_ts)) / tsfnz(this.e, this.con * this.lat_ts, this.con * Math.sin(this.lat_ts)); } this.ms1 = msfnz(this.e, this.sinlat0, this.coslat0); @@ -50,6 +54,7 @@ export function init() { this.cosX0 = Math.cos(this.X0); this.sinX0 = Math.sin(this.X0); } + console.log('HERHEHRHERHEHRHEHR A 2', this.k0); } // Stereographic forward equations--mapping lat,long to x,y diff --git a/proj4js-master/lib/projections/tmerc.js b/proj4js-master/lib/projections/tmerc.js index 738c3927..8a778357 100644 --- a/proj4js-master/lib/projections/tmerc.js +++ b/proj4js-master/lib/projections/tmerc.js @@ -66,6 +66,7 @@ export function forward(p) { } } else { + console.log('ELSE', this.en, this.a, this.x0, this.k0, this.ep2, this.es, this.ml0, this.y0); var al = cos_phi * delta_lon; var als = Math.pow(al, 2); var c = this.ep2 * Math.pow(cos_phi, 2); diff --git a/proj4js-master/lib/projections/utm.js b/proj4js-master/lib/projections/utm.js index 5b126a00..60ec67b3 100644 --- a/proj4js-master/lib/projections/utm.js +++ b/proj4js-master/lib/projections/utm.js @@ -10,7 +10,9 @@ export function init() { throw new Error('unknown utm zone'); } this.lat0 = 0; + console.log('ZOME', this.zone, zone, this.long0); this.long0 = ((6 * Math.abs(zone)) - 183) * D2R; + console.log('ZONE AFTER', this.zone, zone, this.long0); this.x0 = 500000; this.y0 = this.utmSouth ? 10000000 : 0; this.k0 = 0.9996; diff --git a/proj4js-master/lib/transform.js b/proj4js-master/lib/transform.js index 81cced26..07deae26 100644 --- a/proj4js-master/lib/transform.js +++ b/proj4js-master/lib/transform.js @@ -12,7 +12,7 @@ function checkNotWGS(source, dest) { } export default function transform(source, dest, point, enforceAxis) { - // console.log('TRANFORM', source, dest, point, enforceAxis) + console.log('BEGIN TRANSFORM', source, dest, enforceAxis) var wgs84; if (Array.isArray(point)) { point = toPoint(point); @@ -30,6 +30,7 @@ export default function transform(source, dest, point, enforceAxis) { // Workaround for datum shifts towgs84, if either source or destination projection is not wgs84 if (source.datum && dest.datum && checkNotWGS(source, dest)) { wgs84 = new proj('WGS84'); + console.log('THIS IS CALLED A') point = transform(source, wgs84, point, enforceAxis); source = wgs84; } @@ -46,6 +47,7 @@ export default function transform(source, dest, point, enforceAxis) { }; } else { if (source.to_meter) { + console.log('METER!', source.to_meter) point = { x: point.x * source.to_meter, y: point.y * source.to_meter, @@ -56,6 +58,7 @@ export default function transform(source, dest, point, enforceAxis) { if (!point) { return; } + console.log('STEP 1: INVERSE A', point) } // Adjust for the prime meridian if necessary if (source.from_greenwich) { @@ -68,6 +71,8 @@ export default function transform(source, dest, point, enforceAxis) { return; } + console.log('STEP 2: MID DATUM A', point); + // Adjust for the prime meridian if necessary if (dest.from_greenwich) { point = { @@ -85,7 +90,9 @@ export default function transform(source, dest, point, enforceAxis) { z: point.z || 0 }; } else { // else project + // console.log('STEP 3: FORWARD A DEST: ', dest); point = dest.forward(point); + console.log('STEP 3: FORWARD A', point); if (dest.to_meter) { point = { x: point.x / dest.to_meter, @@ -94,6 +101,7 @@ export default function transform(source, dest, point, enforceAxis) { }; } } + console.log('STEP 4: METER A', point); // DGR, 2010/11/12 if (enforceAxis && dest.axis !== 'enu') { diff --git a/src/converters/index.ts b/src/converters/index.ts index e69de29b..d11748f0 100644 --- a/src/converters/index.ts +++ b/src/converters/index.ts @@ -0,0 +1 @@ +export * from './toJSON'; diff --git a/src/converters/toJSON/index.ts b/src/converters/toJSON/index.ts new file mode 100644 index 00000000..125b669e --- /dev/null +++ b/src/converters/toJSON/index.ts @@ -0,0 +1,96 @@ +import { convert } from '../../geometry/convert'; +import { mergeBBoxes } from '../../geometry'; + +import type { FeatureIterator } from 's2-tools/readers'; +import type { Writer } from '../../writers'; +import type { BBOX, Projection, VectorFeatures } from '../../geometry'; + +/** User defined options on how to store the features */ +export interface Options { + projection?: Projection; + buildBBox?: boolean; + onFeature?: (feature: VectorFeatures) => VectorFeatures | undefined; +} + +/** + * @param writer - the writer to append strings to + * @param iterators - the collection of iterators to write + * @param opts - user defined options [optional] + */ +export async function toJSON( + writer: Writer, + iterators: FeatureIterator[], + opts?: Options, +): Promise { + const projection = opts?.projection ?? 'S2'; + const type = projection === 'S2' ? 'S2FeatureCollection' : 'FeatureCollection'; + const faces = new Set(); + const buildBBox = opts?.buildBBox ?? false; + let bbox: BBOX | undefined; + const onFeature = opts?.onFeature ?? ((feature) => feature); + + await writer.appendString(`{\n\t"type": "${type}",\n`); + await writer.appendString('\t"features": [\n'); + + let first = true; + for (const iterator of iterators) { + for await (const feature of iterator) { + const convertedFeatures = convert( + projection, + feature, + undefined, + undefined, + buildBBox, + false, + ); + for (const convertedFeature of convertedFeatures) { + const userFeature = onFeature(convertedFeature); + if (userFeature === undefined) continue; + faces.add(userFeature.face ?? 0); + if (buildBBox && userFeature.geometry.bbox !== undefined) + bbox = mergeBBoxes(bbox, userFeature.geometry.bbox); + if (!first) await writer.appendString(',\n'); + first = false; + await writer.appendString(`\t\t${JSON.stringify(userFeature)}`); + } + } + } + + await writer.appendString('\n\t],'); + await writer.appendString(`\n\t"faces": ${JSON.stringify([...faces])}`); + if (bbox) await writer.appendString(`,\n\t"bbox": ${JSON.stringify(bbox)}`); + await writer.appendString('\n}'); +} + +/** + * @param writer - the writer to apppend strings to + * @param iterators - the collection of iterators to write + * @param opts - user defined options [optional] + */ +export async function toJSONLD( + writer: Writer, + iterators: FeatureIterator[], + opts?: Options, +): Promise { + const projection = opts?.projection ?? 'S2'; + const onFeature = opts?.onFeature ?? ((feature) => feature); + const buildBBox = opts?.buildBBox ?? false; + + for (const iterator of iterators) { + for await (const feature of iterator) { + const convertedFeatures = convert( + projection, + feature, + undefined, + undefined, + buildBBox, + false, + ); + for (const convertedFeature of convertedFeatures) { + const userFeature = onFeature(convertedFeature); + if (userFeature === undefined) continue; + writer.appendString(JSON.stringify(userFeature) + '\n'); + } + } + } +} diff --git a/src/converters/toVectorTiles/index.ts b/src/converters/toVectorTiles/index.ts new file mode 100644 index 00000000..5cebeb31 --- /dev/null +++ b/src/converters/toVectorTiles/index.ts @@ -0,0 +1,121 @@ +import { MetadataBuilder } from 's2-tilejson'; + +import type { Reader } from '../../readers'; +import type { VectorFeatures } from '../../geometry'; +import type { Attribution, Encoding, LayerMetaData, Scheme } from 's2-tilejson'; +import type { TileWriter, Writer } from '../../writers'; + +import type { ClusterOptions } from '../../dataStructures/pointCluster'; + +/** A layer defines the exact mechanics of what data to parse and how the data is stored */ +export interface Layer { + /** Name of the layer */ + name: string; + /** Components of how the layer is built and stored */ + metadata: LayerMetaData; +} + +/** Sources are the blueprints of what data to fetch and how to store it */ +export interface Source { + /** The reader to parse the data from */ + data: Reader; + /** If options are provided, the assumption is the point data is clustered */ + cluster?: ClusterOptions; + /** Before tiling the data, you can mutate it here. It can also act as a filter if you return undefined */ + onFeature?: (feature: VectorFeatures) => VectorFeatures | undefined; + /** The layers to construct and organize the data around for this source */ + layers: Layer[]; +} + +/** A user defined guide on building the vector tiles */ +export interface BuildGuide { + /** The name of the data */ + name: string; + /** The description of the data */ + description?: string; + /** User defined versioning for their data */ + version?: string; + /** + * What kind of output format should be used. Used for describing either S2 or WM + * projections [Default: 'fzxy'] + */ + scheme?: Scheme; + /** The encoding format. Can be either 'gz', 'br', 'zstd' or 'none' [Default: 'gz'] */ + encoding?: Encoding; + /** The sources that the tile is built from and how the layers are to be stored */ + sources: Source[]; + /** The attribution of the data. Store as [key: presentation name]: [value: href link] */ + attribution?: Attribution; + /** + * The vector format if applicable helps define how the vector data is stored. + * - The more modern vector format is the 'open-v2' which supports things like m-values + * and 3D geometries. + * - The legacy vector format is the 'open-v1' which only supports 2D geometries and works on + * older map engines like Mapbox-gl-js. + */ + vectorFormat?: 'open-v2' | 'open-v1'; + /** + * The data created will be stored in either a folder structure or a pmtiles file + * Folder structure is either '{face}/{zoom}/{x}/{y}.pbf' or '{zoom}/{x}/{y}.pbf'. + * PMTiles store all data in a single data file. + */ + tileWriter: TileWriter; + /** Explain to the module what kind of writer to use (A buffer or file writer) */ + writer: Writer; +} + +/** + * Build vector tiles give a guide on what sources to parse data from and how to store it + * @param buildGuide - the user defined guide on building the vector tiles + */ +export function toVectorTiles(buildGuide: BuildGuide): void { + const { tileWriter } = buildGuide; + + // first setup our metadata builder + const metaBuilder = new MetadataBuilder(); + updateBuilder(metaBuilder, buildGuide); + + // TODO: iterate features and have workers split/store them + + // TODO: have workers build tiles + + // FINISH: + const metadata = metaBuilder.commit(); + tileWriter.commit(metadata); +} + +/** + * @param metaBuilder - the metadata builder to update + * @param buildGuide - the user defined guide on building the vector tiles + */ +function updateBuilder(metaBuilder: MetadataBuilder, buildGuide: BuildGuide): void { + const { name, description, version, scheme, encoding, attribution, sources } = buildGuide; + + metaBuilder.setName(name); + metaBuilder.setExtension('pbf'); + metaBuilder.setDescription(description ?? 'Built by S2-Tools'); + metaBuilder.setVersion(version ?? '1.0.0'); + metaBuilder.setScheme(scheme ?? 'fzxy'); // 'fzxy' | 'tfzxy' | 'xyz' | 'txyz' | 'tms' + metaBuilder.setType('vector'); + metaBuilder.setEncoding(encoding ?? 'gz'); // 'gz' | 'br' | 'none' + if (attribution !== undefined) { + for (const [displayName, href] of Object.entries(attribution)) { + metaBuilder.addAttribution(displayName, href); + } + } + for (const { layers } of sources) { + for (const layer of layers) { + metaBuilder.addLayer(layer.name, layer.metadata); + } + } +} + +// TODO: +// - step 1: ship individual features to workers +// - - step 1a: splite the features into tiles, requesting all tiles from range min->max of the layer +// - - step 1b: store all those features into a multimap where the key is the tile-id and the value is the features +// - step 2: build tiles from each worker +// - - step 2a: given a tile-id, retrieve the features from the multimap +// - - step 2b: build the tile from the features, gzip, etc. then ship the buffer and metadata to main thread +// - - step 2c: store metadata into metaBuilder and the buffer into the store +// finish diff --git a/src/dataStore/README.md b/src/dataStore/README.md index 9c8c7bfc..a78fd754 100644 --- a/src/dataStore/README.md +++ b/src/dataStore/README.md @@ -1,3 +1,3 @@ # DataBases -The value of the `kv` and `multimap` structures is that almost all GIS computations are done in two stages: write all then read. So once we get to the reading stage, the DB is essentially immutable. This allows a ton of optimization. +The value/purpose of the `kv`, `vector` and `multimap` structures is that almost all GIS computations are done in two stages: write all you data to the DB and then read requests after. So once we get to the reading stage, the DB is essentially immutable. This allows for a ton of optimization. diff --git a/src/dataStore/vector/file.ts b/src/dataStore/vector/file.ts new file mode 100644 index 00000000..e69de29b diff --git a/src/dataStore/vector/index.ts b/src/dataStore/vector/index.ts new file mode 100644 index 00000000..4bb1a2f7 --- /dev/null +++ b/src/dataStore/vector/index.ts @@ -0,0 +1,58 @@ +import type { Stringifiable } from '../'; + +/** Represents a vector store or an array */ +export interface VectorStore { + push: (value: V) => void; + get: (index: number) => V; + sort: (compareFn?: (a: V, b: V) => number) => void; + length: () => number; + values: () => IterableIterator; + [Symbol.iterator]: () => IterableIterator; +} + +/** Just a placeholder to explain what a local key-value store essentially is */ +export class Vector implements VectorStore { + #store: V[] = []; + + /** @param value - the value to store */ + push(value: V): void { + this.#store.push(value); + } + + /** + * @param index - the position in the store to get the value from + * @returns the value + */ + get(index: number): V { + return this.#store[index]; + } + + /** + * Sort the store + * @param compareFn - the compare function explaining how the data should be sorted + */ + sort(compareFn?: (a: V, b: V) => number): void { + this.#store.sort(compareFn); + } + + /** @returns the length of the store */ + length(): number { + return this.#store.length; + } + + /** + * iterate through the values + * @returns an iterator + */ + values(): IterableIterator { + return this.#store.values(); + } + + /** + * iterate through the values + * @returns an iterator + */ + [Symbol.iterator](): IterableIterator { + return this.values(); + } +} diff --git a/src/dataStructures/cache.ts b/src/dataStructures/cache.ts new file mode 100644 index 00000000..66e93d68 --- /dev/null +++ b/src/dataStructures/cache.ts @@ -0,0 +1,52 @@ +/** A cache of values with a max size to ensure that too much old data is not stored. */ +export default class Cache extends Map { + order: K[] = []; + /** + * @param maxSize - the max size of the cache before dumping old data + * @param onDelete - if provided, will be called when a value is removed + */ + constructor( + private readonly maxSize: number, + private onDelete?: (key: K, value: V) => void, + ) { + super(); + } + + /** + * @param key - the offset position in the data + * @param value - the value to store + * @returns this + */ + set(key: K, value: V): this { + // if key exists, we just update the place in the array + if (super.has(key)) this.order.splice(this.order.indexOf(key), 1); + // add the key to the start of the array + this.order.unshift(key); + while (this.order.length > this.maxSize) this.delete(this.order.pop() as K); + + return super.set(key, value); + } + + /** + * @param key - the offset position in the data + * @returns - the value if found + */ + get(key: K): V | undefined { + // update the place in the array and than get + if (super.has(key)) { + this.order.splice(this.order.indexOf(key), 1); + this.order.unshift(key); + } + return super.get(key); + } + + /** + * @param key - the offset position in the data + * @returns - true if found + */ + delete(key: K): boolean { + const value = super.get(key); + if (value !== undefined && this.onDelete) this.onDelete(key, value); + return super.delete(key); + } +} diff --git a/src/dataStructures/index.ts b/src/dataStructures/index.ts new file mode 100644 index 00000000..7b4277ba --- /dev/null +++ b/src/dataStructures/index.ts @@ -0,0 +1,10 @@ +export { default as Cache } from './cache'; +export { default as PointCluster } from './pointCluster'; +export { default as PointIndex } from './pointIndex'; +export { default as PriorityQueue } from './priorityQueue'; +export * from './tile'; + +export type * from './pointCluster'; +export type * from './pointIndex'; +export type * from '../tools/polylabel'; +export type * from './priorityQueue'; diff --git a/src/dataStructures/pointCluster.ts b/src/dataStructures/pointCluster.ts index 771b8dfe..de117a61 100644 --- a/src/dataStructures/pointCluster.ts +++ b/src/dataStructures/pointCluster.ts @@ -1,5 +1,5 @@ import { fromS2Points } from 's2-tools/geometry/s1/chordAngle'; -import PointIndex, { Point } from './pointIndex'; +import PointIndex, { PointShape as Point } from './pointIndex'; import { Tile, fromFacePosLevel, getVertices, level, range } from '../geometry'; import { addMut, @@ -134,7 +134,7 @@ export default class PointCluster { cmp: Comparitor, ): void { const radius = this.#getLevelRadius(zoom); - for (const clusterPoint of queryIndex.iterate()) { + for (const clusterPoint of queryIndex) { const { point, data: clusterData } = clusterPoint; if (clusterData.visited) continue; clusterData.visited = true; diff --git a/src/dataStructures/pointIndex.ts b/src/dataStructures/pointIndex.ts index 42f1f0cb..0e783625 100644 --- a/src/dataStructures/pointIndex.ts +++ b/src/dataStructures/pointIndex.ts @@ -8,7 +8,7 @@ import type { Uint64Cell } from '../wasm/uint64'; import type { Point3D, S2CellId } from '../geometry'; /** A point shape to be indexed */ -export class Point { +export class PointShape { /** * @param cell - the cell that defines the point * @param point - the point to track current location @@ -23,7 +23,7 @@ export class Point { /** An index of cells with radius queries */ export default class PointIndex { - #store: Point[] = []; + #store: PointShape[] = []; #unsorted: boolean = false; cellGen = new Uint64CellGenerator(); @@ -33,26 +33,24 @@ export default class PointIndex { */ insert(point: Point3D, data: T): void { const cell = this.cellGen.fromS2Point(point); - this.#store.push(new Point(cell, point, data)); + this.#store.push(new PointShape(cell, point, data)); this.#unsorted = true; } /** * iterate through the points - * @yields a point in the index + * @returns an iterator */ - *iterate(): IterableIterator> { + [Symbol.iterator](): IterableIterator> { this.#sort(); - for (const point of this.#store) { - yield point; - } + return this.#store.values(); } /** * add points from perhaps another index * @param points - array of the points to add */ - insertPoints(points: Point[]): void { + insertPoints(points: PointShape[]): void { this.#store.push(...points); this.#unsorted = true; } @@ -97,9 +95,9 @@ export default class PointIndex { * @param high - the upper bound * @returns the points in the range */ - searchRange(low: S2CellId, high: S2CellId): Point[] { + searchRange(low: S2CellId, high: S2CellId): PointShape[] { this.#sort(); - const res: Point[] = []; + const res: PointShape[] = []; let lo = this.lowerBound(low); const hiID = this.cellGen.fromBigInt(high); @@ -116,9 +114,9 @@ export default class PointIndex { * @param radius - the search radius * @returns the points within the radius */ - searchRadius(target: Point3D, radius: S1ChordAngle): Point[] { + searchRadius(target: Point3D, radius: S1ChordAngle): PointShape[] { this.#sort(); - const res: Point[] = []; + const res: PointShape[] = []; if (radius < 0) return res; const cap = fromS1ChordAngle(target, radius, undefined); for (const cell of getIntersectingCells(cap)) { diff --git a/src/dataStructures/priorityQueue.ts b/src/dataStructures/priorityQueue.ts index 79fa58e8..1c964f83 100644 --- a/src/dataStructures/priorityQueue.ts +++ b/src/dataStructures/priorityQueue.ts @@ -1,5 +1,5 @@ /** How the comparison function needs to work */ -export type CompareFunction = (a: T, b: T) => number; +export type PriorityCompare = (a: T, b: T) => number; /** A Priority Queue */ export default class PriorityQueue { @@ -10,7 +10,7 @@ export default class PriorityQueue { */ constructor( private data: T[] = [], - private compare: CompareFunction = (a: T, b: T): number => (a < b ? -1 : a > b ? 1 : 0), + private compare: PriorityCompare = (a: T, b: T): number => (a < b ? -1 : a > b ? 1 : 0), ) { this.#length = data.length; if (this.#length > 0) { diff --git a/src/geometry/bbox.ts b/src/geometry/bbox.ts index d3f1793b..083c4d86 100644 --- a/src/geometry/bbox.ts +++ b/src/geometry/bbox.ts @@ -58,8 +58,9 @@ export function extendBBox(bbox: BBOX | undefined, point: VectorPoint): BBOX { * @param b2 - the second bounding box * @returns - the merged bounding box */ -export function mergeBBoxes(b1: BBOX, b2: BBOX): BBOX { +export function mergeBBoxes(b1: BBOX | undefined, b2: BBOX): BBOX { const { min, max } = Math; + if (b1 === undefined) b1 = [...b2]; b1[0] = min(b1[0] ?? b2[0], b2[0]); b1[1] = min(b1[1] ?? b2[1], b2[1]); b1[2] = max(b1[2] ?? b2[2], b2[2]); diff --git a/src/geometry/index.ts b/src/geometry/index.ts index 4bfe9a8e..65d8d420 100644 --- a/src/geometry/index.ts +++ b/src/geometry/index.ts @@ -100,13 +100,18 @@ export interface S2Feature< export type Attributions = Record; /** Either an S2 or WG FeatureCollection */ -export type FeatureCollections = FeatureCollection | S2FeatureCollection; +export type FeatureCollections> = + | FeatureCollection + | S2FeatureCollection; /** Either an S2 or WG Feature */ -export type Features = Feature | VectorFeature | S2Feature; +export type Features> = Feature | VectorFeature | S2Feature; /** Any Vector Geometry type */ -export type VectorFeatures = VectorFeature | S2Feature; +export type VectorFeatures> = VectorFeature | S2Feature; /** All major S2JSON types */ -export type JSONCollection = FeatureCollection | S2FeatureCollection | Features; +export type JSONCollection> = + | FeatureCollection + | S2FeatureCollection + | Features; diff --git a/src/index.ts b/src/index.ts index f6a22c31..db9b7e01 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1 +1,8 @@ -export { default as S2CellGenerator } from './experimental/s2cell'; +export * from './converters'; +export * from './dataStore'; +export * from './dataStructures'; +export * from './geometry'; +export * from './proj4'; +export * from './readers'; +export * from './util'; +// export * from './writers'; diff --git a/src/proj4/common.ts b/src/proj4/common.ts index f2c291da..6b3df67f 100644 --- a/src/proj4/common.ts +++ b/src/proj4/common.ts @@ -379,8 +379,7 @@ export function phi2z(eccent: number, ts: number): number { return phi; } } - //console.log("phi2z has NoConvergence"); - return -9999; + throw new Error('phi2z has NoConvergence'); } /** diff --git a/src/proj4/constants/datum.ts b/src/proj4/constants/datum.ts deleted file mode 100644 index bdd0e680..00000000 --- a/src/proj4/constants/datum.ts +++ /dev/null @@ -1,139 +0,0 @@ -/** Description of a WGS84 datum */ -export interface ToWGS84Datum { - towgs84: string; - ellipse: string; - datumName: string; -} - -/** Description of a NADGRIDS datum */ -export interface NADGRIDSDatum { - nadgrids: string; - ellipse: string; - datumName: string; -} - -/** WGS84 Datum */ -export const wgs84: ToWGS84Datum = { - towgs84: '0,0,0', - ellipse: 'WGS84', - datumName: 'WGS84', -}; - -/** Swiss Datum */ -export const ch1903: ToWGS84Datum = { - towgs84: '674.374,15.056,405.346', - ellipse: 'bessel', - datumName: 'swiss', -}; - -/** Greek_Geodetic_Reference_System_1987 Datum */ -export const ggrs87: ToWGS84Datum = { - towgs84: '-199.87,74.79,246.62', - ellipse: 'GRS80', - datumName: 'Greek_Geodetic_Reference_System_1987', -}; - -/** North_American_Datum_1983 Datum */ -export const nad83: ToWGS84Datum = { - towgs84: '0,0,0', - ellipse: 'GRS80', - datumName: 'North_American_Datum_1983', -}; - -/** North_American_Datum_1927 Datum */ -export const nad27: NADGRIDSDatum = { - nadgrids: '@conus,@alaska,@ntv2_0.gsb,@ntv1_can.dat', - ellipse: 'clrk66', - datumName: 'North_American_Datum_1927', -}; - -/** Potsdam Rauenberg 1950 DHDN Datum */ -export const potsdam: ToWGS84Datum = { - towgs84: '598.1,73.7,418.2,0.202,0.045,-2.455,6.7', - ellipse: 'bessel', - datumName: 'Potsdam Rauenberg 1950 DHDN', -}; - -/** Carthage 1934 Tunisia Datum */ -export const carthage: ToWGS84Datum = { - towgs84: '-263.0,6.0,431.0', - ellipse: 'clark80', - datumName: 'Carthage 1934 Tunisia', -}; - -/** Hermannskogel Datum */ -export const hermannskogel: ToWGS84Datum = { - towgs84: '577.326,90.129,463.919,5.137,1.474,5.297,2.4232', - ellipse: 'bessel', - datumName: 'Hermannskogel', -}; - -/** Militar-Geographische Institut Datum */ -export const militargeographischeInstitut: ToWGS84Datum = { - towgs84: '577.326,90.129,463.919,5.137,1.474,5.297,2.4232', - ellipse: 'bessel', - datumName: 'Militar-Geographische Institut', -}; - -/** Irish National Datum */ -export const osni52: ToWGS84Datum = { - towgs84: '482.530,-130.596,564.557,-1.042,-0.214,-0.631,8.15', - ellipse: 'airy', - datumName: 'Irish National', -}; - -/** Ireland 1965 Datum */ -export const ire65: ToWGS84Datum = { - towgs84: '482.530,-130.596,564.557,-1.042,-0.214,-0.631,8.15', - ellipse: 'mod_airy', - datumName: 'Ireland 1965', -}; - -/** Rassadiran Datum */ -export const rassadiran: ToWGS84Datum = { - towgs84: '-133.63,-157.5,-158.62', - ellipse: 'intl', - datumName: 'Rassadiran', -}; - -/** New Zealand Geodetic Datum 1949 Datum */ -export const nzgd49: ToWGS84Datum = { - towgs84: '59.47,-5.04,187.44,0.47,-0.1,1.024,-4.5993', - ellipse: 'intl', - datumName: 'New Zealand Geodetic Datum 1949', -}; - -/** Airy 1830 Datum */ -export const osgb36: ToWGS84Datum = { - towgs84: '446.448,-125.157,542.060,0.1502,0.2470,0.8421,-20.4894', - ellipse: 'airy', - datumName: 'Airy 1830', -}; - -/** S-JTSK (Ferro) Datum */ -export const s_jtsk: ToWGS84Datum = { - towgs84: '589,76,480', - ellipse: 'bessel', - datumName: 'S-JTSK (Ferro)', -}; - -/** Beduaram Datum */ -export const beduaram: ToWGS84Datum = { - towgs84: '-106,-87,188', - ellipse: 'clrk80', - datumName: 'Beduaram', -}; - -/** Gunung Segara Jakarta Datum */ -export const gunung_segara: ToWGS84Datum = { - towgs84: '-403,684,41', - ellipse: 'bessel', - datumName: 'Gunung Segara Jakarta', -}; - -/** Reseau National Belge 1972 */ -export const rnb72: ToWGS84Datum = { - towgs84: '106.869,-52.2978,103.724,-0.33657,0.456955,-1.84218,1', - ellipse: 'intl', - datumName: 'Reseau National Belge 1972', -}; diff --git a/src/proj4/constants/datums.ts b/src/proj4/constants/datums.ts new file mode 100644 index 00000000..727650e9 --- /dev/null +++ b/src/proj4/constants/datums.ts @@ -0,0 +1,163 @@ +/** Description of a WGS84 datum */ +export interface ToWGS84Datum { + datumParams: number[]; + ellipse: string; + datumName: string; +} + +/** Description of a NADGRIDS datum */ +export interface NADGRIDSDatum { + nadgrids: string[]; + ellipse: string; + datumName: string; +} + +/** WGS84 Datum */ +const wgs84: ToWGS84Datum = { + datumParams: [0, 0, 0, 0, 0, 0, 0], + ellipse: 'WGS84', + datumName: 'WGS84', +}; + +/** Swiss Datum */ +const ch1903: ToWGS84Datum = { + datumParams: [674.374, 15.056, 405.346], + ellipse: 'bessel', + datumName: 'swiss', +}; + +/** Greek_Geodetic_Reference_System_1987 Datum */ +const ggrs87: ToWGS84Datum = { + datumParams: [-199.87, 74.79, 246.62], + ellipse: 'GRS80', + datumName: 'Greek_Geodetic_Reference_System_1987', +}; + +/** North_American_Datum_1983 Datum */ +const nad83: ToWGS84Datum = { + datumParams: [0, 0, 0], + ellipse: 'GRS80', + datumName: 'North_American_Datum_1983', +}; + +/** North_American_Datum_1927 Datum */ +const nad27: NADGRIDSDatum = { + nadgrids: ['@conus', '@alaska', '@ntv2_0.gsb', '@ntv1_can.dat'], + ellipse: 'clrk66', + datumName: 'North_American_Datum_1927', +}; + +/** Potsdam Rauenberg 1950 DHDN Datum */ +const potsdam: ToWGS84Datum = { + datumParams: [598.1, 73.7, 418.2, 0.202, 0.045, -2.455, 6.7], + ellipse: 'bessel', + datumName: 'Potsdam Rauenberg 1950 DHDN', +}; + +/** Carthage 1934 Tunisia Datum */ +const carthage: ToWGS84Datum = { + datumParams: [-263.0, 6.0, 431.0], + ellipse: 'clark80', + datumName: 'Carthage 1934 Tunisia', +}; + +/** Hermannskogel Datum */ +const hermannskogel: ToWGS84Datum = { + datumParams: [577.326, 90.129, 463.919, 5.137, 1.474, 5.297, 2.4232], + ellipse: 'bessel', + datumName: 'Hermannskogel', +}; + +/** Militar-Geographische Institut Datum */ +const militargeographischeInstitut: ToWGS84Datum = { + datumParams: [577.326, 90.129, 463.919, 5.137, 1.474, 5.297, 2.4232], + ellipse: 'bessel', + datumName: 'Militar-Geographische Institut', +}; + +/** Irish National Datum */ +const osni52: ToWGS84Datum = { + datumParams: [482.53, -130.596, 564.557, -1.042, -0.214, -0.631, 8.15], + ellipse: 'airy', + datumName: 'Irish National', +}; + +/** Ireland 1965 Datum */ +const ire65: ToWGS84Datum = { + datumParams: [482.53, -130.596, 564.557, -1.042, -0.214, -0.631, 8.15], + ellipse: 'mod_airy', + datumName: 'Ireland 1965', +}; + +/** Rassadiran Datum */ +const rassadiran: ToWGS84Datum = { + datumParams: [-133.63, -157.5, -158.62], + ellipse: 'intl', + datumName: 'Rassadiran', +}; + +/** New Zealand Geodetic Datum 1949 Datum */ +const nzgd49: ToWGS84Datum = { + datumParams: [59.47, -5.04, 187.44, 0.47, -0.1, 1.024, -4.5993], + ellipse: 'intl', + datumName: 'New Zealand Geodetic Datum 1949', +}; + +/** Airy 1830 Datum */ +const osgb36: ToWGS84Datum = { + datumParams: [446.448, -125.157, 542.06, 0.1502, 0.247, 0.8421, -20.4894], + ellipse: 'airy', + datumName: 'Airy 1830', +}; + +/** S-JTSK (Ferro) Datum */ +const s_jtsk: ToWGS84Datum = { + datumParams: [589, 76, 480], + ellipse: 'bessel', + datumName: 'S-JTSK (Ferro)', +}; + +/** Beduaram Datum */ +const beduaram: ToWGS84Datum = { + datumParams: [-106, -87, 188], + ellipse: 'clrk80', + datumName: 'Beduaram', +}; + +/** Gunung Segara Jakarta Datum */ +const gunung_segara: ToWGS84Datum = { + datumParams: [-403, 684, 41], + ellipse: 'bessel', + datumName: 'Gunung Segara Jakarta', +}; + +/** Reseau National Belge 1972 */ +const rnb72: ToWGS84Datum = { + datumParams: [106.869, -52.2978, 103.724, -0.33657, 0.456955, -1.84218, 1], + ellipse: 'intl', + datumName: 'Reseau National Belge 1972', +}; + +export const DATUMS: Record = { + wgs84, + ch1903, + ggrs87, + nad83, + rassadiran, + nzgd49, + osgb36, + s_jtsk, + beduaram, + potsdam, + carthage, + hermannskogel, + militargeographischeInstitut, + osni52, + ire65, + rnb72, + gunung_segara, +}; + +export const NAD_GRIDS: Record = { + nad27, +}; diff --git a/src/proj4/constants/derives.ts b/src/proj4/constants/derives.ts index d2034cbb..c5169ea0 100644 --- a/src/proj4/constants/derives.ts +++ b/src/proj4/constants/derives.ts @@ -12,7 +12,7 @@ export interface EccentricityParams { es?: number; e?: number; ep2?: number; - rA?: number; + rA?: boolean; } /** @@ -62,15 +62,13 @@ export function deriveSphere(obj: SphereParams): void { obj.b = ellipse.b; obj.rf = ellipse.rf; } - if (obj.rf && obj.b === undefined) { + if (obj.rf && !obj.b) { obj.b = (1.0 - 1.0 / obj.rf) * obj.a; - } else { - obj.b = obj.a; } - if (obj.rf === undefined) { + if (obj.rf === undefined && obj.b) { obj.rf = (obj.a - obj.b) / obj.a; } - if (obj.rf === 0 || Math.abs(obj.a - obj.b) < EPSLN) { + if (obj.rf === 0 || (obj.b && Math.abs(obj.a - obj.b) < EPSLN)) { obj.sphere = true; obj.b = obj.a; } diff --git a/src/proj4/constants/ellipsoid.ts b/src/proj4/constants/ellipsoid.ts index 467d9a76..c77ade0a 100644 --- a/src/proj4/constants/ellipsoid.ts +++ b/src/proj4/constants/ellipsoid.ts @@ -3,29 +3,26 @@ export interface Ellipsoid { /** semi-major axis */ a: number; /** semi-minor axis */ - b: number; + b?: number; /** inverse flattening */ - rf: number; + rf?: number; } /** MERIT 1983 */ const MERIT: Ellipsoid = { a: 6_378_137.0, - b: 6_356_752.31, rf: 298.257, }; /** Soviet Geodetic System 85 */ const SGS85: Ellipsoid = { a: 6_378_136.0, - b: 6_356_751.99, rf: 298.257, }; /** GRS 1980(IUGG, 1980) */ const GRS80: Ellipsoid = { a: 6_378_137.0, - b: 6_356_752.3141, rf: 298.257222101, }; @@ -40,20 +37,17 @@ const IAU76: Ellipsoid = { const airy: Ellipsoid = { a: 6_377_563.396, b: 6_356_256.91, - rf: 299.325, }; /** Appl. Physics. 1965 */ const APL4: Ellipsoid = { a: 6_378_137, - b: 6_356_752.31, rf: 298.25, }; /** Naval Weapons Lab., 1965 */ const NWL9D: Ellipsoid = { a: 6_378_145.0, - b: 6_356_760.298, rf: 298.25, }; @@ -61,41 +55,35 @@ const NWL9D: Ellipsoid = { const mod_airy: Ellipsoid = { a: 6_377_340.189, b: 6_356_034.446, - rf: 299.325, }; /** Andrae 1876 (Den., Iclnd.) */ const andrae: Ellipsoid = { a: 6_377_104.43, - b: 6_356_913.06, rf: 300.0, }; /** Australian Natl & S. Amer. 1969 */ const aust_SA: Ellipsoid = { a: 6_378_160.0, - b: 6_356_752.31, rf: 298.25, }; /** GRS 67(IUGG 1967) */ const GRS67: Ellipsoid = { a: 6_378_160.0, - b: 6_356_750.197, rf: 298.247167427, }; /** Bessel 1841 */ const bessel: Ellipsoid = { a: 6_377_397.155, - b: 6_356_078.24, rf: 299.1528128, }; /** Bessel 1841 (Namibia) */ const bess_nam: Ellipsoid = { a: 6_377_483.865, - b: 6_356_292.65, rf: 299.1528128, }; @@ -103,13 +91,11 @@ const bess_nam: Ellipsoid = { const clrk66: Ellipsoid = { a: 6_378_206.4, b: 6_356_583.8, - rf: 294.979, }; /** Clarke 1880 mod. */ const clrk80: Ellipsoid = { a: 6_378_249.145, - b: 6_356_592.34, rf: 293.4663, }; @@ -123,126 +109,108 @@ const clrk80ign: Ellipsoid = { /** Clarke 1858 */ const clrk58: Ellipsoid = { a: 6_378_293.645208759, - b: 6_356_670.51, rf: 294.2606763692654, }; /** Comm. des Poids et Mesures 1799 */ const CPM: Ellipsoid = { a: 6_375_738.7, - b: 6_356_204.15, rf: 334.29, }; /** Delambre 1810 (Belgium) */ const delmbr: Ellipsoid = { a: 6_376_428.0, - b: 6_356_225.95, rf: 311.5, }; /** Engelis 1985 */ const engelis: Ellipsoid = { a: 6_378_136.05, - b: 6_356_752.27, rf: 298.2566, }; /** Everest 1830 (Angola) */ const evrst30: Ellipsoid = { a: 6_377_276.345, - b: 6_356_626.27, rf: 300.8017, }; /** Everest 1948 */ const evrst48: Ellipsoid = { a: 6_377_304.063, - b: 6_356_622.48, rf: 300.8017, }; /** Everest 1956 */ const evrst56: Ellipsoid = { a: 6_377_301.243, - b: 6_356_622.78, rf: 300.8017, }; /** Everest 1969 */ const evrst69: Ellipsoid = { a: 6_377_295.664, - b: 6_356_628.28, rf: 300.8017, }; /** Everest (Sabah & Sarawak) */ const evrstSS: Ellipsoid = { a: 6_377_298.556, - b: 6_356_628.91, rf: 300.8017, }; /** Fischer (Mercury Datum) 1960 */ const fschr60: Ellipsoid = { a: 6_378_166.0, - b: 6_356_741.84, rf: 298.3, }; /** Fischer 1960 */ const fschr60m: Ellipsoid = { a: 6_378_155.0, - b: 6_356_733.14, rf: 298.3, }; /** Fischer 1968 */ const fschr68: Ellipsoid = { a: 6_378_150.0, - b: 6_356_728.78, rf: 298.3, }; /** Helmert 1906 */ const helmert: Ellipsoid = { a: 6_378_200.0, - b: 6_356_730.91, rf: 298.3, }; /** Hough */ const hough: Ellipsoid = { a: 6_378_270.0, - b: 6_356_735.96, rf: 297.0, }; /** International 1909 (Hayford) */ const intl: Ellipsoid = { a: 6_378_388.0, - b: 6_356_847.3, rf: 297.0, }; /** Kaula 1961 */ const kaula: Ellipsoid = { a: 6_378_163.0, - b: 6_356_751.95, rf: 298.24, }; /** Lerch 1979 */ const lerch: Ellipsoid = { a: 6_378_139.0, - b: 6_356_751.82, rf: 298.257, }; /** Maupertius 1738 */ const mprts: Ellipsoid = { a: 6_397_300.0, - b: 6_365_965.29, rf: 191.0, }; @@ -250,20 +218,17 @@ const mprts: Ellipsoid = { const new_intl: Ellipsoid = { a: 6_378_157.5, b: 6_356_772.2, - rf: 298.255, }; /** Plessis 1817 (France) */ const plessis: Ellipsoid = { a: 6_376_523.0, - b: 6_355_863.0, rf: 308.533, }; /** Krassovsky, 1942 */ const krass: Ellipsoid = { a: 6_378_245.0, - b: 6_356_738.58, rf: 298.3, }; @@ -271,41 +236,35 @@ const krass: Ellipsoid = { const SEasia: Ellipsoid = { a: 6_378_155.0, b: 6_356_773.3205, - rf: 298.257, }; /** Walbeck */ const walbeck: Ellipsoid = { a: 6_376_896.0, b: 6_355_834.8467, - rf: 302.081, }; /** WGS 60 */ const WGS60: Ellipsoid = { a: 6_378_165.0, - b: 6_356_748.16, rf: 298.3, }; /** WGS 66 */ const WGS66: Ellipsoid = { a: 6_378_145.0, - b: 6_356_746.7, rf: 298.25, }; /** WGS 72 */ const WGS7: Ellipsoid = { a: 6_378_135.0, - b: 6_356_756.81, rf: 298.26, }; /** WGS 84 */ const WGS84: Ellipsoid = { a: 6_378_137.0, - b: 6_356_752.314, rf: 298.257223563, }; @@ -313,7 +272,6 @@ const WGS84: Ellipsoid = { const SPHERE: Ellipsoid = { a: 6370997.0, b: 6370997.0, - rf: Infinity, // Indicates a sphere with no flattening }; const ellipsoids: Record = { diff --git a/src/proj4/constants/index.ts b/src/proj4/constants/index.ts index 6f391045..30e7de6f 100644 --- a/src/proj4/constants/index.ts +++ b/src/proj4/constants/index.ts @@ -1,4 +1,4 @@ -export * from './datum'; +export * from './datums'; export * from './derives'; export * from './ellipsoid'; export * from './primeMeridian'; diff --git a/src/proj4/datum.ts b/src/proj4/datum.ts index a9921f29..31ed3f87 100644 --- a/src/proj4/datum.ts +++ b/src/proj4/datum.ts @@ -1,5 +1,6 @@ import { adjustLon } from './common'; import { + DATUMS, HALF_PI, PJD_3PARAM, PJD_7PARAM, @@ -13,85 +14,86 @@ import { SRS_WGS84_SEMIMINOR, } from './constants'; +import type { DatumParams } from 's2-tools/readers/wkt'; import type { VectorPoint } from 's2-tools/geometry'; +import type { ProjectionParams, ProjectionTransform } from '.'; + +const { abs, sin, cos, sqrt, atan2, atan, PI, floor } = Math; /** - * @param datumCode - * @param datum_params - * @param a - * @param b - * @param es - * @param ep2 - * @param nadgrids + * @param params - projection specific parameters to be adjusted + * @param nadgrids - nad grid data if applicable */ -export function buildDatum(datumCode, datum_params, a, b, es, ep2, nadgrids) { - const out = {}; - - if (datumCode === undefined || datumCode === 'none') { - out.datum_type = PJD_NODATUM; +export function buildDatum(params: ProjectionParams): void { + if (params.datumCode === undefined || params.datumCode === 'none') { + params.datumType = PJD_NODATUM; } else { - out.datum_type = PJD_WGS84; + params.datumType = PJD_WGS84; + } + + // TODO: 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 (datum_params) { - out.datum_params = datum_params.map(parseFloat); - if (out.datum_params[0] !== 0 || out.datum_params[1] !== 0 || out.datum_params[2] !== 0) { - out.datum_type = PJD_3PARAM; + if (params.datumParams !== undefined) { + if (params.datumParams[0] !== 0 || params.datumParams[1] !== 0 || params.datumParams[2] !== 0) { + params.datumType = PJD_3PARAM; } - if (out.datum_params.length > 3) { + if (params.datumParams.length > 3) { if ( - out.datum_params[3] !== 0 || - out.datum_params[4] !== 0 || - out.datum_params[5] !== 0 || - out.datum_params[6] !== 0 + params.datumParams[3] !== 0 || + params.datumParams[4] !== 0 || + params.datumParams[5] !== 0 || + params.datumParams[6] !== 0 ) { - out.datum_type = PJD_7PARAM; - out.datum_params[3] *= SEC_TO_RAD; - out.datum_params[4] *= SEC_TO_RAD; - out.datum_params[5] *= SEC_TO_RAD; - out.datum_params[6] = out.datum_params[6] / 1000000.0 + 1.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; } } } - if (nadgrids) { - out.datum_type = PJD_GRIDSHIFT; - out.grids = nadgrids; - } - out.a = a; //datum object also uses these values - out.b = b; - out.es = es; - out.ep2 = ep2; - - return out; + // TODO: Just upgrade datumType if grids exists in params + // if (params.nadgrids) { + // params.datumType = PJD_GRIDSHIFT; + // params.grids = params.nadgrids; + // } } /** * @param source * @param dest */ -export function compareDatums(source, dest) { - if (source.datum_type !== dest.datum_type) { +export function compareDatums(source: ProjectionTransform, dest: ProjectionTransform): boolean { + 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) { + } else if (source.a !== dest.a || 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.datum_type === PJD_3PARAM) { + } else if (source.datumType === PJD_3PARAM) { return ( - source.datum_params[0] === dest.datum_params[0] && - source.datum_params[1] === dest.datum_params[1] && - source.datum_params[2] === dest.datum_params[2] + source.datumParams[0] === dest.datumParams[0] && + source.datumParams[1] === dest.datumParams[1] && + source.datumParams[2] === dest.datumParams[2] ); - } else if (source.datum_type === PJD_7PARAM) { + } else if (source.datumType === PJD_7PARAM) { return ( - source.datum_params[0] === dest.datum_params[0] && - source.datum_params[1] === dest.datum_params[1] && - source.datum_params[2] === dest.datum_params[2] && - source.datum_params[3] === dest.datum_params[3] && - source.datum_params[4] === dest.datum_params[4] && - source.datum_params[5] === dest.datum_params[5] && - source.datum_params[6] === dest.datum_params[6] + 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 @@ -112,69 +114,54 @@ export function compareDatums(source, dest) { * */ /** - * @param p - * @param es - * @param a + * @param p - lon-lat WGS84 point + * @param es - eccentricity + * @param a - semi-major axis */ -export function geodeticToGeocentric(p, es, a) { +export function geodeticToGeocentric(p: VectorPoint, es: number, a: number): void { let Longitude = p.x; let Latitude = p.y; const Height = p.z ? p.z : 0; //Z value not always supplied - let Rn; /* Earth radius at location */ - let Sin_Lat; /* Math.sin(Latitude) */ - let Sin2_Lat; /* Square of Math.sin(Latitude) */ - let 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. + ** 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) { - /* Latitude out of range */ - //..reportError('geocent:lat out of range:' + Latitude); - return { x: -Infinity, y: -Infinity, z: p.z }; + throw new Error('geocent:lat out of range:' + Latitude); } else if (Latitude > HALF_PI) { - /* Latitude out of range */ - return { x: Infinity, y: Infinity, z: p.z }; + throw new Error('geocent:lat out of range:' + Latitude); } - 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 = a / Math.sqrt(1.0 - es * Sin2_Lat); - return { - x: (Rn + Height) * Cos_Lat * Math.cos(Longitude), - y: (Rn + Height) * Cos_Lat * Math.sin(Longitude), - z: (Rn * (1 - es) + Height) * Sin_Lat, - }; -} // cs_geodetic_to_geocentric() + 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; +} /** - * @param p - * @param es - * @param a - * @param b + * @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) { +export function geocentricToGeodetic(p: VectorPoint, es: number, a: number, b: number): void { /* local defintions and variables */ /* end-criterium of loop, accuracy of sin(Latitude) */ const genau = 1e-12; const genau2 = genau * genau; const maxiter = 30; - let P; /* distance between semi-minor axis and location */ - let RR; /* distance between center and location */ - let CT; /* sin of geocentric latitude */ - let ST; /* cos of geocentric latitude */ let RX; let RK; let RN; /* Earth radius at location */ @@ -192,8 +179,8 @@ export function geocentricToGeodetic(p, es, a, b) { let Latitude; let Height; - P = Math.sqrt(X * X + Y * Y); - RR = Math.sqrt(X * X + Y * Y + Z * Z); + 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) { @@ -205,16 +192,12 @@ export function geocentricToGeodetic(p, es, a, b) { if (RR / a < genau) { Latitude = HALF_PI; Height = -b; - return { - x: p.x, - y: p.y, - z: p.z, - }; + return; } } else { /* ellipsoidal (geodetic) longitude * interval: -PI < Longitude <= +PI */ - Longitude = Math.atan2(Y, X); + Longitude = atan2(Y, X); } /* -------------------------------------------------------------- @@ -226,9 +209,9 @@ export function geocentricToGeodetic(p, es, a, b) { * 2*10**-7 arcsec. * -------------------------------------------------------------- */ - CT = Z / RR; - ST = P / RR; - RX = 1.0 / Math.sqrt(1.0 - es * (2.0 - es) * ST * ST); + 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; @@ -237,13 +220,13 @@ export function geocentricToGeodetic(p, es, a, b) { * until |sin(Latitude(iter)-Latitude(iter-1))| < genau */ do { iter++; - RN = a / Math.sqrt(1.0 - es * SPHI0 * SPHI0); + 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 / Math.sqrt(1.0 - RK * (2.0 - RK) * ST * ST); + 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; @@ -252,127 +235,141 @@ export function geocentricToGeodetic(p, es, a, b) { } while (SDPHI * SDPHI > genau2 && iter < maxiter); /* ellipsoidal (geodetic) latitude */ - Latitude = Math.atan(SPHI / Math.abs(CPHI)); - return { - x: Longitude, - y: Latitude, - z: Height, - }; -} // cs_geocentric_to_geodetic() - -/****************************************************************/ -// pj_geocentic_to_wgs84( p ) -// p = point to transform in geocentric coordinates (x,y,z) + Latitude = atan(SPHI / abs(CPHI)); + + p.x = Longitude; + p.y = Latitude; + p.z = Height; +} /** - 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 - * @param datum_type - * @param datum_params + * 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, datum_type, datum_params) { - if (datum_type === PJD_3PARAM) { +export function geocentricToWgs84( + p: VectorPoint, + datumType: number, + datumParams: DatumParams, +): void { + const z = p.z ?? 0; + if (datumType === PJD_3PARAM) { // if( x[io] === HUGE_VAL ) // continue; - return { - x: p.x + datum_params[0], - y: p.y + datum_params[1], - z: p.z + datum_params[2], - }; - } else if (datum_type === PJD_7PARAM) { - const Dx_BF = datum_params[0]; - const Dy_BF = datum_params[1]; - const Dz_BF = datum_params[2]; - const Rx_BF = datum_params[3]; - const Ry_BF = datum_params[4]; - const Rz_BF = datum_params[5]; - const M_BF = datum_params[6]; + 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; - return { - x: M_BF * (p.x - Rz_BF * p.y + Ry_BF * p.z) + Dx_BF, - y: M_BF * (Rz_BF * p.x + p.y - Rx_BF * p.z) + Dy_BF, - z: M_BF * (-Ry_BF * p.x + Rx_BF * p.y + p.z) + Dz_BF, - }; + 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}`); } -} // cs_geocentric_to_wgs84 +} -/****************************************************************/ -// pj_geocentic_from_wgs84() -// coordinate system definition, -// point to transform in geocentric coordinates (x,y,z) /** - * @param p - * @param datum_type - * @param datum_params + * 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, datum_type, datum_params) { - if (datum_type === PJD_3PARAM) { +export function geocentricFromWgs84( + p: VectorPoint, + datumType: number, + datumParams: DatumParams, +): void { + const z = p.z ?? 0; + if (datumType === PJD_3PARAM) { //if( x[io] === HUGE_VAL ) // continue; - return { - x: p.x - datum_params[0], - y: p.y - datum_params[1], - z: p.z - datum_params[2], - }; - } else if (datum_type === PJD_7PARAM) { - const Dx_BF = datum_params[0]; - const Dy_BF = datum_params[1]; - const Dz_BF = datum_params[2]; - const Rx_BF = datum_params[3]; - const Ry_BF = datum_params[4]; - const Rz_BF = datum_params[5]; - const M_BF = datum_params[6]; + 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 = (p.z - Dz_BF) / M_BF; + const z_tmp = (z - Dz_BF) / M_BF; //if( x[io] === HUGE_VAL ) // continue; - - return { - x: x_tmp + Rz_BF * y_tmp - Ry_BF * z_tmp, - y: -Rz_BF * x_tmp + y_tmp + Rx_BF * z_tmp, - z: Ry_BF * x_tmp - Rx_BF * y_tmp + z_tmp, - }; - } //cs_geocentric_from_wgs84() + 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}`); + } } /** - * @param type + * @param type - datum type + * @returns - true if 1 or 2 */ -function checkParams(type) { +function checkParams(type: number): boolean { return type === PJD_3PARAM || type === PJD_7PARAM; } /** * @param source * @param dest - * @param point */ -export default function (source, dest, point) { - // Short cut if the datums are identical. - if (compareDatums(source, dest)) { - return point; // in this case, zero is sucess, - // whereas cs_compare_datums returns 1 to indicate TRUE - // confusing, should fix this - } +export function checkNotWGS(source: ProjectionTransform, dest: ProjectionTransform) { + 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') + ); +} +/** + * @param point - lon-lat WGS84 point to mutate + * @param source - source projection + * @param dest - destination projection + */ +export function datumTransform( + point: VectorPoint, + source: ProjectionTransform, + dest: ProjectionTransform, +): void { + // 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.datum_type === PJD_NODATUM || dest.datum_type === PJD_NODATUM) { - return point; - } + if (source.datumType === PJD_NODATUM || dest.datumType === PJD_NODATUM) return; // If this datum requires grid shifts, then apply it to geodetic coordinates. let source_a = source.a; let source_es = source.es; - if (source.datum_type === PJD_GRIDSHIFT) { + if (source.datumType === PJD_GRIDSHIFT) { + // source const gridShiftCode = applyGridShift(source, false, point); - if (gridShiftCode !== 0) { - return undefined; - } + if (gridShiftCode !== 0) return; source_a = SRS_WGS84_SEMIMAJOR; source_es = SRS_WGS84_ESQUARED; } @@ -380,7 +377,7 @@ export default function (source, dest, point) { let dest_a = dest.a; let dest_b = dest.b; let dest_es = dest.es; - if (dest.datum_type === PJD_GRIDSHIFT) { + if (dest.datumType === PJD_GRIDSHIFT) { dest_a = SRS_WGS84_SEMIMAJOR; dest_b = SRS_WGS84_SEMIMINOR; dest_es = SRS_WGS84_ESQUARED; @@ -390,31 +387,23 @@ export default function (source, dest, point) { if ( source_es === dest_es && source_a === dest_a && - !checkParams(source.datum_type) && - !checkParams(dest.datum_type) - ) { - return point; - } + !checkParams(source.datumType) && + !checkParams(dest.datumType) + ) + return; // Convert to geocentric coordinates. - point = geodeticToGeocentric(point, source_es, source_a); + geodeticToGeocentric(point, source_es, source_a); // Convert between datums - if (checkParams(source.datum_type)) { - point = geocentricToWgs84(point, source.datum_type, source.datum_params); - } - if (checkParams(dest.datum_type)) { - point = geocentricFromWgs84(point, dest.datum_type, dest.datum_params); - } - point = geocentricToGeodetic(point, dest_es, dest_a, dest_b); + 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.datum_type === PJD_GRIDSHIFT) { + if (dest.datumType === PJD_GRIDSHIFT) { const destGridShiftResult = applyGridShift(dest, true, point); - if (destGridShiftResult !== 0) { - return undefined; - } + if (destGridShiftResult !== 0) return; } - - return point; } /** @@ -448,7 +437,7 @@ export function applyGridShift(source, inverse, point) { 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 = (Math.abs(subgrid.del[1]) + Math.abs(subgrid.del[0])) / 10000.0; + const epsilon = (abs(subgrid.del[1]) + abs(subgrid.del[0])) / 10000.0; const minX = subgrid.ll[0] - epsilon; const minY = subgrid.ll[1] - epsilon; const maxX = subgrid.ll[0] + (subgrid.lim[0] - 1) * subgrid.del[0] + epsilon; @@ -485,7 +474,7 @@ function applySubgridShift(pin, inverse, ct) { const tb = { x: pin.x, y: pin.y }; tb.x -= ct.ll[0]; tb.y -= ct.ll[1]; - tb.x = adjustLon(tb.x - Math.PI) + Math.PI; + tb.x = adjustLon(tb.x - PI) + PI; const t = nadInterpolate(tb, ct); if (inverse) { if (isNaN(t.x)) { @@ -493,8 +482,8 @@ function applySubgridShift(pin, inverse, ct) { } t.x = tb.x - t.x; t.y = tb.y - t.y; - let i = 9, - tol = 1e-12; + let i = 9; + const tol = 1e-12; let dif, del; do { del = nadInterpolate(t, ct); @@ -506,7 +495,7 @@ function applySubgridShift(pin, inverse, ct) { 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-- && Math.abs(dif.x) > tol && Math.abs(dif.y) > tol); + } while (i-- && abs(dif.x) > tol && abs(dif.y) > tol); if (i < 0) { throw new Error('Inverse grid shift iterator failed to converge.'); } @@ -527,7 +516,7 @@ function applySubgridShift(pin, inverse, ct) { */ function nadInterpolate(pin: VectorPoint, ct): VectorPoint { const t = { x: pin.x / ct.del[0], y: pin.y / ct.del[1] }; - const indx = { x: Math.floor(t.x), y: Math.floor(t.y) }; + const indx = { x: floor(t.x), y: floor(t.y) }; const frct = { x: t.x - 1.0 * indx.x, y: t.y - 1.0 * indx.y }; const val = { x: NaN, y: NaN }; let inx; diff --git a/src/proj4/parseCode.ts b/src/proj4/parseCode.ts index 0599aa1e..ba15e84f 100644 --- a/src/proj4/parseCode.ts +++ b/src/proj4/parseCode.ts @@ -1,3 +1,4 @@ +import { buildDatum } from './datum'; import { D2R, FT_TO_M, @@ -6,7 +7,9 @@ import { deriveEccentricity, deriveSphere, } from './constants'; +import { isWKTProjection, parseWKTProjection } from 's2-tools/readers/wkt'; +import type { DatumParams } from 's2-tools/readers/wkt'; import type { ProjectionParams } from './projections'; /** @@ -14,13 +17,19 @@ import type { ProjectionParams } from './projections'; * @param proj */ export function parseProjStr(code: string): ProjectionParams { - const params = code[0] === '+' ? parseProj4Str(code) : parseWktStr(code); - + const params = + code[0] === '+' ? parseProj4Str(code) : isWKTProjection(code) ? parseWKTProjection(code) : {}; // adjust params as needed deriveSphere(params); deriveEccentricity(params); params.lat1 = params.lat1 ?? params.lat0; // Lambert_Conformal_Conic_1SP, for example, needs this params.k0 = params.k ?? params.k0; + buildDatum(params); + // filter undefined values + Object.keys(params).forEach((key) => { + // @ts-expect-error - key is a string + if (params[key] === undefined) delete params[key]; + }); return params; } @@ -50,155 +59,64 @@ function parseProj4StrKeyValue(res: ProjectionParams, key: string, value: string // adjust key key = key.replace(/_([a-zA-Z0-9])/g, (_, letter) => letter.toUpperCase()); if (key === 'proj') res.name = value; - else if (['rf', 'gamma', 'x0', 'y0', 'k0', 'k', 'a', 'b'].includes(key)) - res[key as 'rf' | 'gamma' | 'x0' | 'y0' | 'k0' | 'k' | 'a' | 'b'] = parseFloat(value); - else if (['alpha', 'lat0', 'lat1', 'lat2', 'latTs'].includes(key)) - res[key as 'alpha' | 'lat0' | 'lat1' | 'lat2' | 'latTs'] = parseFloat(value) * D2R; + else if ( + [ + 'alpha', + 'lat0', + 'lat1', + 'lat2', + 'latTs', + 'fromGreenwich', + 'long0', + 'long1', + 'long2', + 'longc', + ].includes(key) + ) + res[key as 'alpha' | 'lat0' | 'lat1' | 'lat2' | 'latTs' | 'fromGreenwich'] = + parseFloat(value) * D2R; else if (['lon0', 'lon1', 'lon2', 'lonc'].includes(key)) { const modKey = key.replace('n', 'ng') as keyof ProjectionParams; res[modKey as 'long0' | 'long1' | 'long2' | 'longc'] = parseFloat(value) * D2R; - } else if (['ellps', 'datum', 'units'].includes(key)) - res[key as 'ellps' | 'datum' | 'units'] = value; - else if (key === 'noDefs') res.noDefs = true; -} - -/** - * @param code - * @param proj - */ -function parseWktStr(code: string): ProjectionParams { - // TODO: - return {} as ProjectionParams; + } else if (['ellps', 'datumCode'].includes(key)) res[key as 'ellps' | 'datumCode'] = value; + else if (key === 'datum') res.datumCode = value; + else if (['noDefs', 'noOff', 'noRot', 'noUoff', 'rA', 'utmSouth', 'approx'].includes(key)) + res[key as 'noDefs' | 'rA' | 'utmSouth' | 'approx'] = true; + else if (key === 'zone') res[key as 'zone'] = parseInt(value, 10); + else if (key === 'units') { + res.units = value; + if (value === 'us-ft') res.toMeter = US_FT_TO_M; + else if (value === 'ft') res.toMeter = FT_TO_M; + } else if (key === 'nadgrids') { + res.nadgrids = value; + if (value === '@null') res.datumCode = 'none'; + } else if (key === 'pm') { + const pm = PRIME_MERIDIAN[value as keyof typeof PRIME_MERIDIAN] ?? 0; + res.fromGreenwich = pm * D2R; + } else if (key === 'towgs84' || key === 'datumParams') { + res.datumParams = value.split(',').map((a) => parseFloat(a)) as DatumParams; + } else if (key === 'axis') { + const legalAxis = 'ewnsud'; + if ( + value.length === 3 && + legalAxis.indexOf(value.slice(0, 1)) !== -1 && + legalAxis.indexOf(value.slice(1, 2)) !== -1 && + legalAxis.indexOf(value.slice(2, 3)) !== -1 + ) { + res.axis = value; + } + } else if (key === 'r' || key === 'R') { + res.a = res.b = parseFloat(value); + } else if (key === 'gamma') { + res.rectifiedGridAngle = parseFloat(value); + } else if (key === 'sweep') { + res.sweep = value === 'x' ? 'x' : 'y'; + } else if (!isNaN(value as unknown as number)) + // @ts-expect-error - key is a string in ProjectionParams + res[key as keyof ProjectionParams] = parseFloat(value); + // @ts-expect-error - key is a string in ProjectionParams + else res[key as keyof ProjectionParams] = value; } -// /** -// * @param defData -// */ -// export function parseProjString(defData) { -// const self = {}; -// const paramObj = defData -// .split('+') -// .map(function (v) { -// return v.trim(); -// }) -// .filter(function (a) { -// return a; -// }) -// .reduce(function (p, a) { -// const split = a.split('='); -// split.push(true); -// p[split[0].toLowerCase()] = split[1]; -// return p; -// }, {}); -// let paramName, paramVal, paramOutname; -// const params = { -// proj: 'projName', -// datum: 'datumCode', - -// /** -// * -// */ -// r_a: function () { -// self.R_A = true; -// }, -// /** -// * @param v -// */ -// zone: function (v) { -// self.zone = parseInt(v, 10); -// }, -// /** -// * -// */ -// south: function () { -// self.utmSouth = true; -// }, -// /** -// * @param v -// */ -// towgs84: function (v) { -// self.datum_params = v.split(',').map(function (a) { -// return parseFloat(a); -// }); -// }, -// /** -// * @param v -// */ -// to_meter: function (v) { -// self.to_meter = parseFloat(v); -// }, -// /** -// * @param v -// */ -// units: function (v) { -// self.units = v; -// // units: -// // ft: {to_meter: 0.3048}, -// // 'us-ft': {to_meter: 1200 / 3937} -// const unit = match(units, v); -// if (unit) { -// self.to_meter = unit.to_meter; -// } -// }, -// /** -// * @param v -// */ -// from_greenwich: function (v) { -// self.from_greenwich = v * D2R; -// }, -// /** -// * @param v -// */ -// pm: function (v) { -// const pm = match(PRIME_MERIDIAN, v); -// self.from_greenwich = (pm ? pm : parseFloat(v)) * D2R; -// }, -// /** -// * @param v -// */ -// nadgrids: function (v) { -// if (v === '@null') { -// self.datumCode = 'none'; -// } else { -// self.nadgrids = v; -// } -// }, -// /** -// * @param v -// */ -// axis: function (v) { -// const legalAxis = 'ewnsud'; -// if ( -// v.length === 3 && -// legalAxis.indexOf(v.substr(0, 1)) !== -1 && -// legalAxis.indexOf(v.substr(1, 1)) !== -1 && -// legalAxis.indexOf(v.substr(2, 1)) !== -1 -// ) { -// self.axis = v; -// } -// }, -// /** -// * -// */ -// approx: function () { -// self.approx = true; -// }, -// }; -// for (paramName in paramObj) { -// paramVal = paramObj[paramName]; -// if (paramName in params) { -// paramOutname = params[paramName]; -// if (typeof paramOutname === 'function') { -// paramOutname(paramVal); -// } else { -// self[paramOutname] = paramVal; -// } -// } else { -// self[paramName] = paramVal; -// } -// } -// if (typeof self.datumCode === 'string' && self.datumCode !== 'WGS84') { -// self.datumCode = self.datumCode.toLowerCase(); -// } -// return self; -// } +// noOff: false, +// noRot: false, diff --git a/src/proj4/projections/aea.ts b/src/proj4/projections/aea.ts index 2c0831c4..2a830f60 100644 --- a/src/proj4/projections/aea.ts +++ b/src/proj4/projections/aea.ts @@ -47,7 +47,7 @@ const { abs, pow, sin, cos, sqrt, atan2, asin, log } = Math; */ export class AlbersConicEqualArea extends ProjectionBase implements ProjectionTransform { name = 'Albers_Conic_Equal_Area'; - names = [this.name, 'Albers', 'aea']; + static names = ['Albers_Conic_Equal_Area', 'Albers', 'aea']; // AlbersConicEqualArea specific variables ns0 = 0; temp = 0; @@ -109,9 +109,8 @@ export class AlbersConicEqualArea extends ProjectionBase implements ProjectionTr /** * Albers Conical Equal Area forward equations--mapping lon-lat to x-y * @param p - lon-lat WGS84 point - * @returns - an Albers Conic Equal Area point */ - forward(p: VectorPoint): VectorPoint { + forward(p: VectorPoint): void { const lon = p.x; const lat = p.y; @@ -125,15 +124,13 @@ export class AlbersConicEqualArea extends ProjectionBase implements ProjectionTr p.x = x; p.y = y; - return p; } /** * Albers Conical Equal Area inverse equations--mapping x-y to lon-lat * @param p - Albers Conic Equal Area point - * @returns - lon-lat WGS84 point */ - inverse(p: VectorPoint): VectorPoint { + inverse(p: VectorPoint): void { let rh1, qs, con, theta; let lat = 0; @@ -160,7 +157,6 @@ export class AlbersConicEqualArea extends ProjectionBase implements ProjectionTr p.x = adjustLon(theta / this.ns0 + this.long0); p.y = lat; - return p; } } diff --git a/src/proj4/projections/aeqd.ts b/src/proj4/projections/aeqd.ts index 5d40df95..03cf5c5d 100644 --- a/src/proj4/projections/aeqd.ts +++ b/src/proj4/projections/aeqd.ts @@ -45,10 +45,10 @@ const { abs, pow, sin, cos, sqrt, atan2, asin, acos, PI, tan, atan } = Math; */ export class AzimuthalEquidistant extends ProjectionBase implements ProjectionTransform { name = 'Azimuthal_Equidistant'; - static names = [this.name, 'aeqd']; + static names = ['Azimuthal_Equidistant', 'aeqd']; // AzimuthalEquidistant specific variables - sin_p12 = 0; - cos_p12 = 0; + sinP12 = 0; + cosP12 = 0; /** * Preps an Albers Conic Equal Area projection @@ -56,16 +56,15 @@ export class AzimuthalEquidistant extends ProjectionBase implements ProjectionTr */ constructor(params?: ProjectionParams) { super(params); - this.sin_p12 = sin(this.lat0); - this.cos_p12 = cos(this.lat0); + this.sinP12 = sin(this.lat0); + this.cosP12 = cos(this.lat0); } /** * Azimuthal Equidistant forward equations--mapping lon-lat to x-y * @param p - lon-lat WGS84 point - * @returns - an Azimuthal Equidistant point */ - forward(p: VectorPoint): VectorPoint { + forward(p: VectorPoint): void { const lon = p.x; const lat = p.y; const sinphi = sin(p.y); @@ -95,60 +94,60 @@ export class AzimuthalEquidistant extends ProjectionBase implements ProjectionTr s4, s5; if (this.sphere) { - if (abs(this.sin_p12 - 1) <= EPSLN) { + if (abs(this.sinP12 - 1) <= EPSLN) { //North Pole case p.x = this.x0 + this.a * (HALF_PI - lat) * sin(dlon); p.y = this.y0 - this.a * (HALF_PI - lat) * cos(dlon); - return p; - } else if (abs(this.sin_p12 + 1) <= EPSLN) { + return; + } else if (abs(this.sinP12 + 1) <= EPSLN) { //South Pole case p.x = this.x0 + this.a * (HALF_PI + lat) * sin(dlon); p.y = this.y0 + this.a * (HALF_PI + lat) * cos(dlon); - return p; + return; } else { //default case - cos_c = this.sin_p12 * sinphi + this.cos_p12 * cosphi * cos(dlon); + cos_c = this.sinP12 * sinphi + this.cosP12 * cosphi * cos(dlon); c = acos(cos_c); kp = c ? c / sin(c) : 1; p.x = this.x0 + this.a * kp * cosphi * sin(dlon); - p.y = this.y0 + this.a * kp * (this.cos_p12 * sinphi - this.sin_p12 * cosphi * cos(dlon)); - return p; + p.y = this.y0 + this.a * kp * (this.cosP12 * sinphi - this.sinP12 * cosphi * cos(dlon)); + return; } } else { e0 = e0fn(this.es); e1 = e1fn(this.es); e2 = e2fn(this.es); e3 = e3fn(this.es); - if (abs(this.sin_p12 - 1) <= EPSLN) { + if (abs(this.sinP12 - 1) <= EPSLN) { //North Pole case Mlp = this.a * mlfn(e0, e1, e2, e3, HALF_PI); Ml = this.a * mlfn(e0, e1, e2, e3, lat); p.x = this.x0 + (Mlp - Ml) * sin(dlon); p.y = this.y0 - (Mlp - Ml) * cos(dlon); - return p; - } else if (abs(this.sin_p12 + 1) <= EPSLN) { + return; + } else if (abs(this.sinP12 + 1) <= EPSLN) { //South Pole case Mlp = this.a * mlfn(e0, e1, e2, e3, HALF_PI); Ml = this.a * mlfn(e0, e1, e2, e3, lat); p.x = this.x0 + (Mlp + Ml) * sin(dlon); p.y = this.y0 + (Mlp + Ml) * cos(dlon); - return p; + return; } else { //Default case tanphi = sinphi / cosphi; - Nl1 = gN(this.a, this.e, this.sin_p12); + Nl1 = gN(this.a, this.e, this.sinP12); Nl = gN(this.a, this.e, sinphi); - psi = atan((1 - this.es) * tanphi + (this.es * Nl1 * this.sin_p12) / (Nl * cosphi)); - Az = atan2(sin(dlon), this.cos_p12 * tan(psi) - this.sin_p12 * cos(dlon)); + psi = atan((1 - this.es) * tanphi + (this.es * Nl1 * this.sinP12) / (Nl * cosphi)); + Az = atan2(sin(dlon), this.cosP12 * tan(psi) - this.sinP12 * cos(dlon)); if (Az === 0) { - s = asin(this.cos_p12 * sin(psi) - this.sin_p12 * cos(psi)); + s = asin(this.cosP12 * sin(psi) - this.sinP12 * cos(psi)); } else if (abs(abs(Az) - PI) <= EPSLN) { - s = -asin(this.cos_p12 * sin(psi) - this.sin_p12 * cos(psi)); + s = -asin(this.cosP12 * sin(psi) - this.sinP12 * cos(psi)); } else { s = asin((sin(dlon) * cos(psi)) / sin(Az)); } - G = (this.e * this.sin_p12) / sqrt(1 - this.es); - H = (this.e * this.cos_p12 * cos(Az)) / sqrt(1 - this.es); + G = (this.e * this.sinP12) / sqrt(1 - this.es); + H = (this.e * this.cosP12 * cos(Az)) / sqrt(1 - this.es); GH = G * H; Hs = H * H; s2 = s * s; @@ -165,7 +164,7 @@ export class AzimuthalEquidistant extends ProjectionBase implements ProjectionTr (s5 / 48) * GH); p.x = this.x0 + c * sin(Az); p.y = this.y0 + c * cos(Az); - return p; + return; } } } @@ -173,9 +172,8 @@ export class AzimuthalEquidistant extends ProjectionBase implements ProjectionTr /** * Azimuthal Equidistant inverse equations--mapping x-y to lon-lat * @param p - Azimuthal Equidistant point - * @returns - lon-lat WGS84 point */ - inverse(p: VectorPoint): VectorPoint { + inverse(p: VectorPoint): void { p.x -= this.x0; p.y -= this.y0; let rh, @@ -216,7 +214,7 @@ export class AzimuthalEquidistant extends ProjectionBase implements ProjectionTr if (abs(rh) <= EPSLN) { lat = this.lat0; } else { - lat = asinz(cosz * this.sin_p12 + (p.y * sinz * this.cos_p12) / rh); + lat = asinz(cosz * this.sinP12 + (p.y * sinz * this.cosP12) / rh); con = abs(this.lat0) - HALF_PI; if (abs(con) <= EPSLN) { if (this.lat0 >= 0) { @@ -225,28 +223,28 @@ export class AzimuthalEquidistant extends ProjectionBase implements ProjectionTr lon = adjustLon(this.long0 - atan2(-p.x, p.y)); } } else { - /*con = cosz - this.sin_p12 * sin(lat); + /*con = cosz - this.sinP12 * sin(lat); if ((abs(con) < EPSLN) && (abs(p.x) < EPSLN)) { //no-op, just keep the lon value as is } else { - var temp = atan2((p.x * sinz * this.cos_p12), (con * rh)); - lon = adjustLon(this.long0 + atan2((p.x * sinz * this.cos_p12), (con * rh))); + var temp = atan2((p.x * sinz * this.cosP12), (con * rh)); + lon = adjustLon(this.long0 + atan2((p.x * sinz * this.cosP12), (con * rh))); }*/ lon = adjustLon( - this.long0 + atan2(p.x * sinz, rh * this.cos_p12 * cosz - p.y * this.sin_p12 * sinz), + this.long0 + atan2(p.x * sinz, rh * this.cosP12 * cosz - p.y * this.sinP12 * sinz), ); } } p.x = lon; p.y = lat; - return p; + return; } else { e0 = e0fn(this.es); e1 = e1fn(this.es); e2 = e2fn(this.es); e3 = e3fn(this.es); - if (abs(this.sin_p12 - 1) <= EPSLN) { + if (abs(this.sinP12 - 1) <= EPSLN) { //North pole case Mlp = this.a * mlfn(e0, e1, e2, e3, HALF_PI); rh = sqrt(p.x * p.x + p.y * p.y); @@ -255,8 +253,8 @@ export class AzimuthalEquidistant extends ProjectionBase implements ProjectionTr lon = adjustLon(this.long0 + atan2(p.x, -1 * p.y)); p.x = lon; p.y = lat; - return p; - } else if (abs(this.sin_p12 + 1) <= EPSLN) { + return; + } else if (abs(this.sinP12 + 1) <= EPSLN) { //South pole case Mlp = this.a * mlfn(e0, e1, e2, e3, HALF_PI); rh = sqrt(p.x * p.x + p.y * p.y); @@ -266,26 +264,25 @@ export class AzimuthalEquidistant extends ProjectionBase implements ProjectionTr lon = adjustLon(this.long0 + atan2(p.x, p.y)); p.x = lon; p.y = lat; - return p; + return; } else { //default case rh = sqrt(p.x * p.x + p.y * p.y); Az = atan2(p.x, p.y); - N1 = gN(this.a, this.e, this.sin_p12); + N1 = gN(this.a, this.e, this.sinP12); cosAz = cos(Az); - tmp = this.e * this.cos_p12 * cosAz; + tmp = this.e * this.cosP12 * cosAz; A = (-tmp * tmp) / (1 - this.es); - B = (3 * this.es * (1 - A) * this.sin_p12 * this.cos_p12 * cosAz) / (1 - this.es); + B = (3 * this.es * (1 - A) * this.sinP12 * this.cosP12 * cosAz) / (1 - this.es); D = rh / N1; Ee = D - (A * (1 + A) * pow(D, 3)) / 6 - (B * (1 + 3 * A) * pow(D, 4)) / 24; F = 1 - (A * Ee * Ee) / 2 - (D * Ee * Ee * Ee) / 6; - psi = asin(this.sin_p12 * cos(Ee) + this.cos_p12 * sin(Ee) * cosAz); + psi = asin(this.sinP12 * cos(Ee) + this.cosP12 * sin(Ee) * cosAz); lon = adjustLon(this.long0 + asin((sin(Az) * sin(Ee)) / cos(psi))); sinpsi = sin(psi); - lat = atan2((sinpsi - this.es * F * this.sin_p12) * tan(psi), sinpsi * (1 - this.es)); + lat = atan2((sinpsi - this.es * F * this.sinP12) * tan(psi), sinpsi * (1 - this.es)); p.x = lon; p.y = lat; - return p; } } } diff --git a/src/proj4/projections/base.ts b/src/proj4/projections/base.ts index ab73b061..3eedf91f 100644 --- a/src/proj4/projections/base.ts +++ b/src/proj4/projections/base.ts @@ -1,12 +1,15 @@ -import { D2R, FT_TO_M, R2D } from '../constants'; +import { D2R, PJD_NODATUM, R2D } from '../constants'; +import type { DatumParams } from 's2-tools/readers/wkt'; import type { ProjectionTransform } from '.'; import type { VectorPoint } from 's2-tools/geometry'; /** Define the projection with all it's variable components */ export interface ProjectionParams { name?: string; - datum?: string; + PROJECTION?: string; + datumCode?: string; + datumType?: number; srsCode?: string; long0?: number; long1?: number; @@ -25,16 +28,20 @@ export interface ProjectionParams { k?: number; k0?: number; rf?: number; - rA?: number; rc?: number; es?: number; ep2?: number; alpha?: number; gamma?: number; zone?: number; + h?: number; + azi?: number; + tilt?: number; + sweep?: 'x' | 'y'; rectifiedGridAngle?: number; utmSouth?: boolean; - datumParams?: number[]; + datumParams?: DatumParams; + scaleFactor?: number; toMeter?: number; units?: string; fromGreenwich?: number; @@ -44,13 +51,20 @@ export interface ProjectionParams { sphere?: boolean; ellps?: string; noDefs?: boolean; + noOff?: boolean; + noRot?: boolean; + rA?: boolean; + projName?: string; } /** Base class for all projections */ export class ProjectionBase implements ProjectionTransform { - name: string = ''; + name = 'longlat'; + projName?: string; static names: string[] = ['longlat', 'identity']; - datum = 'none'; + datumCode = 'none'; + datumType = PJD_NODATUM; + datumParams: DatumParams = [0, 0, 0, 0, 0, 0, 0]; srsCode = ''; // these are all variables must have a default value across all projections lon0 = 0; @@ -58,31 +72,28 @@ export class ProjectionBase implements ProjectionTransform { lon2 = 0; long0 = 0; long1 = 0; - longc = 0; lat0 = 0; lat1 = 0; lat2 = 0; - latTs = 0; + latTs?: number; a = 0; b = 0; e = 0; - R = 0; x0 = 0; y0 = 0; k?: number; - k0?: number; + k0 = 1; rf = 0; - rA = 0; + rA = false; rc?: number; es = 0; ep2 = 0; alpha?: number; gamma?: number; - zone = 0; - rectifiedGridAngle = 0; + zone?: number; + rectifiedGridAngle?: number; utmSouth = false; - datumParams: number[] = []; - toMeter = FT_TO_M; + toMeter?: number; units = 'ft'; fromGreenwich = 0; approx = false; @@ -93,33 +104,24 @@ export class ProjectionBase implements ProjectionTransform { /** @param params - projection specific parameters */ constructor(params?: ProjectionParams) { - params = params ?? ({} as ProjectionParams); Object.assign(this, params ?? {}); - - // var nadgrids = getNadgrids(json.nadgrids); - // var datumObj = json.datum || datum(json.datumCode, json.datum_params, sphere_.a, sphere_.b, ecc.es, ecc.ep2, - // nadgrids); - // // add in the datum object - // this.datum = datumObj; } /** * Forward projection from x-y to lon-lat. In this case, radians to degrees * @param p - Vector Point. This is a placeholder for a lon-lat WGS84 point - * @returns - the point itself */ - forward(p: VectorPoint): VectorPoint { - const { x, y, z, m } = p; - return { x: x * R2D, y: y * R2D, z, m }; + forward(p: VectorPoint): void { + p.x *= R2D; + p.y *= R2D; } /** * Inverse projection from lon-lat to x-y. In this case, degrees to radians * @param p - Vector Point. This is a placeholder for a lon-lat WGS84 point - * @returns - the point itself */ - inverse(p: VectorPoint): VectorPoint { - const { x, y, z, m } = p; - return { x: x * D2R, y: y * D2R, z, m }; + inverse(p: VectorPoint): void { + p.x *= D2R; + p.y *= D2R; } } diff --git a/src/proj4/projections/bonne.ts b/src/proj4/projections/bonne.ts index 501e58c7..ad761709 100644 --- a/src/proj4/projections/bonne.ts +++ b/src/proj4/projections/bonne.ts @@ -45,7 +45,7 @@ const { abs, sin, cos, sqrt, atan2, tan } = Math; */ export class BonneWerner extends ProjectionBase implements ProjectionTransform { name = 'Bonne (Werner lat_1=90)'; - names = [this.name, 'bonne']; + static names = ['Bonne (Werner lat_1=90)', 'bonne_werner', 'bonne']; // BonneWernerProjection specific variables phi1 = 0; en: En = [0, 0, 0, 0, 0]; @@ -65,7 +65,7 @@ export class BonneWerner extends ProjectionBase implements ProjectionTransform { if (abs(this.phi1) < EPS10) { throw new Error('Invalid latitude'); } - if (this.es) { + if (this.ep2 !== Infinity) { this.en = pjEnfn(this.es); this.m1 = pjMlfn(this.phi1, (this.am1 = sin(this.phi1)), (c = cos(this.phi1)), this.en); this.am1 = c / (sqrt(1 - this.es * this.am1 * this.am1) * this.am1); diff --git a/src/proj4/projections/cass.ts b/src/proj4/projections/cass.ts index 4a0e3953..8ac7873b 100644 --- a/src/proj4/projections/cass.ts +++ b/src/proj4/projections/cass.ts @@ -48,7 +48,7 @@ const { abs, sin, cos, asin, atan2, tan, pow } = Math; */ export class CassiniSoldner extends ProjectionBase implements ProjectionTransform { name = 'Cassini_Soldner'; - names = [this.name, 'Cassini', 'cass']; + static names = ['Cassini_Soldner', 'Cassini', 'cass']; e0: number = 0; e1: number = 0; e2: number = 0; @@ -73,9 +73,8 @@ export class CassiniSoldner extends ProjectionBase implements ProjectionTransfor /** * Cassini Soldner forward equations--mapping lon-lat to x-y * @param p - lon-lat WGS84 point - * @returns - a Cassini Soldner point */ - forward(p: VectorPoint): VectorPoint { + forward(p: VectorPoint): void { let x, y; let lam = p.x; const phi = p.y; @@ -101,15 +100,13 @@ export class CassiniSoldner extends ProjectionBase implements ProjectionTransfor p.x = x + this.x0; p.y = y + this.y0; - return p; } /** * Cassini Soldner inverse equations--mapping x-y to lon-lat * @param p - A Cassini Soldner point - * @returns - lon-lat WGS84 point */ - inverse(p: VectorPoint): VectorPoint { + inverse(p: VectorPoint): void { p.x -= this.x0; p.y -= this.y0; const x = p.x / this.a; @@ -130,7 +127,7 @@ export class CassiniSoldner extends ProjectionBase implements ProjectionTransfor if (y < 0) { p.y *= -1; } - return p; + return; } const nl1 = gN(this.a, this.e, sin(phi1)); @@ -144,6 +141,5 @@ export class CassiniSoldner extends ProjectionBase implements ProjectionTransfor p.x = adjustLon(lam + this.long0); p.y = adjustLat(phi); - return p; } } diff --git a/src/proj4/projections/cea.ts b/src/proj4/projections/cea.ts index f1757c33..883028c4 100644 --- a/src/proj4/projections/cea.ts +++ b/src/proj4/projections/cea.ts @@ -60,9 +60,9 @@ const { sin, cos, asin } = Math; */ export class CylindricalEqualArea extends ProjectionBase implements ProjectionTransform { name = 'Equal_Area_Cylindrical'; - names = [this.name, 'cea']; + static names = ['Equal_Area_Cylindrical', 'cea']; // Equal Area Cylindrical specific variables - declare k0: number; + declare latTs: number; /** * Preps an Equal Area Cylindrical projection @@ -70,7 +70,8 @@ export class CylindricalEqualArea extends ProjectionBase implements ProjectionTr */ constructor(params?: ProjectionParams) { super(params); - if (this.sphere === undefined) { + if (this.latTs === undefined) this.latTs = 0; + if (!this.sphere) { this.k0 = msfnz(this.e, sin(this.latTs), cos(this.latTs)); } } @@ -78,9 +79,8 @@ export class CylindricalEqualArea extends ProjectionBase implements ProjectionTr /** * Equal Area Cylindrical forward equations--mapping lon-lat to x-y * @param p - lon-lat WGS84 point - * @returns - Equal Area Cylindrical point */ - forward(p: VectorPoint): VectorPoint { + forward(p: VectorPoint): void { const lon = p.x; const lat = p.y; let x, y; @@ -98,15 +98,13 @@ export class CylindricalEqualArea extends ProjectionBase implements ProjectionTr p.x = x; p.y = y; - return p; } /** * Equal Area Cylindrical inverse equations--mapping x-y to lon-lat * @param p - Equal Area Cylindrical point - * @returns - lon-lat WGS84 point */ - inverse(p: VectorPoint): VectorPoint { + inverse(p: VectorPoint): void { p.x -= this.x0; p.y -= this.y0; let lon, lat; @@ -121,6 +119,5 @@ export class CylindricalEqualArea extends ProjectionBase implements ProjectionTr p.x = lon; p.y = lat; - return p; } } diff --git a/src/proj4/projections/eqc.ts b/src/proj4/projections/eqc.ts index c5f461d6..46b9bca8 100644 --- a/src/proj4/projections/eqc.ts +++ b/src/proj4/projections/eqc.ts @@ -91,7 +91,12 @@ import type { ProjectionParams, ProjectionTransform } from '.'; */ export class EquidistantCylindrical extends ProjectionBase implements ProjectionTransform { name = 'Equidistant Cylindrical (Plate Carre)'; - names = [this.name, 'Equirectangular', 'Equidistant_Cylindrical', 'eqc']; + static names = [ + 'Equidistant Cylindrical (Plate Carre)', + 'Equirectangular', + 'Equidistant_Cylindrical', + 'eqc', + ]; // EquidistantCylindricalProjection specific variables rc: number; @@ -114,27 +119,22 @@ export class EquidistantCylindrical extends ProjectionBase implements Projection /** * EquidistantCylindricalProjection forward equations--mapping lon-lat to x-y * @param p - lon-lat WGS84 point - * @returns - EquidistantCylindricalProjection point */ - forward(p: VectorPoint): VectorPoint { + forward(p: VectorPoint): void { const { x: lon, y: lat } = p; const dlon = adjustLon(lon - this.long0); const dlat = adjustLat(lat - this.lat0); p.x = this.x0 + this.a * dlon * this.rc; p.y = this.y0 + this.a * dlat; - - return p; } /** * EquidistantCylindricalProjection inverse equations--mapping x-y to lon-lat * @param p - EquidistantCylindricalProjection point - * @returns - lon-lat WGS84 point */ - inverse(p: VectorPoint): VectorPoint { + inverse(p: VectorPoint): void { const { x, y } = p; p.x = adjustLon(this.long0 + (x - this.x0) / (this.a * this.rc)); p.y = adjustLat(this.lat0 + (y - this.y0) / this.a); - return p; } } diff --git a/src/proj4/projections/eqdc.ts b/src/proj4/projections/eqdc.ts new file mode 100644 index 00000000..6f9e527c --- /dev/null +++ b/src/proj4/projections/eqdc.ts @@ -0,0 +1,157 @@ +import { EPSLN } from '../constants'; +import { ProjectionBase } from '.'; +import { adjustLat, adjustLon, e0fn, e1fn, e2fn, e3fn, imlfn, mlfn, msfnz } from '../common'; + +import type { VectorPoint } from 's2-tools/geometry'; +import type { ProjectionParams, ProjectionTransform } from '.'; + +const { abs, sin, cos, sqrt, atan2 } = Math; + +/** + * # Equidistant Conic + * + * **Classification**: Conic + * + * **Available forms**: Forward and inverse, ellipsoidal + * + * **Defined area**: Global + * + * **Alias**: eqdc + * + * **Domain**: 2D + * + * **Input type**: Geodetic coordinates + * + * **Output type**: Projected coordinates + * + * ## Projection String + * ``` + * +proj=eqdc +lat_1=55 +lat_2=60 + * ``` + * + * ## Parameters + * + * ### Required + * - `+lat_1` (Latitude of the first standard parallel) + * - `+lat_2` (Latitude of the second standard parallel) + * + * ### Optional + * - `+lon_0` (Central meridian) + * - `+ellps` (Ellipsoid name) + * - `+R` (Radius of the sphere) + * - `+x_0` (False easting) + * - `+y_0` (False northing) + * + * ![Equidistant Conic](./images/eqdc.png) + */ +export class EquidistantConic extends ProjectionBase implements ProjectionTransform { + name = 'Equidistant_Conic'; + static names = ['Equidistant_Conic', 'eqdc']; + // EquidistantConic specific variables + rh: number; + temp: number; + g: number; + e0: number; + e1: number; + e2: number; + e3: number; + sinphi: number; + cosphi: number; + ms1: number; + ms2 = 0; + ml0: number; + ml1: number; + ml2 = 0; + ns: number; + + /** + * Preps an EquidistantConic projection + * @param params - projection specific parameters + */ + constructor(params?: ProjectionParams) { + super(params); + + if (abs(this.lat1 + this.lat2) < EPSLN) + throw new Error('Standard parallels cannot be equal and on opposite sides of the equator'); + this.lat2 = this.lat2 || this.lat1; + this.temp = this.b / this.a; + this.es = 1 - Math.pow(this.temp, 2); + this.e = Math.sqrt(this.es); + this.e0 = e0fn(this.es); + this.e1 = e1fn(this.es); + this.e2 = e2fn(this.es); + this.e3 = e3fn(this.es); + + this.sinphi = Math.sin(this.lat1); + this.cosphi = Math.cos(this.lat1); + + this.ms1 = msfnz(this.e, this.sinphi, this.cosphi); + this.ml1 = mlfn(this.e0, this.e1, this.e2, this.e3, this.lat1); + + if (abs(this.lat1 - this.lat2) < EPSLN) { + this.ns = this.sinphi; + } else { + this.sinphi = sin(this.lat2); + this.cosphi = cos(this.lat2); + this.ms2 = msfnz(this.e, this.sinphi, this.cosphi); + this.ml2 = mlfn(this.e0, this.e1, this.e2, this.e3, this.lat2); + this.ns = (this.ms1 - this.ms2) / (this.ml2 - this.ml1); + } + this.g = this.ml1 + this.ms1 / this.ns; + this.ml0 = mlfn(this.e0, this.e1, this.e2, this.e3, this.lat0); + this.rh = this.a * (this.g - this.ml0); + } + + /** + * EquidistantConic forward equations--mapping lon-lat to x-y + * @param p - lon-lat WGS84 point + */ + forward(p: VectorPoint): void { + const { x: lon, y: lat } = p; + let rh1; + if (this.sphere) { + rh1 = this.a * (this.g - lat); + } else { + const ml = mlfn(this.e0, this.e1, this.e2, this.e3, lat); + rh1 = this.a * (this.g - ml); + } + const theta = this.ns * adjustLon(lon - this.long0); + const x = this.x0 + rh1 * sin(theta); + const y = this.y0 + this.rh - rh1 * cos(theta); + p.x = x; + p.y = y; + } + + /** + * EquidistantConic inverse equations--mapping x-y to lon-lat + * @param p - EquidistantConic point + */ + inverse(p: VectorPoint): void { + p.x -= this.x0; + p.y = this.rh - p.y + this.y0; + let con, rh1, lat, lon; + if (this.ns >= 0) { + rh1 = sqrt(p.x * p.x + p.y * p.y); + con = 1; + } else { + rh1 = -sqrt(p.x * p.x + p.y * p.y); + con = -1; + } + let theta = 0; + if (rh1 !== 0) { + theta = atan2(con * p.x, con * p.y); + } + if (this.sphere) { + lon = adjustLon(this.long0 + theta / this.ns); + lat = adjustLat(this.g - rh1 / this.a); + p.x = lon; + p.y = lat; + } else { + const ml = this.g - rh1 / this.a; + lat = imlfn(ml, this.e0, this.e1, this.e2, this.e3); + lon = adjustLon(this.long0 + theta / this.ns); + p.x = lon; + p.y = lat; + } + } +} diff --git a/src/proj4/projections/eqearth.ts b/src/proj4/projections/eqearth.ts new file mode 100644 index 00000000..8d3ae5af --- /dev/null +++ b/src/proj4/projections/eqearth.ts @@ -0,0 +1,161 @@ +/** + * Copyright 2018 Bernie Jenny, Monash University, Melbourne, Australia. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * Equal Earth is a projection inspired by the Robinson projection, but unlike + * the Robinson projection retains the relative size of areas. The projection + * was designed in 2018 by Bojan Savric, Tom Patterson and Bernhard Jenny. + * + * Publication: + * Bojan Savric, Tom Patterson & Bernhard Jenny (2018). The Equal Earth map + * projection, International Journal of Geographical Information Science, + * DOI: 10.1080/13658816.2018.1504949 + * + * Code released August 2018 + * Ported to JavaScript and adapted for mapshaper-proj by Matthew Bloch August 2018 + * Modified for proj4js by Andreas Hocevar by Andreas Hocevar March 2024 + */ + +import { ProjectionBase } from '.'; +import { adjustLon } from '../common'; + +import type { VectorPoint } from 's2-tools/geometry'; +import type { ProjectionParams, ProjectionTransform } from '.'; + +const { abs, sin, cos, sqrt, asin } = Math; + +/** + * # Equal Earth + * + * **Classification**: Pseudo cylindrical + * + * **Available forms**: Forward and inverse, spherical and ellipsoidal projection + * + * **Defined area**: Global + * + * **Alias**: eqearth + * + * **Domain**: 2D + * + * **Input type**: Geodetic coordinates + * + * **Output type**: Projected coordinates + * + * ## Projection String + * ``` + * +proj=eqearth + * ``` + * + * ## Usage + * + * The Equal Earth projection is designed for world maps and retains the relative size of areas. It was inspired by the Robinson projection. + * + * Example: + * ``` + * $ echo 122 47 | proj +proj=eqearth +R=1 + * 1.55 0.89 + * ``` + * + * ## Parameters + * + * **Note**: All parameters for this projection are optional. + * + * ### Optional + * - `+lon_0` (Central meridian) + * - `+ellps` (Ellipsoid name) + * - `+R` (Radius of the sphere) + * - `+x_0` (False easting) + * - `+y_0` (False northing) + * + * ## Further Reading + * - [The Equal Earth map projection](https://www.researchgate.net/profile/Bojan_Savric2/publication/326879978_The_Equal_Earth_map_projection/links/5b69d0ae299bf14c6d951b77/The-Equal-Earth-map-projection.pdf) by Bojan Savric, Tom Patterson & Bernhard Jenny (2018) + * + * ![Equal Earth](./images/eqearth.png) + */ +export class EqualEarth extends ProjectionBase implements ProjectionTransform { + name = 'EqualEarth'; + static names = ['EqualEarth', 'Equal_Area_Cylindrical', 'eqearth', 'Equal Earth', 'Equal_Earth']; + // EqualEarth specific variables + es: number; + A1 = 1.340264; + A2 = -0.081106; + A3 = 0.000893; + A4 = 0.003796; + M = sqrt(3) / 2.0; + + /** + * Preps an EqualEarth projection + * @param params - projection specific parameters + */ + constructor(params?: ProjectionParams) { + super(params); + this.es = 0; + this.long0 = this.long0 !== undefined ? this.long0 : 0; + } + + /** + * EqualEarth forward equations--mapping lon-lat to x-y + * @param p - lon-lat WGS84 point + */ + forward(p: VectorPoint): void { + const { M, A1, A2, A3, A4, a, x0, y0, long0 } = this; + const lam = adjustLon(p.x - long0); + const phi = p.y; + const paramLat = asin(M * sin(phi)), + paramLatSq = paramLat * paramLat, + paramLatPow6 = paramLatSq * paramLatSq * paramLatSq; + p.x = + (lam * cos(paramLat)) / + (M * (A1 + 3 * A2 * paramLatSq + paramLatPow6 * (7 * A3 + 9 * A4 * paramLatSq))); + p.y = paramLat * (A1 + A2 * paramLatSq + paramLatPow6 * (A3 + A4 * paramLatSq)); + p.x = a * p.x + x0; + p.y = a * p.y + y0; + } + + /** + * EqualEarth inverse equations--mapping x-y to lon-lat + * @param p - EqualEarth point + */ + inverse(p: VectorPoint): void { + const { M, A1, A2, A3, A4, a, x0, y0, long0 } = this; + p.x = (p.x - x0) / a; + p.y = (p.y - y0) / a; + const EPS = 1e-9; + const NITER = 12; + let paramLat = p.y, + paramLatSq, + paramLatPow6, + fy, + fpy, + dlat, + i; + for (i = 0; i < NITER; ++i) { + paramLatSq = paramLat * paramLat; + paramLatPow6 = paramLatSq * paramLatSq * paramLatSq; + fy = paramLat * (A1 + A2 * paramLatSq + paramLatPow6 * (A3 + A4 * paramLatSq)) - p.y; + fpy = A1 + 3 * A2 * paramLatSq + paramLatPow6 * (7 * A3 + 9 * A4 * paramLatSq); + paramLat -= dlat = fy / fpy; + if (abs(dlat) < EPS) { + break; + } + } + paramLatSq = paramLat * paramLat; + paramLatPow6 = paramLatSq * paramLatSq * paramLatSq; + p.x = + (M * p.x * (A1 + 3 * A2 * paramLatSq + paramLatPow6 * (7 * A3 + 9 * A4 * paramLatSq))) / + cos(paramLat); + p.y = asin(sin(paramLat) / M); + p.x = adjustLon(p.x + long0); + } +} diff --git a/src/proj4/projections/equi.ts b/src/proj4/projections/equi.ts new file mode 100644 index 00000000..5fc903b1 --- /dev/null +++ b/src/proj4/projections/equi.ts @@ -0,0 +1,56 @@ +import { ProjectionBase } from '.'; +import { adjustLon } from '../common'; + +import type { VectorPoint } from 's2-tools/geometry'; +import type { ProjectionParams, ProjectionTransform } from '.'; + +/** + * EquiRectangular Projection + */ +export class EquiRectangular extends ProjectionBase implements ProjectionTransform { + name = 'EquiRectangular'; + static names = ['EquiRectangular', 'equi']; + // EquiRectangular specific variables + + /** + * Preps an EquiRectangular projection + * @param params - projection specific parameters + */ + constructor(params?: ProjectionParams) { + super(params); + + this.x0 = this.x0 ?? 0; + this.y0 = this.y0 ?? 0; + this.lat0 = this.lat0 ?? 0; + this.long0 = this.long0 ?? 0; + } + + /** + * EquiRectangular forward equations--mapping lon-lat to x-y + * @param p - lon-lat WGS84 point + */ + forward(p: VectorPoint): void { + const { a, long0, lat0, x0, y0 } = this; + const { x: lon, y: lat } = p; + const dlon = adjustLon(lon - long0); + const x = x0 + a * dlon * Math.cos(lat0); + const y = y0 + a * lat; + // this.t1 = x; + // this.t2 = Math.cos(this.lat0); + p.x = x; + p.y = y; + } + + /** + * EquiRectangular inverse equations--mapping x-y to lon-lat + * @param p - EquiRectangular point + */ + inverse(p: VectorPoint): void { + p.x -= this.x0; + p.y -= this.y0; + const lat = p.y / this.a; + const lon = adjustLon(this.long0 + p.x / (this.a * Math.cos(this.lat0))); + p.x = lon; + p.y = lat; + } +} diff --git a/src/proj4/projections/etmerc.ts b/src/proj4/projections/etmerc.ts new file mode 100644 index 00000000..0195f788 --- /dev/null +++ b/src/proj4/projections/etmerc.ts @@ -0,0 +1,193 @@ +// Heavily based on this etmerc projection implementation +// https://github.com/mbloch/mapshaper-proj/blob/master/src/projections/etmerc.js + +import { TransverseMercator } from './tmerc'; +import { adjustLon, asinhy, clens, clensCmplx, gatg, hypot, sinh } from '../common'; + +import type { VectorPoint } from 's2-tools/geometry'; +import type { ProjectionParams, ProjectionTransform } from '.'; + +const { abs, pow, sin, cos, sqrt, atan2, tan, atan } = Math; + +/** + * Extended Transverse Mercator + */ +export class ExtendedTransverseMercator extends TransverseMercator implements ProjectionTransform { + name = 'ExtendedTransverseMercator'; + static names = [ + 'ExtendedTransverseMercator', + 'Extended_Transverse_Mercator', + 'Extended Transverse Mercator', + 'etmerc', + ]; + // ExtendedTransverseMercator specific variables + Qn: number; + Zb: number; + cgb: [number, number, number, number, number, number]; + cbg: [number, number, number, number, number, number]; + utg: [number, number, number, number, number, number]; + gtu: [number, number, number, number, number, number]; + + /** + * Preps an ExtendedTransverseMercator projection + * @param params - projection specific parameters + * @param precompute - optional precompute function (used by UTM) + */ + constructor( + params?: ProjectionParams, + precompute?: (etmerc: ExtendedTransverseMercator) => void, + ) { + super(params); + if (precompute) precompute(this); + + if (!this.approx && (this.ep2 === Infinity || this.es <= 0)) { + throw new Error( + 'Incorrect elliptical usage. Try using the +approx option in the proj string, or PROJECTION["Fast_Transverse_Mercator"] in the WKT.', + ); + } + if (this.approx) { + // When '+approx' is set, use tmerc instead + this.forward = super.forward; + this.inverse = super.inverse; + } + + this.x0 = this.x0 !== undefined ? this.x0 : 0; + this.y0 = this.y0 !== undefined ? this.y0 : 0; + this.long0 = this.long0 !== undefined ? this.long0 : 0; + this.lat0 = this.lat0 !== undefined ? this.lat0 : 0; + + this.cgb = [0, 0, 0, 0, 0, 0]; + this.cbg = [0, 0, 0, 0, 0, 0]; + this.utg = [0, 0, 0, 0, 0, 0]; + this.gtu = [0, 0, 0, 0, 0, 0]; + + const f = this.es / (1 + sqrt(1 - this.es)); + const n = f / (2 - f); + let np = n; + + this.cgb[0] = + n * (2 + n * (-2 / 3 + n * (-2 + n * (116 / 45 + n * (26 / 45 + n * (-2854 / 675)))))); + this.cbg[0] = + n * (-2 + n * (2 / 3 + n * (4 / 3 + n * (-82 / 45 + n * (32 / 45 + n * (4642 / 4725)))))); + + np = np * n; + this.cgb[1] = + np * (7 / 3 + n * (-8 / 5 + n * (-227 / 45 + n * (2704 / 315 + n * (2323 / 945))))); + this.cbg[1] = + np * (5 / 3 + n * (-16 / 15 + n * (-13 / 9 + n * (904 / 315 + n * (-1522 / 945))))); + + np = np * n; + this.cgb[2] = np * (56 / 15 + n * (-136 / 35 + n * (-1262 / 105 + n * (73814 / 2835)))); + this.cbg[2] = np * (-26 / 15 + n * (34 / 21 + n * (8 / 5 + n * (-12686 / 2835)))); + + np = np * n; + this.cgb[3] = np * (4279 / 630 + n * (-332 / 35 + n * (-399572 / 14175))); + this.cbg[3] = np * (1237 / 630 + n * (-12 / 5 + n * (-24832 / 14175))); + + np = np * n; + this.cgb[4] = np * (4174 / 315 + n * (-144838 / 6237)); + this.cbg[4] = np * (-734 / 315 + n * (109598 / 31185)); + + np = np * n; + this.cgb[5] = np * (601676 / 22275); + this.cbg[5] = np * (444337 / 155925); + + np = pow(n, 2); + this.Qn = (this.k0 / (1 + n)) * (1 + np * (1 / 4 + np * (1 / 64 + np / 256))); + + this.utg[0] = + n * + (-0.5 + + n * (2 / 3 + n * (-37 / 96 + n * (1 / 360 + n * (81 / 512 + n * (-96199 / 604800)))))); + this.gtu[0] = + n * + (0.5 + n * (-2 / 3 + n * (5 / 16 + n * (41 / 180 + n * (-127 / 288 + n * (7891 / 37800)))))); + + this.utg[1] = + np * (-1 / 48 + n * (-1 / 15 + n * (437 / 1440 + n * (-46 / 105 + n * (1118711 / 3870720))))); + this.gtu[1] = + np * (13 / 48 + n * (-3 / 5 + n * (557 / 1440 + n * (281 / 630 + n * (-1983433 / 1935360))))); + + np = np * n; + this.utg[2] = np * (-17 / 480 + n * (37 / 840 + n * (209 / 4480 + n * (-5569 / 90720)))); + this.gtu[2] = np * (61 / 240 + n * (-103 / 140 + n * (15061 / 26880 + n * (167603 / 181440)))); + + np = np * n; + this.utg[3] = np * (-4397 / 161280 + n * (11 / 504 + n * (830251 / 7257600))); + this.gtu[3] = np * (49561 / 161280 + n * (-179 / 168 + n * (6601661 / 7257600))); + + np = np * n; + this.utg[4] = np * (-4583 / 161280 + n * (108847 / 3991680)); + this.gtu[4] = np * (34729 / 80640 + n * (-3418889 / 1995840)); + + np = np * n; + this.utg[5] = np * (-20648693 / 638668800); + this.gtu[5] = np * (212378941 / 319334400); + + const Z = gatg(this.cbg, this.lat0); + this.Zb = -this.Qn * (Z + clens(this.gtu, 2 * Z)); + } + + /** + * ExtendedTransverseMercator forward equations--mapping lon-lat to x-y + * @param p - lon-lat WGS84 point + */ + forward(p: VectorPoint): void { + let Ce = adjustLon(p.x - this.long0); + let Cn = p.y; + Cn = gatg(this.cbg, Cn); + const sin_Cn = sin(Cn); + const cos_Cn = cos(Cn); + const sin_Ce = sin(Ce); + const cos_Ce = cos(Ce); + Cn = atan2(sin_Cn, cos_Ce * cos_Cn); + Ce = atan2(sin_Ce * cos_Cn, hypot(sin_Cn, cos_Cn * cos_Ce)); + Ce = asinhy(tan(Ce)); + const tmp = clensCmplx(this.gtu, 2 * Cn, 2 * Ce); + Cn = Cn + tmp[0]; + Ce = Ce + tmp[1]; + let x; + let y; + if (abs(Ce) <= 2.623395162778) { + x = this.a * (this.Qn * Ce) + this.x0; + y = this.a * (this.Qn * Cn + this.Zb) + this.y0; + } else { + x = Infinity; + y = Infinity; + } + p.x = x; + p.y = y; + } + + /** + * ExtendedTransverseMercator inverse equations--mapping x-y to lon-lat + * @param p - ExtendedTransverseMercator point + */ + inverse(p: VectorPoint): void { + let Ce = (p.x - this.x0) * (1 / this.a); + let Cn = (p.y - this.y0) * (1 / this.a); + Cn = (Cn - this.Zb) / this.Qn; + Ce = Ce / this.Qn; + let lon; + let lat; + if (abs(Ce) <= 2.623395162778) { + const tmp = clensCmplx(this.utg, 2 * Cn, 2 * Ce); + Cn = Cn + tmp[0]; + Ce = Ce + tmp[1]; + Ce = atan(sinh(Ce)); + const sin_Cn = sin(Cn); + const cos_Cn = cos(Cn); + const sin_Ce = sin(Ce); + const cos_Ce = cos(Ce); + Cn = atan2(sin_Cn * cos_Ce, hypot(sin_Ce, cos_Ce * cos_Cn)); + Ce = atan2(sin_Ce, cos_Ce * cos_Cn); + lon = adjustLon(Ce + this.long0); + lat = gatg(this.cgb, Cn); + } else { + lon = Infinity; + lat = Infinity; + } + p.x = lon; + p.y = lat; + } +} diff --git a/src/proj4/projections/gauss.ts b/src/proj4/projections/gauss.ts new file mode 100644 index 00000000..d2ce9d3b --- /dev/null +++ b/src/proj4/projections/gauss.ts @@ -0,0 +1,79 @@ +import { ProjectionBase } from '.'; +import { srat } from '../common'; +import { HALF_PI, QUART_PI } from '../constants'; + +import type { VectorPoint } from 's2-tools/geometry'; +import type { ProjectionParams, ProjectionTransform } from '.'; + +const { abs, pow, sin, cos, sqrt, asin, tan, atan } = Math; + +/** + * Gauss Kruger (deprecated form of Transverse Mercator) + */ +export class GaussKruger extends ProjectionBase implements ProjectionTransform { + name = 'GaussKruger'; + static names = ['GaussKruger', 'gauss', 'Gauss Kruger', 'Gauss_Kruger']; + // GaussKruger specific variables + C: number; + phic0: number; + rc: number; + ratexp: number; + K: number; + + /** + * Preps an GaussKruger projection + * @param params - projection specific parameters + */ + constructor(params?: ProjectionParams) { + super(params); + + const sphi = sin(this.lat0); + let cphi = cos(this.lat0); + cphi *= cphi; + this.rc = sqrt(1 - this.es) / (1 - this.es * sphi * sphi); + this.C = sqrt(1 + (this.es * cphi * cphi) / (1 - this.es)); + this.phic0 = asin(sphi / this.C); + this.ratexp = 0.5 * this.C * this.e; + this.K = + tan(0.5 * this.phic0 + QUART_PI) / + (pow(tan(0.5 * this.lat0 + QUART_PI), this.C) * srat(this.e * sphi, this.ratexp)); + } + + /** + * GaussKruger forward equations--mapping lon-lat to x-y + * @param p - lon-lat WGS84 point + */ + forward(p: VectorPoint): void { + const { x: lon, y: lat } = p; + p.y = + 2 * + atan( + this.K * pow(tan(0.5 * lat + QUART_PI), this.C) * srat(this.e * sin(lat), this.ratexp), + ) - + HALF_PI; + p.x = this.C * lon; + } + + /** + * GaussKruger inverse equations--mapping x-y to lon-lat + * @param p - GaussKruger point + */ + inverse(p: VectorPoint): void { + const DEL_TOL = 1e-14; + const lon = p.x / this.C; + let lat = p.y; + const num = pow(tan(0.5 * lat + QUART_PI) / this.K, 1 / this.C); + let i = 0; + for (i = 20; i > 0; --i) { + lat = 2 * atan(num * srat(this.e * sin(p.y), -0.5 * this.e)) - HALF_PI; + if (abs(lat - p.y) < DEL_TOL) { + break; + } + p.y = lat; + } + /* convergence failed */ + if (i === 0) throw new Error('convergence failed'); + p.x = lon; + p.y = lat; + } +} diff --git a/src/proj4/projections/geocent.ts b/src/proj4/projections/geocent.ts new file mode 100644 index 00000000..1456af09 --- /dev/null +++ b/src/proj4/projections/geocent.ts @@ -0,0 +1,47 @@ +import { ProjectionBase } from '.'; +import { geocentricToGeodetic, geodeticToGeocentric } from '../datum'; + +import type { VectorPoint } from 's2-tools/geometry'; +import type { ProjectionParams, ProjectionTransform } from '.'; + +/** Geocentric Projection */ +export class Geocentric extends ProjectionBase implements ProjectionTransform { + name = 'Geocentric'; + static names = ['Geocentric', 'geocent']; + // Geocentric specific variables + + /** + * Preps an Geocentric projection + * @param params - projection specific parameters + */ + constructor(params?: ProjectionParams) { + super(params); + } + + /** + * Geocentric forward equations--mapping lon-lat to x-y + * + * 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 + */ + forward(p: VectorPoint): void { + geodeticToGeocentric(p, this.es, this.a); + } + + /** + * Geocentric inverse equations--mapping x-y to lon-lat + * @param p - Geocentric point + */ + inverse(p: VectorPoint): void { + geocentricToGeodetic(p, this.es, this.a, this.b); + } +} diff --git a/src/proj4/projections/geos.ts b/src/proj4/projections/geos.ts new file mode 100644 index 00000000..07dbd464 --- /dev/null +++ b/src/proj4/projections/geos.ts @@ -0,0 +1,198 @@ +import { ProjectionBase } from '.'; +import { hypot } from '../common'; + +import type { VectorPoint } from 's2-tools/geometry'; +import type { ProjectionParams, ProjectionTransform } from '.'; + +const { sin, cos, sqrt, tan, atan, atan2 } = Math; + +/** + * # Geostationary Satellite View (geos) + * + * **Classification**: Azimuthal + * + * **Available forms**: Forward and inverse, spherical and ellipsoidal + * + * **Defined area**: Global + * + * **Alias**: geos + * + * **Domain**: 2D + * + * **Input type**: Geodetic coordinates + * + * **Output type**: Projected coordinates + * + * ## Projection String + * ``` + * +proj=geos +h=35785831.0 +lon_0=-60 +sweep=y + * ``` + * + * ## Required Parameters + * - `+h`: The height of the satellite above the earth. + * + * ## Optional Parameters + * - `+sweep`: Sweep angle axis of the viewing instrument, can be "x" or "y" (default is "y"). + * - `+lon_0`: Central meridian (longitude of origin). + * - `+R`: Earth radius. + * - `+ellps`: Ellipsoid. + * - `+x_0`: False easting. + * - `+y_0`: False northing. + * + * ![Geostationary Satellite View](./images/geos.png) + */ +export class GeostationarySatelliteView extends ProjectionBase implements ProjectionTransform { + name = 'GeostationarySatelliteView'; + static names = [ + 'GeostationarySatelliteView', + 'Geostationary Satellite View', + 'Geostationary_Satellite', + 'geos', + ]; + // GeostationarySatelliteView specific variables + declare sweep: string; + flip_axis: number; + declare h: number; + radiusG: number; + radiusG1: number; + radiusP: number; + radiusP2: number; + radiusPInv2: number; + C: number; + shape: string; + + /** + * Preps an GeostationarySatelliteView projection + * @param params - projection specific parameters + */ + constructor(params?: ProjectionParams) { + super(params); + if (this.sweep === undefined) this.sweep = 'y'; + if (this.h === undefined) this.h = 35785831.0; + + this.flip_axis = this.sweep === 'x' ? 1 : 0; + this.radiusG1 = this.h / this.a; + + if (this.radiusG1 <= 0 || this.radiusG1 > 1e10) throw new Error('h/a out of range'); + + this.radiusG = 1.0 + this.radiusG1; + this.C = this.radiusG * this.radiusG - 1.0; + + if (this.es !== 0.0) { + const one_es = 1.0 - this.es; + const rone_es = 1 / one_es; + + this.radiusP = sqrt(one_es); + this.radiusP2 = one_es; + this.radiusPInv2 = rone_es; + + this.shape = 'ellipse'; // Use as a condition in the forward and inverse functions. + } else { + this.radiusP = 1.0; + this.radiusP2 = 1.0; + this.radiusPInv2 = 1.0; + + this.shape = 'sphere'; // Use as a condition in the forward and inverse functions. + } + } + + /** + * GeostationarySatelliteView forward equations--mapping lon-lat to x-y + * @param p - lon-lat WGS84 point + */ + forward(p: VectorPoint): void { + let { x: lon, y: lat } = p; + let tmp, v_x, v_y, v_z; + lon = lon - this.long0; + if (this.shape === 'ellipse') { + lat = atan(this.radiusP2 * tan(lat)); + const r = this.radiusP / hypot(this.radiusP * cos(lat), sin(lat)); + v_x = r * cos(lon) * cos(lat); + v_y = r * sin(lon) * cos(lat); + v_z = r * sin(lat); + if ((this.radiusG - v_x) * v_x - v_y * v_y - v_z * v_z * this.radiusPInv2 < 0.0) { + throw new Error('h/a out of range'); + } + tmp = this.radiusG - v_x; + if (this.flip_axis) { + p.x = this.radiusG1 * atan(v_y / hypot(v_z, tmp)); + p.y = this.radiusG1 * atan(v_z / tmp); + } else { + p.x = this.radiusG1 * atan(v_y / tmp); + p.y = this.radiusG1 * atan(v_z / hypot(v_y, tmp)); + } + } else if (this.shape === 'sphere') { + tmp = cos(lat); + v_x = cos(lon) * tmp; + v_y = sin(lon) * tmp; + v_z = sin(lat); + tmp = this.radiusG - v_x; + if (this.flip_axis) { + p.x = this.radiusG1 * atan(v_y / hypot(v_z, tmp)); + p.y = this.radiusG1 * atan(v_z / tmp); + } else { + p.x = this.radiusG1 * atan(v_y / tmp); + p.y = this.radiusG1 * atan(v_z / hypot(v_y, tmp)); + } + } + p.x = p.x * this.a; + p.y = p.y * this.a; + } + + /** + * GeostationarySatelliteView inverse equations--mapping x-y to lon-lat + * @param p - GeostationarySatelliteView point + */ + inverse(p: VectorPoint): void { + let v_x = -1.0; + let v_y = 0.0; + let v_z = 0.0; + let a, b, det, k; + p.x = p.x / this.a; + p.y = p.y / this.a; + if (this.shape === 'ellipse') { + if (this.flip_axis) { + v_z = tan(p.y / this.radiusG1); + v_y = tan(p.x / this.radiusG1) * hypot(1.0, v_z); + } else { + v_y = tan(p.x / this.radiusG1); + v_z = tan(p.y / this.radiusG1) * hypot(1.0, v_y); + } + const v_zp = v_z / this.radiusP; + a = v_y * v_y + v_zp * v_zp + v_x * v_x; + b = 2 * this.radiusG * v_x; + det = b * b - 4 * a * this.C; + if (det < 0.0) { + throw new Error('det < 0'); + } + k = (-b - sqrt(det)) / (2.0 * a); + v_x = this.radiusG + k * v_x; + v_y *= k; + v_z *= k; + p.x = atan2(v_y, v_x); + p.y = atan((v_z * cos(p.x)) / v_x); + p.y = atan(this.radiusPInv2 * tan(p.y)); + } else if (this.shape === 'sphere') { + if (this.flip_axis) { + v_z = tan(p.y / this.radiusG1); + v_y = tan(p.x / this.radiusG1) * sqrt(1.0 + v_z * v_z); + } else { + v_y = tan(p.x / this.radiusG1); + v_z = tan(p.y / this.radiusG1) * sqrt(1.0 + v_y * v_y); + } + a = v_y * v_y + v_z * v_z + v_x * v_x; + b = 2 * this.radiusG * v_x; + det = b * b - 4 * a * this.C; + if (det < 0.0) { + throw new Error('det < 0'); + } + k = (-b - sqrt(det)) / (2.0 * a); + v_x = this.radiusG + k * v_x; + v_y *= k; + v_z *= k; + p.x = atan2(v_y, v_x); + p.y = atan((v_z * cos(p.x)) / v_x); + } + p.x = p.x + this.long0; + } +} diff --git a/src/proj4/projections/gnom.ts b/src/proj4/projections/gnom.ts new file mode 100644 index 00000000..8b181d5d --- /dev/null +++ b/src/proj4/projections/gnom.ts @@ -0,0 +1,144 @@ +import { EPSLN } from '../constants'; +import { ProjectionBase } from '.'; +import { adjustLon, asinz } from '../common'; + +import type { VectorPoint } from 's2-tools/geometry'; +import type { ProjectionParams, ProjectionTransform } from '.'; + +const { abs, sin, cos, sqrt, atan2 } = Math; + +/** + * # Gnomonic (gnom) + * + * For a sphere, the gnomonic projection is a projection from the center of + * the sphere onto a plane tangent to the center point of the projection. + * This projects great circles to straight lines. For an ellipsoid, it is + * the limit of a doubly azimuthal projection, a projection where the + * azimuths from 2 points are preserved, as the two points merge into the + * center point. In this case, geodesics project to approximately straight + * lines (these are exactly straight if the geodesic includes the center + * point). For details, see Section 8 of :cite:`Karney2013`. + * + * **Classification**: Azimuthal + * + * **Available forms**: Forward and inverse, spherical and ellipsoidal + * + * **Defined area**: Within a quarter circumference of the center point + * + * **Alias**: gnom + * + * **Domain**: 2D + * + * **Input type**: Geodetic coordinates + * + * **Output type**: Projected coordinates + * + * ## Projection String + * ``` + * +proj=gnom +lat_0=90 +lon_0=-50 +R=6.4e6 + * ``` + * + * ## Required Parameters + * - None, all parameters are optional for this projection. + * + * ## Optional Parameters + * - `+lon_0`: Longitude of origin (central meridian). + * - `+lat_0`: Latitude of origin. + * - `+x_0`: False easting. + * - `+y_0`: False northing. + * - `+ellps`: Ellipsoid. + * - `+R`: Earth radius. + * + * Reference: + * Wolfram Mathworld "Gnomonic Projection" + * http://mathworld.wolfram.com/GnomonicProjection.html + * Accessed: 12th November 2009 + * + * ![Gnomonic](./images/gnom.png) + */ +export class Gnomonic extends ProjectionBase implements ProjectionTransform { + name = 'GnomonicProjection'; + static names = ['GnomonicProjection', 'Gnomonic Projection', 'gnom']; + // Gnomonic specific variables + rc: number; + declare phic0: number; + sinP14: number; + cosP14: number; + infinityDist: number; + + /** + * Preps an Gnomonic projection + * @param params - projection specific parameters + */ + constructor(params?: ProjectionParams) { + super(params); + + this.sinP14 = sin(this.lat0); + this.cosP14 = cos(this.lat0); + // Approximation for projecting points to the horizon (infinity) + this.infinityDist = 1_000 * this.a; + this.rc = 1; + } + + /** + * Gnomonic forward equations--mapping lon-lat to x-y + * @param p - lon-lat WGS84 point + */ + forward(p: VectorPoint): void { + const { x: lon, y: lat } = p; + let x, y; + /* Forward equations + -----------------*/ + const dlon = adjustLon(lon - this.long0); /* delta longitude value */ + const sinphi = sin(lat); + const cosphi = cos(lat); + const coslon = cos(dlon); /* cos of longitude */ + const g = this.sinP14 * sinphi + this.cosP14 * cosphi * coslon; + const ksp = 1; /* scale factor */ + if (g > 0 || abs(g) <= EPSLN) { + x = this.x0 + (this.a * ksp * cosphi * sin(dlon)) / g; + y = this.y0 + (this.a * ksp * (this.cosP14 * sinphi - this.sinP14 * cosphi * coslon)) / g; + } else { + // Point is in the opposing hemisphere and is unprojectable + // We still need to return a reasonable point, so we project + // to infinity, on a bearing + // equivalent to the northern hemisphere equivalent + // This is a reasonable approximation for short shapes and lines that + // straddle the horizon. + x = this.x0 + this.infinityDist * cosphi * sin(dlon); + y = this.y0 + this.infinityDist * (this.cosP14 * sinphi - this.sinP14 * cosphi * coslon); + } + p.x = x; + p.y = y; + } + + /** + * Gnomonic inverse equations--mapping x-y to lon-lat + * @param p - Gnomonic point + */ + inverse(p: VectorPoint): void { + let rh; /* Rho */ + let sinc, cosc; + let c; + let lon, lat; + /* Inverse equations + -----------------*/ + p.x = (p.x - this.x0) / this.a; + p.y = (p.y - this.y0) / this.a; + p.x /= this.k0; + p.y /= this.k0; + if ((rh = sqrt(p.x * p.x + p.y * p.y))) { + c = atan2(rh, this.rc); + sinc = sin(c); + cosc = cos(c); + lat = asinz(cosc * this.sinP14 + (p.y * sinc * this.cosP14) / rh); + lon = atan2(p.x * sinc, rh * this.cosP14 * cosc - p.y * this.sinP14 * sinc); + lon = adjustLon(this.long0 + lon); + } else { + lat = this.phic0; + lon = 0; + } + p.x = lon; + p.y = lat; + } +} diff --git a/src/proj4/projections/gstmerc.ts b/src/proj4/projections/gstmerc.ts new file mode 100644 index 00000000..1a66e155 --- /dev/null +++ b/src/proj4/projections/gstmerc.ts @@ -0,0 +1,124 @@ +import { ProjectionBase } from '.'; +import { cosh, invlatiso, latiso, sinh } from '../common'; + +import type { VectorPoint } from 's2-tools/geometry'; +import type { ProjectionParams, ProjectionTransform } from '.'; + +const { pow, sin, cos, sqrt, atan, asin } = Math; + +/** + * # Gauss-Schreiber Transverse Mercator (aka Gauss-Laborde Reunion) + * + * **Classification**: Conformal + * + * **Available forms**: Forward and inverse, spherical projection + * + * **Defined area**: Global + * + * **Alias**: gstmerc + * + * **Domain**: 2D + * + * **Input type**: Geodetic coordinates + * + * **Output type**: Projected coordinates + * + * ## Projection String + * ``` + * +proj=gstmerc + * ``` + * + * ## Optional Parameters + * - `+k_0=`: Scale factor at the central meridian. + * - `+lon_0=`: Longitude of the central meridian. + * - `+lat_0=`: Latitude of origin. + * - `+ellps=`: Ellipsoid name (e.g., GRS80, WGS84). + * - `+R=`: Radius of the sphere (used in spherical projections). + * - `+x_0=`: False easting. + * - `+y_0=`: False northing. + * + * ## Usage Example + * ``` + * echo 12 55 | proj +proj=gstmerc +ellps=WGS84 + * echo 12 55 | proj +proj=gstmerc +k_0=1 +lon_0=0 +x_0=500000 +y_0=0 + * ``` + * + * ![Gauss-Schreiber Transverse Mercator](./images/gstmerc.png) + */ +export class GaussSchreiberTransverseMercator + extends ProjectionBase + implements ProjectionTransform +{ + name = 'GaussSchreiberTransverseMercator'; + static names = [ + 'GaussSchreiberTransverseMercator', + 'Gauss_Schreiber_Transverse_Mercator', + 'gstmerg', + 'gstmerc', + ]; + // GaussSchreiberTransverseMercator specific variables + longc: number; + cp: number; + n1: number; + n2: number; + xs: number; + ys: number; + + /** + * TODO: This whole file is arguably wrong. https://github.com/OSGeo/PROJ/blob/e2174f8292a39cdd2f92ce8122601e9b264555c2/src/projections/gstmerc.cpp#L15 + * Preps an GaussSchreiberTransverseMercator projection + * @param params - projection specific parameters + */ + constructor(params?: ProjectionParams) { + super(params); + + const temp = this.b / this.a; + this.e = sqrt(1 - temp * temp); + this.longc = this.long0; + this.n1 = sqrt(1 + (this.es * pow(cos(this.lat0), 4)) / (1 - this.es)); + const sinz = sin(this.lat0); + const pc = asin(sinz / this.n1); + const sinzpc = sin(pc); + this.cp = latiso(0, pc, sinzpc) - this.n1 * latiso(this.e, this.lat0, sinz); + this.n2 = (this.k0 * this.a * sqrt(1 - this.es)) / (1 - this.es * sinz * sinz); + this.xs = this.x0; + this.ys = this.y0 - this.n2 * pc; + + // Q->lamc = P->lam0; + // Q->n1 = sqrt(1 + P->es * pow(cos(P->phi0), 4.0) / (1 - P->es)); + // Q->phic = asin(sin(P->phi0) / Q->n1); + // Q->c = log(pj_tsfn(-Q->phic, -sin(P->phi0) / Q->n1, 0.0)) - + // Q->n1 * log(pj_tsfn(-P->phi0, -sin(P->phi0), P->e)); + // Q->n2 = P->k0 * P->a * sqrt(1 - P->es) / + // (1 - P->es * sin(P->phi0) * sin(P->phi0)); + // Q->XS = 0; + // Q->YS = -Q->n2 * Q->phic; + } + + /** + * GaussSchreiberTransverseMercator forward equations--mapping lon-lat to x-y + * @param p - lon-lat WGS84 point + */ + forward(p: VectorPoint): void { + const { x: lon, y: lat } = p; + const L = this.n1 * (lon - this.longc); + const Ls = this.cp + this.n1 * latiso(this.e, lat, sin(lat)); + const lat1 = asin(sin(L) / cosh(Ls)); + const Ls1 = latiso(0, lat1, sin(lat1)); + p.x = this.xs + this.n2 * Ls1; + p.y = this.ys + this.n2 * atan(sinh(Ls) / cos(L)); + } + + /** + * GaussSchreiberTransverseMercator inverse equations--mapping x-y to lon-lat + * @param p - GaussSchreiberTransverseMercator point + */ + inverse(p: VectorPoint): void { + const { x, y } = p; + const L = atan(sinh((x - this.xs) / this.n2) / cos((y - this.ys) / this.n2)); + const lat1 = asin(sin((y - this.ys) / this.n2) / cosh((x - this.xs) / this.n2)); + const LC = latiso(0, lat1, sin(lat1)); + p.x = this.longc + L / this.n1; + p.y = invlatiso(this.e, (LC - this.cp) / this.n1); + } +} diff --git a/src/proj4/projections/index.ts b/src/proj4/projections/index.ts index b1f57b14..d8d1487e 100644 --- a/src/proj4/projections/index.ts +++ b/src/proj4/projections/index.ts @@ -3,11 +3,40 @@ import { AzimuthalEquidistant } from './aeqd'; import { BonneWerner } from './bonne'; import { CassiniSoldner } from './cass'; import { CylindricalEqualArea } from './cea'; +import { EqualEarth } from './eqearth'; +import { EquiRectangular } from './equi'; +import { EquidistantConic } from './eqdc'; import { EquidistantCylindrical } from './eqc'; +import { ExtendedTransverseMercator } from './etmerc'; +import { GaussKruger } from './gauss'; +import { GaussSchreiberTransverseMercator } from './gstmerc'; +import { Geocentric } from './geocent'; +import { GeostationarySatelliteView } from './geos'; +import { Gnomonic } from './gnom'; +import { HotineObliqueMercator } from './omerc'; +import { Krovak } from './krovak'; +import { LambertAzimuthalEqualArea } from './laea'; +import { LambertConformalConic } from './lcc'; import { Mercator } from './merc'; +import { MillerCylindrical } from './mill'; +import { Mollweide } from './moll'; +import { NewZealandMapGrid } from './nzmg'; +import { Orthographic } from './ortho'; +import { Polyconic } from './poly'; +import { QuadrilateralizedSphericalCube } from './qsc'; +import { Robinson } from './robin'; +import { Sinusoidal } from './sinu'; +import { StereographicNorthPole } from './sterea'; +import { StereographicSouthPole } from './stere'; +import { SwissObliqueMercator } from './somerc'; +import { TiltedPerspective } from './tpers'; +import { TransverseMercator } from './tmerc'; +import { UniversalTransverseMercator } from './utm'; +import { VanDerGrinten } from './vandg'; import { ProjectionBase } from './base'; +import type { DatumParams } from 's2-tools/readers/wkt'; import type { VectorPoint } from 's2json-spec'; export * from './aea'; @@ -17,8 +46,36 @@ export * from './bonne'; export * from './cass'; export * from './cea'; export * from './eqc'; +export * from './eqdc'; +export * from './eqearth'; +export * from './equi'; +export * from './etmerc'; +export * from './gauss'; +export * from './geocent'; +export * from './geos'; +export * from './gnom'; +export * from './gstmerc'; +export * from './krovak'; +export * from './laea'; +export * from './lcc'; export * from './merc'; +export * from './mill'; +export * from './moll'; +export * from './nzmg'; +export * from './omerc'; +export * from './ortho'; +export * from './poly'; +export * from './qsc'; export * from './references'; +export * from './robin'; +export * from './sinu'; +export * from './somerc'; +export * from './stere'; +export * from './sterea'; +export * from './tmerc'; +export * from './tpers'; +export * from './utm'; +export * from './vandg'; /** Defines a projection class that isn't instantiated yet */ export type ProjectionTransformDefinition = typeof ProjectionBase; @@ -26,8 +83,19 @@ export type ProjectionTransformDefinition = typeof ProjectionBase; /** All projections need these parameters */ export interface ProjectionTransform { name: string; - forward: (p: VectorPoint) => VectorPoint; - inverse: (p: VectorPoint) => VectorPoint; + projName?: string; + axis: string; + toMeter?: number; + fromGreenwich: number; + datum?: string; + datumCode: string; + datumType: number; + datumParams: DatumParams; + a: number; + b: number; + es: number; + forward: (p: VectorPoint) => void; + inverse: (p: VectorPoint) => void; } /** Contains all projections */ @@ -38,6 +106,34 @@ export const ALL_DEFINITIONS: ProjectionTransformDefinition[] = [ CassiniSoldner, CylindricalEqualArea, EquidistantCylindrical, + EquidistantConic, + EqualEarth, + EquiRectangular, + ExtendedTransverseMercator, + GaussKruger, + GaussSchreiberTransverseMercator, + Geocentric, + GeostationarySatelliteView, + Gnomonic, + HotineObliqueMercator, + Krovak, + LambertAzimuthalEqualArea, + LambertConformalConic, + MillerCylindrical, + Mollweide, + NewZealandMapGrid, + Orthographic, + Polyconic, + QuadrilateralizedSphericalCube, + Robinson, + Sinusoidal, + StereographicNorthPole, + StereographicSouthPole, + SwissObliqueMercator, + TiltedPerspective, + TransverseMercator, + UniversalTransverseMercator, + VanDerGrinten, ]; /** diff --git a/src/proj4/projections/krovak.ts b/src/proj4/projections/krovak.ts new file mode 100644 index 00000000..df16dcd1 --- /dev/null +++ b/src/proj4/projections/krovak.ts @@ -0,0 +1,189 @@ +import { ProjectionBase } from '.'; +import { adjustLon } from '../common'; + +import type { VectorPoint } from 's2-tools/geometry'; +import type { ProjectionParams, ProjectionTransform } from '.'; + +const { abs, pow, sin, cos, sqrt, atan2, asin, tan, atan } = Math; + +/** + * # Krovak + * + * **Classification**: Conformal Conical + * + * **Available forms**: Forward and inverse, spherical and ellipsoidal + * + * **Defined area**: Global, but more accurate around Czech Republic and Slovakia + * + * **Alias**: krovak + * + * **Domain**: 2D + * + * **Input type**: Geodetic coordinates + * + * **Output type**: Projected coordinates + * + * ## Projection String + * ``` + * +proj=krovak + * ``` + * + * By default, coordinates in the forward direction are output in easting, northing, + * and negative in the Czech Republic and Slovakia, with absolute value of + * easting/westing being smaller than absolute value of northing/southing. + * + * See also `mod_krovak` for a variation of Krovak used with the S-JTSK/05 datum + * in the Czech Republic. + * + * ## Required Parameters + * - None, all parameters are optional for this projection. + * + * ## Optional Parameters + * - `+czech`: Reverses the sign of the output coordinates for use in the Czech Republic and Slovakia (positive values become westing and southing). + * - `+lon_0`: Longitude of projection center. Defaults to `24°50'` (24.8333). + * - `+lat_0`: Latitude of projection center. Defaults to `49.5`. + * - `+k_0`: Scale factor. Defaults to `0.9999`. + * - `+x_0`: False easting. Defaults to `0`. + * - `+y_0`: False northing. Defaults to `0`. + * + * ## Notes + * - The latitude of the pseudo standard parallel is hardcoded to `78.5°`. + * - The ellipsoid used is Bessel by default. + * - Before PROJ 9.4, using custom `x_0` or `y_0` without the `+czech` switch resulted in incorrect values. + * + * ![Krovak](./images/krovak.png) + */ +export class Krovak extends ProjectionBase implements ProjectionTransform { + name = 'Krovak'; + static names = ['krovak']; + // Krovak specific variables + s45: number; + s90: number; + fi0: number; + e2: number; + e: number; + alfa: number; + uq: number; + u0: number; + g: number; + a: number; + es: number; + k1: number; + n0: number; + n: number; + s0: number; + ro0: number; + ad: number; + k: number; + czech?: number; + + /** + * Preps an Krovak projection + * @param params - projection specific parameters + */ + constructor(params?: ProjectionParams) { + super(params); + + this.a = 6377397.155; + this.es = 0.006674372230614; + this.e = sqrt(this.es); + if (!this.lat0) { + this.lat0 = 0.863937979737193; + } + if (!this.long0) { + this.long0 = 0.7417649320975901 - 0.308341501185665; + } + /* if scale not set default to 0.9999 */ + if (!this.k0) { + this.k0 = 0.9999; + } + this.s45 = 0.785398163397448; /* 45 */ + this.s90 = 2 * this.s45; + this.fi0 = this.lat0; + this.e2 = this.es; + this.e = sqrt(this.e2); + this.alfa = sqrt(1 + (this.e2 * pow(cos(this.fi0), 4)) / (1 - this.e2)); + this.uq = 1.04216856380474; + this.u0 = asin(sin(this.fi0) / this.alfa); + this.g = pow( + (1 + this.e * sin(this.fi0)) / (1 - this.e * sin(this.fi0)), + (this.alfa * this.e) / 2, + ); + this.k = (tan(this.u0 / 2 + this.s45) / pow(tan(this.fi0 / 2 + this.s45), this.alfa)) * this.g; + this.k1 = this.k0; + this.n0 = (this.a * sqrt(1 - this.e2)) / (1 - this.e2 * pow(sin(this.fi0), 2)); + this.s0 = 1.37008346281555; + this.n = sin(this.s0); + this.ro0 = (this.k1 * this.n0) / tan(this.s0); + this.ad = this.s90 - this.uq; + } + + /** + * Krovak forward equations--mapping lon-lat to x-y + * @param p - lon-lat WGS84 point + */ + forward(p: VectorPoint): void { + const { x: lon, y: lat } = p; + const delta_lon = adjustLon(lon - this.long0); + /* Transformation */ + const gfi = pow((1 + this.e * sin(lat)) / (1 - this.e * sin(lat)), (this.alfa * this.e) / 2); + const u = 2 * (atan((this.k * pow(tan(lat / 2 + this.s45), this.alfa)) / gfi) - this.s45); + const deltav = -delta_lon * this.alfa; + const s = asin(cos(this.ad) * sin(u) + sin(this.ad) * cos(u) * cos(deltav)); + const d = asin((cos(u) * sin(deltav)) / cos(s)); + const eps = this.n * d; + const ro = + (this.ro0 * pow(tan(this.s0 / 2 + this.s45), this.n)) / pow(tan(s / 2 + this.s45), this.n); + p.y = (ro * cos(eps)) / 1; + p.x = (ro * sin(eps)) / 1; + if (!this.czech) { + p.y *= -1; + p.x *= -1; + } + } + + /** + * Krovak inverse equations--mapping x-y to lon-lat + * @param p - Krovak point + */ + inverse(p: VectorPoint): void { + let ok; + /* Transformation */ + /* revert y, x*/ + const tmp = p.x; + p.x = p.y; + p.y = tmp; + if (!this.czech) { + p.y *= -1; + p.x *= -1; + } + const ro = sqrt(p.x * p.x + p.y * p.y); + const eps = atan2(p.y, p.x); + const d = eps / sin(this.s0); + const s = 2 * (atan(pow(this.ro0 / ro, 1 / this.n) * tan(this.s0 / 2 + this.s45)) - this.s45); + const u = asin(cos(this.ad) * sin(s) - sin(this.ad) * cos(s) * cos(d)); + const deltav = asin((cos(s) * sin(d)) / cos(u)); + p.x = this.long0 - deltav / this.alfa; + let fi1 = u; + ok = 0; + let iter = 0; + do { + p.y = + 2 * + (atan( + pow(this.k, -1 / this.alfa) * + pow(tan(u / 2 + this.s45), 1 / this.alfa) * + pow((1 + this.e * sin(fi1)) / (1 - this.e * sin(fi1)), this.e / 2), + ) - + this.s45); + if (abs(fi1 - p.y) < 0.0000000001) { + ok = 1; + } + fi1 = p.y; + iter += 1; + } while (ok === 0 && iter < 15); + if (iter >= 15) { + throw new Error('Failed to converge'); + } + } +} diff --git a/src/proj4/projections/laea.ts b/src/proj4/projections/laea.ts new file mode 100644 index 00000000..b8f14ff9 --- /dev/null +++ b/src/proj4/projections/laea.ts @@ -0,0 +1,360 @@ +import { ProjectionBase } from '.'; +import { EPSLN, HALF_PI, QUART_PI } from '../constants'; +import { adjustLon, qsfnz } from '../common'; + +import type { VectorPoint } from 's2-tools/geometry'; +import type { ProjectionParams, ProjectionTransform } from '.'; + +export const S_POLE = 1; + +export const N_POLE = 2; +export const EQUIT = 3; +export const OBLIQ = 4; + +// /* determine latitude from authalic latitude */ +const P00 = 0.3333333333333333; +const P01 = 0.17222222222222222; +const P02 = 0.1025793650793651; +const P10 = 0.06388888888888888; +const P11 = 0.0664021164021164; +const P20 = 0.01641501294219154; + +const { abs, sin, cos, sqrt, atan2, asin } = Math; + +/** Lambert Azimuthal Equal Area projection parameters */ +export type APA = [number, number, number]; + +/** + * # Lambert Azimuthal Equal Area + * + * **Classification**: Azimuthal + * + * **Available forms**: Forward and inverse, spherical and ellipsoidal + * + * **Defined area**: Global + * + * **Alias**: laea + * + * **Domain**: 2D + * + * **Input type**: Geodetic coordinates + * + * **Output type**: Projected coordinates + * + * ## Projection String + * ``` + * +proj=laea + * ``` + * + * ## Required Parameters + * - None, all parameters are optional for this projection. + * + * ## Optional Parameters + * - `+lon_0`: Longitude of projection center. Defaults to `0`. + * - `+lat_0`: Latitude of projection center. Defaults to `0`. + * - `+ellps`: Ellipsoid. Defaults to `WGS84`. + * - `+R`: Radius of the sphere. + * - `+x_0`: False easting. Defaults to `0`. + * - `+y_0`: False northing. Defaults to `0`. + * + * Reference + * "New Equal-Area Map Projections for Noncircular Regions", John P. Snyder, + * The American Cartographer, Vol 15, No. 4, October 1988, pp. 341-355. + * + * ![Lambert Azimuthal Equal Area](./images/laea.png) + */ +export class LambertAzimuthalEqualArea extends ProjectionBase implements ProjectionTransform { + name = 'LambertAzimuthalEqualArea'; + static names = [ + 'LambertAzimuthalEqualArea', + 'Lambert Azimuthal Equal Area', + 'Lambert_Azimuthal_Equal_Area', + 'laea', + ]; + // LambertAzimuthalEqualArea specific variables + mode: number; + S_POLE = S_POLE; + N_POLE = N_POLE; + EQUIT = EQUIT; + OBLIQ = OBLIQ; + qp = 0; + mmf = 0; + apa: APA = [0, 0, 0]; + dd = 0; + rq = 0; + xmf = 0; + ymf = 0; + sinb1 = 0; + cosb1 = 0; + sinph0 = 0; + cosph0 = 0; + + /** + * Preps an LambertAzimuthalEqualArea projection + * @param params - projection specific parameters + */ + constructor(params?: ProjectionParams) { + super(params); + + const t = abs(this.lat0); + if (abs(t - HALF_PI) < EPSLN) { + this.mode = this.lat0 < 0 ? this.S_POLE : this.N_POLE; + } else if (abs(t) < EPSLN) { + this.mode = this.EQUIT; + } else { + this.mode = this.OBLIQ; + } + if (this.es > 0) { + let sinphi; + + this.qp = qsfnz(this.e, 1); + this.mmf = 0.5 / (1 - this.es); + this.apa = authset(this.es); + switch (this.mode) { + case this.N_POLE: + this.dd = 1; + break; + case this.S_POLE: + this.dd = 1; + break; + case this.EQUIT: + this.rq = sqrt(0.5 * this.qp); + this.dd = 1 / this.rq; + this.xmf = 1; + this.ymf = 0.5 * this.qp; + break; + case this.OBLIQ: + this.rq = sqrt(0.5 * this.qp); + sinphi = sin(this.lat0); + this.sinb1 = qsfnz(this.e, sinphi) / this.qp; + this.cosb1 = sqrt(1 - this.sinb1 * this.sinb1); + this.dd = cos(this.lat0) / (sqrt(1 - this.es * sinphi * sinphi) * this.rq * this.cosb1); + this.ymf = (this.xmf = this.rq) / this.dd; + this.xmf *= this.dd; + break; + } + } else { + if (this.mode === this.OBLIQ) { + this.sinph0 = sin(this.lat0); + this.cosph0 = cos(this.lat0); + } + } + } + + /** + * LambertAzimuthalEqualArea forward equations--mapping lon-lat to x-y + * @param p - lon-lat WGS84 point + */ + forward(p: VectorPoint): void { + let x, y, coslam, sinlam, sinphi, q, sinb, cosb, b, cosphi; + let lam = p.x; + const phi = p.y; + + lam = adjustLon(lam - this.long0); + if (this.sphere) { + sinphi = sin(phi); + cosphi = cos(phi); + coslam = cos(lam); + if (this.mode === this.OBLIQ || this.mode === this.EQUIT) { + y = + this.mode === this.EQUIT + ? 1 + cosphi * coslam + : 1 + this.sinph0 * sinphi + this.cosph0 * cosphi * coslam; + if (y <= EPSLN) { + throw new Error('Invalid point'); + } + y = sqrt(2 / y); + x = y * cosphi * sin(lam); + y *= + this.mode === this.EQUIT ? sinphi : this.cosph0 * sinphi - this.sinph0 * cosphi * coslam; + } else if (this.mode === this.N_POLE || this.mode === this.S_POLE) { + if (this.mode === this.N_POLE) { + coslam = -coslam; + } + if (abs(phi + this.lat0) < EPSLN) { + throw new Error('Invalid point'); + } + y = QUART_PI - phi * 0.5; + y = 2 * (this.mode === this.S_POLE ? cos(y) : sin(y)); + x = y * sin(lam); + y *= coslam; + } + } else { + sinb = 0; + cosb = 0; + b = 0; + coslam = cos(lam); + sinlam = sin(lam); + sinphi = sin(phi); + q = qsfnz(this.e, sinphi); + if (this.mode === this.OBLIQ || this.mode === this.EQUIT) { + sinb = q / this.qp; + cosb = sqrt(1 - sinb * sinb); + } + switch (this.mode) { + case this.OBLIQ: + b = 1 + this.sinb1 * sinb + this.cosb1 * cosb * coslam; + break; + case this.EQUIT: + b = 1 + cosb * coslam; + break; + case this.N_POLE: + b = HALF_PI + phi; + q = this.qp - q; + break; + case this.S_POLE: + b = phi - HALF_PI; + q = this.qp + q; + break; + } + if (abs(b) < EPSLN) { + throw new Error('Invalid point'); + } + switch (this.mode) { + case this.OBLIQ: + case this.EQUIT: + b = sqrt(2 / b); + if (this.mode === this.OBLIQ) { + y = this.ymf * b * (this.cosb1 * sinb - this.sinb1 * cosb * coslam); + } else { + y = (b = sqrt(2 / (1 + cosb * coslam))) * sinb * this.ymf; + } + x = this.xmf * b * cosb * sinlam; + break; + case this.N_POLE: + case this.S_POLE: + if (q >= 0) { + x = (b = sqrt(q)) * sinlam; + y = coslam * (this.mode === this.S_POLE ? b : -b); + } else { + x = y = 0; + } + break; + } + } + + if (x === undefined || y === undefined) { + throw new Error('Invalid point'); + } + p.x = this.a * x + this.x0; + p.y = this.a * y + this.y0; + } + + /** + * LambertAzimuthalEqualArea inverse equations--mapping x-y to lon-lat + * @param p - LambertAzimuthalEqualArea point + */ + inverse(p: VectorPoint): void { + p.x -= this.x0; + p.y -= this.y0; + let x = p.x / this.a; + let y = p.y / this.a; + let lam, phi, cCe, sCe, q, rho, ab; + if (this.sphere) { + let cosz = 0, + sinz = 0; + const rh = sqrt(x * x + y * y); + phi = rh * 0.5; + if (phi > 1) { + throw new Error('Point is outside the sphere'); + } + phi = 2 * asin(phi); + if (this.mode === this.OBLIQ || this.mode === this.EQUIT) { + sinz = sin(phi); + cosz = cos(phi); + } + switch (this.mode) { + case this.EQUIT: + phi = abs(rh) <= EPSLN ? 0 : asin((y * sinz) / rh); + x *= sinz; + y = cosz * rh; + break; + case this.OBLIQ: + phi = + abs(rh) <= EPSLN ? this.lat0 : asin(cosz * this.sinph0 + (y * sinz * this.cosph0) / rh); + x *= sinz * this.cosph0; + y = (cosz - sin(phi) * this.sinph0) * rh; + break; + case this.N_POLE: + y = -y; + phi = HALF_PI - phi; + break; + case this.S_POLE: + phi -= HALF_PI; + break; + } + lam = y === 0 && (this.mode === this.EQUIT || this.mode === this.OBLIQ) ? 0 : atan2(x, y); + } else { + ab = 0; + if (this.mode === this.OBLIQ || this.mode === this.EQUIT) { + x /= this.dd; + y *= this.dd; + rho = sqrt(x * x + y * y); + if (rho < EPSLN) { + p.x = this.long0; + p.y = this.lat0; + return; + } + sCe = 2 * asin((0.5 * rho) / this.rq); + cCe = cos(sCe); + x *= sCe = sin(sCe); + if (this.mode === this.OBLIQ) { + ab = cCe * this.sinb1 + (y * sCe * this.cosb1) / rho; + q = this.qp * ab; + y = rho * this.cosb1 * cCe - y * this.sinb1 * sCe; + } else { + ab = (y * sCe) / rho; + q = this.qp * ab; + y = rho * cCe; + } + } else if (this.mode === this.N_POLE || this.mode === this.S_POLE) { + if (this.mode === this.N_POLE) { + y = -y; + } + q = x * x + y * y; + if (!q) { + p.x = this.long0; + p.y = this.lat0; + return; + } + ab = 1 - q / this.qp; + if (this.mode === this.S_POLE) { + ab = -ab; + } + } + lam = atan2(x, y); + phi = authlat(asin(ab), this.apa); + } + p.x = adjustLon(this.long0 + lam); + p.y = phi; + } +} + +/** + * @param es - eccentricity + * @returns [APA0, APA1, APA2] + */ +function authset(es: number): APA { + let t; + const APA: APA = [0, 0, 0]; + APA[0] = es * P00; + t = es * es; + APA[0] += t * P01; + APA[1] = t * P10; + t *= es; + APA[0] += t * P02; + APA[1] += t * P11; + APA[2] = t * P20; + + return APA; +} + +/** + * @param beta - geodetic latitude + * @param APA - [APA0, APA1, APA2] + * @returns authalic latitude + */ +function authlat(beta: number, APA: APA): number { + const t = beta + beta; + return beta + APA[0] * sin(t) + APA[1] * sin(t + t) + APA[2] * sin(t + t + t); +} diff --git a/src/proj4/projections/lcc.ts b/src/proj4/projections/lcc.ts new file mode 100644 index 00000000..1bc21d8b --- /dev/null +++ b/src/proj4/projections/lcc.ts @@ -0,0 +1,206 @@ +import { ProjectionBase } from '.'; +import { EPSLN, HALF_PI } from '../constants'; +import { adjustLon, msfnz, phi2z, sign, tsfnz } from '../common'; + +import type { VectorPoint } from 's2-tools/geometry'; +import type { ProjectionParams, ProjectionTransform } from '.'; + +const { abs, pow, sin, cos, sqrt, atan2, log, PI } = Math; + +/** + * # Lambert Conformal Conic + * + * Lambert Conformal Conic projection (LCC) is a conic map projection + * used for aeronautical charts, portions of the State Plane Coordinate + * System, and many national and regional mapping systems. It is one of + * seven projections introduced by Johann Heinrich Lambert in 1772. + * + * It has several different forms: with one and two standard parallels + * (referred to as 1SP and 2SP in EPSG guidance notes). Additionally we + * provide "2SP Michigan" form which is very similar to normal 2SP, but + * with a scaling factor on the ellipsoid (given as `k_0` parameter). + * It is implemented as per EPSG Guidance Note 7-2 (version 54, August + * 2018, page 25). It is used in a few systems in the EPSG database which + * justifies adding this otherwise non-standard projection. + * + * **Classification**: Conformal conic + * + * **Available forms**: Forward and inverse, spherical and ellipsoidal + * - One or two standard parallels (1SP and 2SP). + * - "LCC 2SP Michigan" form can be used by setting the `+k_0` parameter to specify ellipsoid scale. + * + * **Defined area**: Best for regions predominantly east-west in extent and located in the middle north or south latitudes. + * + * **Alias**: lcc + * + * **Domain**: 2D + * + * **Input type**: Geodetic coordinates + * + * **Output type**: Projected coordinates + * + * ## Projection String + * ``` + * +proj=lcc +lon_0=-90 +lat_1=33 +lat_2=45 + * ``` + * + * ## Required Parameters + * - `+lat_1`: Latitude of the first standard parallel. + * + * ## Optional Parameters + * - `+lon_0`: Longitude of projection center. Defaults to `0`. + * - `+lat_0`: Latitude of projection center. Defaults to `0`. + * - `+lat_2`: Latitude of the second standard parallel. + * - `+ellps`: Ellipsoid. Defaults to `WGS84`. + * - `+R`: Radius of the sphere. + * - `+x_0`: False easting. Defaults to `0`. + * - `+y_0`: False northing. Defaults to `0`. + * - `+k_0`: Scale factor at natural origin (for LCC 1SP) or ellipsoid scale factor (for LCC 2SP Michigan). Defaults to `1.0`. + * + * ![Lambert Conformal Conic](./images/lcc.png) + * + * ## Further reading + * - [Wikipedia on Lambert Conformal Conic](https://en.wikipedia.org/wiki/Lambert_conformal_conic_projection) + * - [Wolfram Mathworld on Lambert Conformal Conic](http://mathworld.wolfram.com/LambertConformalConicProjection.html) + * - [John P. Snyder "Map projections: A working manual"](https://pubs.er.usgs.gov/publication/pp1395) + * - [ArcGIS documentation on "Lambert Conformal Conic"](http://desktop.arcgis.com/en/arcmap/10.3/guide-books/map-projections/lambert-conformal-conic.htm) + * - [EPSG Guidance Note 7-2](http://www.epsg.org/Guidancenotes.aspx) + */ +export class LambertConformalConic extends ProjectionBase implements ProjectionTransform { + name = 'LambertConformalConic'; + static names = [ + 'LambertConformalConic', + 'Lambert Tangential Conformal Conic Projection', + 'Lambert_Conformal_Conic', + 'Lambert_Conformal_Conic_1SP', + 'Lambert_Conformal_Conic_2SP', + 'lcc', + 'Lambert Conic Conformal (1SP)', + 'Lambert Conic Conformal (2SP)', + ]; + // LambertConformalConic specific variables + ns = 0; + f0 = 0; + rh = 0; + + /** + * Preps an LambertConformalConic projection + * @param params - projection specific parameters + */ + constructor(params?: ProjectionParams) { + super(params); + + // double lat0; /* the reference latitude */ + // double long0; /* the reference longitude */ + // double lat1; /* first standard parallel */ + // double lat2; /* second standard parallel */ + // double r_maj; /* major axis */ + // double r_min; /* minor axis */ + // double false_east; /* x offset in meters */ + // double false_north; /* y offset in meters */ + //the above value can be set with proj4.defs + //example: proj4.defs("EPSG:2154","+proj=lcc +lat_1=49 +lat_2=44 +lat_0=46.5 +lon_0=3 +x_0=700000 +y_0=6600000 +ellps=GRS80 +towgs84=0,0,0,0,0,0,0 +units=m +no_defs"); + + if (!this.lat2) { + this.lat2 = this.lat1; + } //if lat2 is not defined + if (!this.k0) { + this.k0 = 1; + } + this.x0 = this.x0 || 0; + this.y0 = this.y0 || 0; + // Standard Parallels cannot be equal and on opposite sides of the equator + if (abs(this.lat1 + this.lat2) < EPSLN) { + return; + } + + const temp = this.b / this.a; + this.e = sqrt(1 - temp * temp); + + const sin1 = sin(this.lat1); + const cos1 = cos(this.lat1); + const ms1 = msfnz(this.e, sin1, cos1); + const ts1 = tsfnz(this.e, this.lat1, sin1); + + const sin2 = sin(this.lat2); + const cos2 = cos(this.lat2); + const ms2 = msfnz(this.e, sin2, cos2); + const ts2 = tsfnz(this.e, this.lat2, sin2); + + const ts0 = tsfnz(this.e, this.lat0, sin(this.lat0)); + + if (abs(this.lat1 - this.lat2) > EPSLN) { + this.ns = log(ms1 / ms2) / log(ts1 / ts2); + } else { + this.ns = sin1; + } + if (isNaN(this.ns)) { + this.ns = sin1; + } + this.f0 = ms1 / (this.ns * pow(ts1, this.ns)); + this.rh = this.a * this.f0 * pow(ts0, this.ns); + } + + /** + * LambertConformalConic forward equations--mapping lon-lat to x-y + * @param p - lon-lat WGS84 point + */ + forward(p: VectorPoint): void { + const lon = p.x; + let lat = p.y; + // singular cases : + if (abs(2 * abs(lat) - PI) <= EPSLN) { + lat = sign(lat) * (HALF_PI - 2 * EPSLN); + } + let con = abs(abs(lat) - HALF_PI); + let ts, rh1; + if (con > EPSLN) { + ts = tsfnz(this.e, lat, sin(lat)); + rh1 = this.a * this.f0 * pow(ts, this.ns); + } else { + con = lat * this.ns; + if (con <= 0) { + throw new Error('latitude out of range'); + } + rh1 = 0; + } + const theta = this.ns * adjustLon(lon - this.long0); + p.x = this.k0 * (rh1 * sin(theta)) + this.x0; + p.y = this.k0 * (this.rh - rh1 * cos(theta)) + this.y0; + } + + /** + * LambertConformalConic inverse equations--mapping x-y to lon-lat + * @param p - LambertConformalConic point + */ + inverse(p: VectorPoint): void { + let rh1, con, ts; + let lat; + const x = (p.x - this.x0) / this.k0; + const y = this.rh - (p.y - this.y0) / this.k0; + if (this.ns > 0) { + rh1 = sqrt(x * x + y * y); + con = 1; + } else { + rh1 = -sqrt(x * x + y * y); + con = -1; + } + let theta = 0; + if (rh1 !== 0) { + theta = atan2(con * x, con * y); + } + if (rh1 !== 0 || this.ns > 0) { + con = 1 / this.ns; + ts = pow(rh1 / (this.a * this.f0), con); + lat = phi2z(this.e, ts); + if (lat === -9999) { + throw new Error('Invalid latitude'); + } + } else { + lat = -HALF_PI; + } + const lon = adjustLon(theta / this.ns + this.long0); + p.x = lon; + p.y = lat; + } +} diff --git a/src/proj4/projections/merc.ts b/src/proj4/projections/merc.ts index 6f668502..4fb52c43 100644 --- a/src/proj4/projections/merc.ts +++ b/src/proj4/projections/merc.ts @@ -114,14 +114,13 @@ const { abs, sin, cos, sqrt, log, tan, atan, exp } = Math; export class Mercator extends ProjectionBase implements ProjectionTransform { name = 'Mercator'; static names = [ - this.name, + 'Mercator', 'Popular Visualisation Pseudo Mercator', 'Mercator_1SP', 'Mercator_Auxiliary_Sphere', 'merc', ]; // Mercator specific variables - declare k0: number; /** * Preps an Mercator projection @@ -152,9 +151,8 @@ export class Mercator extends ProjectionBase implements ProjectionTransform { /** * Mercator forward equations--mapping lon-lat to x-y * @param p - lon-lat WGS84 point - * @returns - Mercator point */ - forward(p: VectorPoint): VectorPoint { + forward(p: VectorPoint): void { const { x: lon, y: lat } = p; // convert to radians // if (lat * R2D > 90 && lat * R2D < -90 && lon * R2D > 180 && lon * R2D < -180) { @@ -176,16 +174,14 @@ export class Mercator extends ProjectionBase implements ProjectionTransform { } p.x = x; p.y = y; - return p; } } /** * Mercator inverse equations--mapping x-y to lon-lat * @param p - Mercator point - * @returns - lon-lat WGS84 point */ - inverse(p: VectorPoint): VectorPoint { + inverse(p: VectorPoint): void { const x = p.x - this.x0; const y = p.y - this.y0; let lat: number; @@ -201,7 +197,5 @@ export class Mercator extends ProjectionBase implements ProjectionTransform { p.x = lon; p.y = lat; - - return p; } } diff --git a/src/proj4/projections/mill.ts b/src/proj4/projections/mill.ts new file mode 100644 index 00000000..bf2f0f9c --- /dev/null +++ b/src/proj4/projections/mill.ts @@ -0,0 +1,106 @@ +import { ProjectionBase } from '.'; +import { adjustLon } from '../common'; + +import type { VectorPoint } from 's2-tools/geometry'; +import type { ProjectionParams, ProjectionTransform } from '.'; + +const { tan, atan, PI, log, exp } = Math; + +/** + * # Miller Cylindrical + * + * The Miller cylindrical projection is a modified Mercator projection, proposed by + * Osborn Maitland Miller in 1942. + * + * **Classification**: Neither conformal nor equal area cylindrical + * + * **Available forms**: Forward and inverse spherical + * + * **Defined area**: Global, but best used near the equator + * + * **Alias**: mill + * + * **Domain**: 2D + * + * **Input type**: Geodetic coordinates + * + * **Output type**: Projected coordinates + * + * ## Projection String + * ``` + * +proj=mill + * ``` + * + * ## Required Parameters + * - None + * + * ## Optional Parameters + * - `+lon_0`: Longitude of projection center. Defaults to `0`. + * - `+R`: Radius of the sphere. + * - `+x_0`: False easting. Defaults to `0`. + * - `+y_0`: False northing. Defaults to `0`. + * + * ## Usage Example + * Using Central meridian 90°W: + * ```bash + * $ echo -100 35 | proj +proj=mill +lon_0=90w + * -1113194.91 4061217.24 + * ``` + * + * ## Mathematical Definition + * ### Forward projection: + * ```math + * x = \lambda + * y = 1.25 * \ln \left[ \tan \left(\frac{\pi}{4} + 0.4 * \phi \right) \right] + * ``` + * ### Inverse projection: + * ```math + * \lambda = x + * \phi = 2.5 * ( \arctan \left[ e^{0.8 * y} \right] - \frac{\pi}{4} ) + * ``` + * + * ## Further reading + * - [Wikipedia on Miller Cylindrical](https://en.wikipedia.org/wiki/Miller_cylindrical_projection) + * - "New Equal-Area Map Projections for Noncircular Regions", John P. Snyder, The American Cartographer, Vol 15, No. 4, October 1988, pp. 341-355. + * + * ![Miller Cylindrical](./images/mill.png) + */ +export class MillerCylindrical extends ProjectionBase implements ProjectionTransform { + name = 'MillerCylindrical'; + static names = ['MillerCylindrical', 'Miller_Cylindrical', 'mill']; + // MillerCylindrical specific variables + + /** + * Preps an MillerCylindrical projection + * @param params - projection specific parameters + */ + constructor(params?: ProjectionParams) { + super(params); + } + + /** + * MillerCylindrical forward equations--mapping lon-lat to x-y + * @param p - lon-lat WGS84 point + */ + forward(p: VectorPoint): void { + const { x: lon, y: lat } = p; + const dlon = adjustLon(lon - this.long0); + const x = this.x0 + this.a * dlon; + const y = this.y0 + this.a * log(tan(PI / 4 + lat / 2.5)) * 1.25; + p.x = x; + p.y = y; + } + + /** + * MillerCylindrical inverse equations--mapping x-y to lon-lat + * @param p - MillerCylindrical point + */ + inverse(p: VectorPoint): void { + p.x -= this.x0; + p.y -= this.y0; + const lon = adjustLon(this.long0 + p.x / this.a); + const lat = 2.5 * (atan(exp((0.8 * p.y) / this.a)) - PI / 4); + p.x = lon; + p.y = lat; + } +} diff --git a/src/proj4/projections/moll.ts b/src/proj4/projections/moll.ts new file mode 100644 index 00000000..b20073b0 --- /dev/null +++ b/src/proj4/projections/moll.ts @@ -0,0 +1,122 @@ +import { EPSLN } from '../constants'; +import { ProjectionBase } from '.'; +import { adjustLon } from '../common'; + +import type { VectorPoint } from 's2-tools/geometry'; +import type { ProjectionParams, ProjectionTransform } from '.'; + +const { abs, PI, sin, cos, asin } = Math; + +/** + * # Mollweide + * + * **Classification**: Pseudocylindrical + * + * **Available forms**: Forward and inverse, spherical projection + * + * **Defined area**: Global + * + * **Alias**: moll + * + * **Domain**: 2D + * + * **Input type**: Geodetic coordinates + * + * **Output type**: Projected coordinates + * + * ## Projection String + * ``` + * +proj=moll + * ``` + * + * ## Required Parameters + * - None + * + * ## Optional Parameters + * - `+lon_0`: Longitude of projection center. Defaults to `0`. + * - `+R`: Radius of the sphere. + * - `+x_0`: False easting. Defaults to `0`. + * - `+y_0`: False northing. Defaults to `0`. + * + * ## Further reading + * - [Wikipedia on Mollweide Projection](https://en.wikipedia.org/wiki/Mollweide_projection) + * + * ![Mollweide](./images/moll.png) + */ +export class Mollweide extends ProjectionBase implements ProjectionTransform { + name = 'Mollweide'; + static names = ['Mollweide', 'moll']; + // Mollweide specific variables + + /** + * Preps an Mollweide projection + * @param params - projection specific parameters + */ + constructor(params?: ProjectionParams) { + super(params); + } + + /** + * Mollweide forward equations--mapping lon-lat to x-y + * @param p - lon-lat WGS84 point + */ + forward(p: VectorPoint): void { + const lon = p.x; + const lat = p.y; + let delta_lon = adjustLon(lon - this.long0); + let theta = lat; + const con = PI * sin(lat); + /* Iterate using the Newton-Raphson method to find theta */ + while (true) { + const delta_theta = -(theta + sin(theta) - con) / (1 + cos(theta)); + theta += delta_theta; + if (abs(delta_theta) < EPSLN) { + break; + } + } + theta /= 2; + // If the latitude is 90 deg, force the x coordinate to be "0 + false easting" + // this is done here because of precision problems with "cos(theta)" + if (PI / 2 - abs(lat) < EPSLN) { + delta_lon = 0; + } + const x = 0.900316316158 * this.a * delta_lon * cos(theta) + this.x0; + const y = 1.4142135623731 * this.a * sin(theta) + this.y0; + p.x = x; + p.y = y; + } + + /** + * Mollweide inverse equations--mapping x-y to lon-lat + * @param p - Mollweide point + */ + inverse(p: VectorPoint): void { + let arg; + /* Inverse equations + -----------------*/ + p.x -= this.x0; + p.y -= this.y0; + arg = p.y / (1.4142135623731 * this.a); + /* Because of division by zero problems, 'arg' can not be 1. Therefore + a number very close to one is used instead. + -------------------------------------------------------------------*/ + if (abs(arg) > 0.999999999999) { + arg = 0.999999999999; + } + const theta = asin(arg); + let lon = adjustLon(this.long0 + p.x / (0.900316316158 * this.a * cos(theta))); + if (lon < -PI) { + lon = -PI; + } + if (lon > PI) { + lon = PI; + } + arg = (2 * theta + sin(2 * theta)) / PI; + if (abs(arg) > 1) { + arg = 1; + } + const lat = asin(arg); + p.x = lon; + p.y = lat; + } +} diff --git a/src/proj4/projections/nzmg.ts b/src/proj4/projections/nzmg.ts new file mode 100644 index 00000000..b12f9aa0 --- /dev/null +++ b/src/proj4/projections/nzmg.ts @@ -0,0 +1,234 @@ +import { ProjectionBase } from '.'; +import { SEC_TO_RAD } from '../constants'; + +import type { VectorPoint } from 's2-tools/geometry'; +import type { ProjectionParams, ProjectionTransform } from '.'; + +/** + * # New Zealand Map Grid (EPSG:27200) + * + * **Classification**: Custom grid-based projection + * + * **Available forms**: Forward and inverse, spherical and ellipsoidal + * + * **Defined area**: New Zealand + * + * **Alias**: nzmg + * + * **Domain**: 2D + * + * **Input type**: Geodetic coordinates + * + * **Output type**: Projected coordinates + * + * ## Projection String + * ``` + * +proj=nzmg + * ``` + * + * ## Required Parameters + * - None (all standard projection parameters are hard-coded for this projection) + * + * ## Reference: + * - Department of Land and Survey Technical Circular 1973/32 + * http://www.linz.govt.nz/docs/miscellaneous/nz-map-definition.pdf + * - OSG Technical Report 4.1 + * http://www.linz.govt.nz/docs/miscellaneous/nzmg.pdf + * + * ![New Zealand Map Grid (EPSG:27200)](./images/nzmg.png) + */ +export class NewZealandMapGrid extends ProjectionBase implements ProjectionTransform { + name = 'NewZealandMapGrid'; + static names = ['NewZealandMapGrid', 'New_Zealand_Map_Grid', 'nzmg']; + // NewZealandMapGrid specific variables + + // * iterations: Number of iterations to refine inverse transform. + // * 0 -> km accuracy + // * 1 -> m accuracy -- suitable for most mapping applications + // * 2 -> mm accuracy + iterations = 1; + + A = [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]; + B_re = [0, 0, 0, 0, 0, 0]; + B_im = [0, 0, 0, 0, 0, 0]; + C_re = [0, 0, 0, 0, 0, 0]; + C_im = [0, 0, 0, 0, 0, 0]; + D = [0, 0, 0, 0, 0, 0, 0, 0, 0, 0]; + + /** + * Preps an NewZealandMapGrid projection + * @param params - projection specific parameters + */ + constructor(params?: ProjectionParams) { + super(params); + + this.A[1] = 0.6399175073; + this.A[2] = -0.1358797613; + this.A[3] = 0.063294409; + this.A[4] = -0.02526853; + this.A[5] = 0.0117879; + this.A[6] = -0.0055161; + this.A[7] = 0.0026906; + this.A[8] = -0.001333; + this.A[9] = 0.00067; + this.A[10] = -0.00034; + + this.B_re[1] = 0.7557853228; + this.B_im[1] = 0; + this.B_re[2] = 0.249204646; + this.B_im[2] = 0.003371507; + this.B_re[3] = -0.001541739; + this.B_im[3] = 0.04105856; + this.B_re[4] = -0.10162907; + this.B_im[4] = 0.01727609; + this.B_re[5] = -0.26623489; + this.B_im[5] = -0.36249218; + this.B_re[6] = -0.6870983; + this.B_im[6] = -1.1651967; + + this.C_re[1] = 1.3231270439; + this.C_im[1] = 0; + this.C_re[2] = -0.577245789; + this.C_im[2] = -0.007809598; + this.C_re[3] = 0.508307513; + this.C_im[3] = -0.112208952; + this.C_re[4] = -0.15094762; + this.C_im[4] = 0.18200602; + this.C_re[5] = 1.01418179; + this.C_im[5] = 1.64497696; + this.C_re[6] = 1.9660549; + this.C_im[6] = 2.5127645; + + this.D[1] = 1.5627014243; + this.D[2] = 0.5185406398; + this.D[3] = -0.03333098; + this.D[4] = -0.1052906; + this.D[5] = -0.0368594; + this.D[6] = 0.007317; + this.D[7] = 0.0122; + this.D[8] = 0.00394; + this.D[9] = -0.0013; + } + + /** + * NewZealandMapGrid forward equations--mapping lon-lat to x-y + * @param p - lon-lat WGS84 point + */ + forward(p: VectorPoint): void { + const { x: lon, y: lat } = p; + let n; + const deltaLat = lat - this.lat0; + const deltaLon = lon - this.long0; + // 1. Calculate d_phi and d_psi ... // and d_lambda + // For this algorithm, deltaLatitude is in seconds of arc x 10-5, so we need to scale to those units. Longitude is radians. + const d_phi = (deltaLat / SEC_TO_RAD) * 1e-5; + const d_lambda = deltaLon; + let d_phi_n = 1; // d_phi^0 + let d_psi = 0; + for (n = 1; n <= 10; n++) { + d_phi_n = d_phi_n * d_phi; + d_psi = d_psi + this.A[n] * d_phi_n; + } + // 2. Calculate theta + const th_re = d_psi; + const th_im = d_lambda; + // 3. Calculate z + let th_n_re = 1; + let th_n_im = 0; // theta^0 + let th_n_re1; + let th_n_im1; + let z_re = 0; + let z_im = 0; + for (n = 1; n <= 6; n++) { + th_n_re1 = th_n_re * th_re - th_n_im * th_im; + th_n_im1 = th_n_im * th_re + th_n_re * th_im; + th_n_re = th_n_re1; + th_n_im = th_n_im1; + z_re = z_re + this.B_re[n] * th_n_re - this.B_im[n] * th_n_im; + z_im = z_im + this.B_im[n] * th_n_re + this.B_re[n] * th_n_im; + } + // 4. Calculate easting and northing + p.x = z_im * this.a + this.x0; + p.y = z_re * this.a + this.y0; + } + + /** + * NewZealandMapGrid inverse equations--mapping x-y to lon-lat + * @param p - NewZealandMapGrid point + */ + inverse(p: VectorPoint): void { + const { x, y } = p; + let n; + const delta_x = x - this.x0; + const delta_y = y - this.y0; + // 1. Calculate z + const z_re = delta_y / this.a; + const z_im = delta_x / this.a; + // 2a. Calculate theta - first approximation gives km accuracy + let z_n_re = 1; + let z_n_im = 0; // z^0 + let z_n_re1; + let z_n_im1; + let th_re = 0; + let th_im = 0; + for (n = 1; n <= 6; n++) { + z_n_re1 = z_n_re * z_re - z_n_im * z_im; + z_n_im1 = z_n_im * z_re + z_n_re * z_im; + z_n_re = z_n_re1; + z_n_im = z_n_im1; + th_re = th_re + this.C_re[n] * z_n_re - this.C_im[n] * z_n_im; + th_im = th_im + this.C_im[n] * z_n_re + this.C_re[n] * z_n_im; + } + // 2b. Iterate to refine the accuracy of the calculation + // 0 iterations gives km accuracy + // 1 iteration gives m accuracy -- good enough for most mapping applications + // 2 iterations bives mm accuracy + for (let i = 0; i < this.iterations; i++) { + let th_n_re = th_re; + let th_n_im = th_im; + let th_n_re1; + let th_n_im1; + let num_re = z_re; + let num_im = z_im; + for (n = 2; n <= 6; n++) { + th_n_re1 = th_n_re * th_re - th_n_im * th_im; + th_n_im1 = th_n_im * th_re + th_n_re * th_im; + th_n_re = th_n_re1; + th_n_im = th_n_im1; + num_re = num_re + (n - 1) * (this.B_re[n] * th_n_re - this.B_im[n] * th_n_im); + num_im = num_im + (n - 1) * (this.B_im[n] * th_n_re + this.B_re[n] * th_n_im); + } + th_n_re = 1; + th_n_im = 0; + let den_re = this.B_re[1]; + let den_im = this.B_im[1]; + for (n = 2; n <= 6; n++) { + th_n_re1 = th_n_re * th_re - th_n_im * th_im; + th_n_im1 = th_n_im * th_re + th_n_re * th_im; + th_n_re = th_n_re1; + th_n_im = th_n_im1; + den_re = den_re + n * (this.B_re[n] * th_n_re - this.B_im[n] * th_n_im); + den_im = den_im + n * (this.B_im[n] * th_n_re + this.B_re[n] * th_n_im); + } + // Complex division + const den2 = den_re * den_re + den_im * den_im; + th_re = (num_re * den_re + num_im * den_im) / den2; + th_im = (num_im * den_re - num_re * den_im) / den2; + } + // 3. Calculate d_phi ... // and d_lambda + const d_psi = th_re; + const d_lambda = th_im; + let d_psi_n = 1; // d_psi^0 + let d_phi = 0; + for (n = 1; n <= 9; n++) { + d_psi_n = d_psi_n * d_psi; + d_phi = d_phi + this.D[n] * d_psi_n; + } + // 4. Calculate latitude and longitude + // d_phi is calcuated in second of arc * 10^-5, so we need to scale back to radians. d_lambda is in radians. + const lat = this.lat0 + d_phi * SEC_TO_RAD * 1e5; + const lon = this.long0 + d_lambda; + p.x = lon; + p.y = lat; + } +} diff --git a/src/proj4/projections/omerc.ts b/src/proj4/projections/omerc.ts new file mode 100644 index 00000000..82c7511c --- /dev/null +++ b/src/proj4/projections/omerc.ts @@ -0,0 +1,337 @@ +import { ProjectionBase } from '.'; +import { D2R, EPSLN, HALF_PI, QUART_PI, TWO_PI } from '../constants'; +import { adjustLon, phi2z, tsfnz } from '../common'; + +import type { VectorPoint } from 's2-tools/geometry'; +import type { ProjectionParams, ProjectionTransform } from '.'; + +const { abs, pow, sin, cos, sqrt, atan2, asin, log, atan, PI, tan, exp } = Math; + +/** + * # Oblique Mercator + * + * **Classification**: Conformal cylindrical + * + * **Available forms**: Forward and inverse, spherical and ellipsoidal + * + * **Defined area**: Global, but reasonably accurate only within 15 degrees of the oblique central line + * + * **Alias**: omerc + * + * **Domain**: 2D + * + * **Input type**: Geodetic coordinates + * + * **Output type**: Projected coordinates + * + * ## Projection String + * ``` + * +proj=omerc +lat_1=45 +lat_2=55 + * ``` + * + * ## Required Parameters + * - `+lat_1=`: Latitude of the first point on the central line. + * - `+lat_2=`: Latitude of the second point on the central line. + * + * ## Optional Parameters + * - `+alpha=`: Azimuth of the centerline clockwise from north at the center point of the line. + * - `+gamma=`: Azimuth of the centerline clockwise from north of the rectified bearing of the centerline. + * - `+lonc=`: Longitude of the projection center (overrides `+lon_0`). + * - `+lat_0=`: Latitude of the projection center. + * - `+no_rot`: Disables rectification (historical reason). + * - `+no_off`: Disables origin offset to the center of projection. + * - `+k_0=`: Scale factor at the central line. + * - `+x_0=`: False easting. + * - `+y_0=`: False northing. + * + * ## Usage Example + * ``` + * echo 12 55 | proj +proj=omerc +alpha=90 +ellps=GRS80 + * echo 12 55 | proj +proj=omerc +alpha=0 +R=6400000 + * echo 12 55 | proj +proj=omerc +lon_1=0 +lat_1=-1 +lon_2=0 +lat_2=0 +R=6400000 + * echo 12 55 | proj +proj=tmerc +R=6400000 + * echo 12 55 | proj +proj=omerc +lon_1=-1 +lat_1=1 +lon_2=0 +lat_2=0 +ellps=GRS80 + * echo 10.536498003 56.229892362 | cs2cs +proj=longlat +ellps=GRS80 +to +proj=omerc +axis=wnu +lonc=9.46 +lat_0=56.13333333 +x_0=-266906.229 +y_0=189617.957 +k=0.9999537 +alpha=-0.76324 +gamma=0 +ellps=GRS80 + * ``` + * + * ## Caveats + * The two-point method with no rectification is probably only marginally useful. + * + * ![Oblique Mercator](./images/omerc.png) + */ +export class HotineObliqueMercator extends ProjectionBase implements ProjectionTransform { + name = 'HotineObliqueMercator'; + static names = [ + 'HotineObliqueMercator', + 'Hotine_Oblique_Mercator', + 'Hotine Oblique Mercator', + 'Hotine_Oblique_Mercator_Azimuth_Natural_Origin', + 'Hotine_Oblique_Mercator_Two_Point_Natural_Origin', + 'Hotine_Oblique_Mercator_Azimuth_Center', + 'Oblique_Mercator', + 'omerc', + ]; + // HotineObliqueMercator specific variables + noOff: boolean; + noRot: boolean; + TOL = 1e-7; + lam0: number; + declare long2: number; + declare longc: number; + A = 0; + B = 0; + E = 0; + singam: number; + cosgam: number; + sinrot: number; + cosrot: number; + rB: number; + ArB: number; + BrA: number; + u0: number; + vPoleN: number; + vPoleS: number; + + /** + * Preps an HotineObliqueMercator projection + * @param params - projection specific parameters + */ + constructor(params?: ProjectionParams) { + super(params); + const { TOL } = this; + + let con, + cosph0, + D, + F, + H, + L, + sinph0, + p, + J, + gamma = 0, + gamma0, + lamc = 0, + lam1 = 0, + lam2 = 0, + phi1 = 0, + phi2 = 0, + alphaC = 0; + + // only Type A uses the noOff or noUoff property + // https://github.com/OSGeo/proj.4/issues/104 + this.noOff = isTypeA(params ?? {}); + this.noRot = params?.noRot ?? false; + + let alp = false; + if ('alpha' in this) { + alp = true; + } + + let gam = false; + if (this.rectifiedGridAngle !== undefined) { + gam = true; + gamma = this.rectifiedGridAngle * D2R; + } + + if (alp) { + alphaC = this.alpha ?? 0; + } + + if (alp || gam) { + lamc = this.longc; + } else { + lam1 = this.long1; + phi1 = this.lat1; + lam2 = this.long2; + phi2 = this.lat2; + + if ( + abs(phi1 - phi2) <= TOL || + (con = abs(phi1)) <= TOL || + abs(con - HALF_PI) <= TOL || + abs(abs(this.lat0) - HALF_PI) <= TOL || + abs(abs(phi2) - HALF_PI) <= TOL + ) { + throw new Error(); + } + } + + const one_es = 1.0 - this.es; + const com = sqrt(one_es); + + if (abs(this.lat0) > EPSLN) { + sinph0 = sin(this.lat0); + cosph0 = cos(this.lat0); + con = 1 - this.es * sinph0 * sinph0; + this.B = cosph0 * cosph0; + this.B = sqrt(1 + (this.es * this.B * this.B) / one_es); + this.A = (this.B * this.k0 * com) / con; + D = (this.B * com) / (cosph0 * sqrt(con)); + F = D * D - 1; + + if (F <= 0) { + F = 0; + } else { + F = sqrt(F); + if (this.lat0 < 0) { + F = -F; + } + } + + this.E = F += D; + this.E *= pow(tsfnz(this.e, this.lat0, sinph0), this.B); + } else { + this.B = 1 / com; + this.A = this.k0; + this.E = D = F = 1; + } + + if (alp || gam) { + if (alp) { + gamma0 = asin(sin(alphaC) / D); + if (!gam) { + gamma = alphaC; + } + } else { + gamma0 = gamma; + alphaC = asin(D * sin(gamma0)); + } + this.lam0 = lamc - asin(0.5 * (F - 1 / F) * tan(gamma0)) / this.B; + } else { + H = pow(tsfnz(this.e, phi1, sin(phi1)), this.B); + L = pow(tsfnz(this.e, phi2, sin(phi2)), this.B); + F = this.E / H; + p = (L - H) / (L + H); + J = this.E * this.E; + J = (J - L * H) / (J + L * H); + con = lam1 - lam2; + + if (con < -PI) { + lam2 -= TWO_PI; + } else if (con > PI) { + lam2 += TWO_PI; + } + + this.lam0 = adjustLon( + 0.5 * (lam1 + lam2) - atan((J * tan(0.5 * this.B * (lam1 - lam2))) / p) / this.B, + ); + gamma0 = atan((2 * sin(this.B * adjustLon(lam1 - this.lam0))) / (F - 1 / F)); + gamma = alphaC = asin(D * sin(gamma0)); + } + + this.singam = sin(gamma0); + this.cosgam = cos(gamma0); + this.sinrot = sin(gamma); + this.cosrot = cos(gamma); + + this.rB = 1 / this.B; + this.ArB = this.A * this.rB; + this.BrA = 1 / this.ArB; + + if (this.noOff) { + this.u0 = 0; + } else { + this.u0 = abs(this.ArB * atan(sqrt(D * D - 1) / cos(alphaC))); + + if (this.lat0 < 0) { + this.u0 = -this.u0; + } + } + + F = 0.5 * gamma0; + this.vPoleN = this.ArB * log(tan(QUART_PI - F)); + this.vPoleS = this.ArB * log(tan(QUART_PI + F)); + } + + /** + * HotineObliqueMercator forward equations--mapping lon-lat to x-y + * @param p - lon-lat WGS84 point + */ + forward(p: VectorPoint): void { + let S, T, U, V, W, temp, u, v; + p.x = p.x - this.lam0; + if (abs(abs(p.y) - HALF_PI) > EPSLN) { + W = this.E / pow(tsfnz(this.e, p.y, sin(p.y)), this.B); + temp = 1 / W; + S = 0.5 * (W - temp); + T = 0.5 * (W + temp); + V = sin(this.B * p.x); + U = (S * this.singam - V * this.cosgam) / T; + if (abs(abs(U) - 1.0) < EPSLN) { + throw new Error(); + } + v = 0.5 * this.ArB * log((1 - U) / (1 + U)); + temp = cos(this.B * p.x); + if (abs(temp) < this.TOL) { + u = this.A * p.x; + } else { + u = this.ArB * atan2(S * this.cosgam + V * this.singam, temp); + } + } else { + v = p.y > 0 ? this.vPoleN : this.vPoleS; + u = this.ArB * p.y; + } + if (this.noRot) { + p.x = u; + p.y = v; + } else { + u -= this.u0; + p.x = v * this.cosrot + u * this.sinrot; + p.y = u * this.cosrot - v * this.sinrot; + } + p.x = this.a * p.x + this.x0; + p.y = this.a * p.y + this.y0; + } + + /** + * HotineObliqueMercator inverse equations--mapping x-y to lon-lat + * @param p - HotineObliqueMercator point + */ + inverse(p: VectorPoint): void { + let u, v; + p.x = (p.x - this.x0) * (1.0 / this.a); + p.y = (p.y - this.y0) * (1.0 / this.a); + if (this.noRot) { + v = p.y; + u = p.x; + } else { + v = p.x * this.cosrot - p.y * this.sinrot; + u = p.y * this.cosrot + p.x * this.sinrot + this.u0; + } + const Qp = exp(-this.BrA * v); + const Sp = 0.5 * (Qp - 1 / Qp); + const Tp = 0.5 * (Qp + 1 / Qp); + const Vp = sin(this.BrA * u); + const Up = (Vp * this.cosgam + Sp * this.singam) / Tp; + if (abs(abs(Up) - 1) < EPSLN) { + p.x = 0; + p.y = Up < 0 ? -HALF_PI : HALF_PI; + } else { + p.y = this.E / sqrt((1 + Up) / (1 - Up)); + p.y = phi2z(this.e, pow(p.y, 1 / this.B)); + if (p.y === Infinity) { + throw new Error(); + } + p.x = -this.rB * atan2(Sp * this.cosgam - Vp * this.singam, cos(this.BrA * u)); + } + p.x += this.lam0; + } +} + +/** + * @param P - projection parameters + * @returns - true if projection is of type A + */ +function isTypeA(P: ProjectionParams): boolean { + const typeAProjections = [ + 'Hotine_Oblique_Mercator', + 'Hotine_Oblique_Mercator_Azimuth_Natural_Origin', + ]; + + return ( + 'noUoff' in P || + 'noOff' in P || + typeAProjections.indexOf(P.PROJECTION ?? P.name ?? 'XXXXXX') !== -1 + ); +} diff --git a/src/proj4/projections/ortho.ts b/src/proj4/projections/ortho.ts new file mode 100644 index 00000000..1cf9fe64 --- /dev/null +++ b/src/proj4/projections/ortho.ts @@ -0,0 +1,132 @@ +import { ProjectionBase } from '.'; +import { EPSLN, HALF_PI } from '../constants'; +import { adjustLon, asinz } from '../common'; + +import type { VectorPoint } from 's2-tools/geometry'; +import type { ProjectionParams, ProjectionTransform } from '.'; + +const { abs, sin, cos, sqrt, atan2 } = Math; + +/** + * # Orthographic + * + * The orthographic projection is a perspective azimuthal projection centered + * around a given latitude and longitude. + * + * **Classification**: Azimuthal + * + * **Available forms**: Forward and inverse, spherical and ellipsoidal + * + * **Defined area**: Global, although only one hemisphere can be seen at a time + * + * **Alias**: ortho + * + * **Domain**: 2D + * + * **Input type**: Geodetic coordinates + * + * **Output type**: Projected coordinates + * + * ## Projection String + * ``` + * +proj=ortho + * ``` + * + * ## Required Parameters + * - None + * + * ## Optional Parameters + * - `+alpha=`: Azimuth clockwise from north at the center of projection. *Defaults to 0.0.* (added in PROJ 9.5.0) + * - `+k_0=`: Scale factor. Determines scale factor used in the projection. *Defaults to 1.0.* (added in PROJ 9.5.0) + * - `+lon_0=` + * - `+lat_0=` + * - `+ellps=` + * - `+R=` + * - `+x_0=` + * - `+y_0=` + * + * ## Notes + * - Before PROJ 7.2, only the spherical formulation was implemented. To replicate PROJ < 7.2 results with newer versions, force the ellipsoid to a sphere using `+f=0`. + * - This projection method corresponds to `EPSG:9840` (or `EPSG:1130` with `k_0` or `alpha`). + * + * ![Orthographic](./images/ortho.png) + */ +export class Orthographic extends ProjectionBase implements ProjectionTransform { + name = 'Orthographic'; + static names = ['Orthographic', 'ortho']; + // Orthographic specific variables + sinP14: number; + cosP14: number; + + /** + * Preps an Orthographic projection + * @param params - projection specific parameters + */ + constructor(params?: ProjectionParams) { + super(params); + + this.sinP14 = sin(this.lat0); + this.cosP14 = cos(this.lat0); + } + + /** + * Orthographic forward equations--mapping lon-lat to x-y + * @param p - lon-lat WGS84 point + */ + forward(p: VectorPoint): void { + const { x: lon, y: lat } = p; + let x, y; + const dlon = adjustLon(lon - this.long0); /* delta longitude value */ + const sinphi = sin(lat); /* sin value */ + const cosphi = cos(lat); /* cos value */ + const coslon = cos(dlon); /* cos of longitude */ + const g = this.sinP14 * sinphi + this.cosP14 * cosphi * coslon; + const ksp = 1; /* scale factor */ + if (g > 0 || abs(g) <= EPSLN) { + x = this.a * ksp * cosphi * sin(dlon); + y = this.y0 + this.a * ksp * (this.cosP14 * sinphi - this.sinP14 * cosphi * coslon); + } else { + throw new Error('latitude out of range'); + } + p.x = x; + p.y = y; + } + + /** + * Orthographic inverse equations--mapping x-y to lon-lat + * @param p - Orthographic point + */ + inverse(p: VectorPoint): void { + let lon, lat; + p.x -= this.x0; + p.y -= this.y0; + const rh = sqrt(p.x * p.x + p.y * p.y); /* height above ellipsoid */ + const z = asinz(rh / this.a); /* angle */ + const sinz = sin(z); /* sin of z */ + const cosz = cos(z); /* cos of z */ + lon = this.long0; + if (abs(rh) <= EPSLN) { + lat = this.lat0; + p.x = lon; + p.y = lat; + return; + } + lat = asinz(cosz * this.sinP14 + (p.y * sinz * this.cosP14) / rh); + const con = abs(this.lat0) - HALF_PI; + if (abs(con) <= EPSLN) { + if (this.lat0 >= 0) { + lon = adjustLon(this.long0 + atan2(p.x, -p.y)); + } else { + lon = adjustLon(this.long0 - atan2(-p.x, p.y)); + } + p.x = lon; + p.y = lat; + return; + } + lon = adjustLon( + this.long0 + atan2(p.x * sinz, rh * this.cosP14 * cosz - p.y * this.sinP14 * sinz), + ); + p.x = lon; + p.y = lat; + } +} diff --git a/src/proj4/projections/poly.ts b/src/proj4/projections/poly.ts new file mode 100644 index 00000000..b3ea4a3a --- /dev/null +++ b/src/proj4/projections/poly.ts @@ -0,0 +1,174 @@ +import { EPSLN } from '../constants'; +import { ProjectionBase } from '.'; +import { adjustLat, adjustLon, e0fn, e1fn, e2fn, e3fn, gN, mlfn } from '../common'; + +import type { VectorPoint } from 's2-tools/geometry'; +import type { ProjectionParams, ProjectionTransform } from '.'; + +const { abs, pow, sin, cos, sqrt, asin, tan } = Math; + +/** + * # Polyconic (American) + * + * **Classification**: Pseudoconical + * + * **Available forms**: Forward and inverse, spherical and ellipsoidal + * + * **Defined area**: Global + * + * **Alias**: poly + * + * **Domain**: 2D + * + * **Input type**: Geodetic coordinates + * + * **Output type**: Projected coordinates + * + * ## Projection String + * ``` + * +proj=poly + * ``` + * + * ## Required Parameters + * - None + * + * ## Optional Parameters + * - `+lon_0=`: Central meridian. + * - `+ellps=`: Ellipsoid used. + * - `+R=`: Radius of the projection sphere. + * - `+x_0=`: False easting. + * - `+y_0=`: False northing. + * + * ![Polyconic (American)](./images/poly.png) + */ +export class Polyconic extends ProjectionBase implements ProjectionTransform { + name = 'Polyconic'; + static names = ['Polyconic', 'poly']; + // Polyconic specific variables + temp: number; + e0: number; + e1: number; + e2: number; + e3: number; + ml0: number; + + /** + * Preps an Polyconic projection + * @param params - projection specific parameters + */ + constructor(params?: ProjectionParams) { + super(params); + + this.temp = this.b / this.a; + this.es = 1 - pow(this.temp, 2); // devait etre dans tmerc.js mais n y est pas donc je commente sinon retour de valeurs nulles + this.e = sqrt(this.es); + this.e0 = e0fn(this.es); + this.e1 = e1fn(this.es); + this.e2 = e2fn(this.es); + this.e3 = e3fn(this.es); + this.ml0 = this.a * mlfn(this.e0, this.e1, this.e2, this.e3, this.lat0); //si que des zeros le calcul ne se fait pas + } + + /** + * Polyconic forward equations--mapping lon-lat to x-y + * @param p - lon-lat WGS84 point + */ + forward(p: VectorPoint): void { + const { x: lon, y: lat } = p; + let x, y; + const dlon = adjustLon(lon - this.long0); + const el = dlon * sin(lat); + if (this.sphere) { + if (abs(lat) <= EPSLN) { + x = this.a * dlon; + y = -1 * this.a * this.lat0; + } else { + x = (this.a * sin(el)) / tan(lat); + y = this.a * (adjustLat(lat - this.lat0) + (1 - cos(el)) / tan(lat)); + } + } else { + if (abs(lat) <= EPSLN) { + x = this.a * dlon; + y = -1 * this.ml0; + } else { + const nl = gN(this.a, this.e, sin(lat)) / tan(lat); + x = nl * sin(el); + y = this.a * mlfn(this.e0, this.e1, this.e2, this.e3, lat) - this.ml0 + nl * (1 - cos(el)); + } + } + p.x = x + this.x0; + p.y = y + this.y0; + } + + /** + * Polyconic inverse equations--mapping x-y to lon-lat + * @param p - Polyconic point + */ + inverse(p: VectorPoint): void { + let lon, i; + let lat = 0; + let al, bl; + let phi, dphi; + const x = p.x - this.x0; + const y = p.y - this.y0; + if (this.sphere) { + if (abs(y + this.a * this.lat0) <= EPSLN) { + lon = adjustLon(x / this.a + this.long0); + lat = 0; + } else { + al = this.lat0 + y / this.a; + bl = (x * x) / this.a / this.a + al * al; + phi = al; + let tanphi; + for (i = 20; i; --i) { + tanphi = tan(phi); + dphi = + (-1 * (al * (phi * tanphi + 1) - phi - 0.5 * (phi * phi + bl) * tanphi)) / + ((phi - al) / tanphi - 1); + phi += dphi; + if (abs(dphi) <= EPSLN) { + lat = phi; + break; + } + } + lon = adjustLon(this.long0 + asin((x * tan(phi)) / this.a) / sin(lat)); + } + } else { + if (abs(y + this.ml0) <= EPSLN) { + lon = adjustLon(this.long0 + x / this.a); + } else { + al = (this.ml0 + y) / this.a; + bl = (x * x) / this.a / this.a + al * al; + phi = al; + let cl, mln, mlnp, ma; + let con; + for (i = 20; i; --i) { + con = this.e * sin(phi); + cl = sqrt(1 - con * con) * tan(phi); + mln = this.a * mlfn(this.e0, this.e1, this.e2, this.e3, phi); + mlnp = + this.e0 - + 2 * this.e1 * cos(2 * phi) + + 4 * this.e2 * cos(4 * phi) - + 6 * this.e3 * cos(6 * phi); + ma = mln / this.a; + dphi = + (al * (cl * ma + 1) - ma - 0.5 * cl * (ma * ma + bl)) / + ((this.es * sin(2 * phi) * (ma * ma + bl - 2 * al * ma)) / (4 * cl) + + (al - ma) * (cl * mlnp - 2 / sin(2 * phi)) - + mlnp); + phi -= dphi; + if (abs(dphi) <= EPSLN) { + lat = phi; + break; + } + } + // lat = phi4z(this.e, this.e0, this.e1, this.e2, this.e3, al, bl, 0, 0); + cl = sqrt(1 - this.es * pow(sin(lat), 2)) * tan(lat); + lon = adjustLon(this.long0 + asin((x * cl) / this.a) / sin(lat)); + } + } + p.x = lon; + p.y = lat; + } +} diff --git a/src/proj4/projections/qsc.ts b/src/proj4/projections/qsc.ts new file mode 100644 index 00000000..2a877856 --- /dev/null +++ b/src/proj4/projections/qsc.ts @@ -0,0 +1,488 @@ +import { ProjectionBase } from '.'; +import { EPSLN, HALF_PI, QUART_PI, SPI, TWO_PI } from '../constants'; + +import type { VectorPoint } from 's2-tools/geometry'; +import type { ProjectionParams, ProjectionTransform } from '.'; + +const { abs, sin, cos, sqrt, atan2, atan, tan, acos } = Math; + +/** Face enum */ +enum FACE_ENUM { + FRONT = 1, + RIGHT = 2, + BACK = 3, + LEFT = 4, + TOP = 5, + BOTTOM = 6, +} + +/** Area enum */ +enum AREA_ENUM { + AREA_0 = 1, + AREA_1 = 2, + AREA_2 = 3, + AREA_3 = 4, +} + +/** Area value object to track */ +interface Area { + value: number; +} + +/** + * # Quadrilateralized Spherical Cube + * + * The purpose of the Quadrilateralized Spherical Cube (QSC) projection is to project + * a sphere surface onto the six sides of a cube: + * + * **Classification**: Azimuthal + * + * **Available forms**: Forward and inverse, ellipsoidal + * + * **Defined area**: Global + * + * **Alias**: qsc + * + * **Domain**: 2D + * + * **Input type**: Geodetic coordinates + * + * **Output type**: Projected coordinates + * + * ## Projection String + * ``` + * +proj=qsc + * ``` + * + * For this purpose, other alternatives can be used, notably `gnom` or + * `healpix`. However, QSC projection has the following favorable properties: + * + * It is an equal-area projection, and at the same time introduces only limited angular + * distortions. It treats all cube sides equally, i.e. it does not use different + * projections for polar areas and equatorial areas. These properties make QSC + * projection a good choice for planetary-scale terrain rendering. Map data can be + * organized in quadtree structures for each cube side. See `LambersKolb2012` for an example. + * + * The QSC projection was introduced by `ONeilLaubscher1976`, + * building on previous work by `ChanONeil1975`. For clarity: The + * earlier QSC variant described in `ChanONeil1975` became known as the COBE QSC since it + * was used by the NASA Cosmic Background Explorer (COBE) project; it is an approximately + * equal-area projection and is not the same as the QSC projection. + * See also `CalabrettaGreisen2002` Sec. 5.6.2 and 5.6.3 for a description of both and + * some analysis. + * + * In this implementation, the QSC projection projects onto one side of a circumscribed + * cube. The cube side is selected by choosing one of the following six projection centers: + * + * `+lat_0=0 +lon_0=0` | front cube side | + * + * `+lat_0=0 +lon_0=90` | right cube side | + * + * `+lat_0=0 +lon_0=180` | back cube side | + * + * `+lat_0=0 +lon_0=-90` | left cube side | + * + * `+lat_0=90` | top cube side | + * + * `+lat_0=-90` | bottom cube side | + * + * ## Required Parameters + * - `+lat_0=`: Latitude of the projection center. + * - `+lon_0=`: Longitude of the projection center. + * + * ## Optional Parameters + * - `+ellps=`: Ellipsoid parameters (default: `WGS84`). + * - `+x_0=`: False easting. + * - `+y_0=`: False northing. + * + * ## Usage Example + * ``` + * gdalwarp -t_srs "+wktext +proj=qsc +units=m +ellps=WGS84 +lat_0=0 +lon_0=0" \ + * -wo SOURCE_EXTRA=100 -wo SAMPLE_GRID=YES -te -6378137 -6378137 6378137 6378137 \ + * worldmap.tiff frontside.tiff + * + * gdalwarp -t_srs "+wktext +proj=qsc +units=m +ellps=WGS84 +lat_0=0 +lon_0=90" \ + * -wo SOURCE_EXTRA=100 -wo SAMPLE_GRID=YES -te -6378137 -6378137 6378137 6378137 \ + * worldmap.tiff rightside.tiff + * + * gdalwarp -t_srs "+wktext +proj=qsc +units=m +ellps=WGS84 +lat_0=0 +lon_0=180" \ + * -wo SOURCE_EXTRA=100 -wo SAMPLE_GRID=YES -te -6378137 -6378137 6378137 6378137 \ + * worldmap.tiff backside.tiff + * + * gdalwarp -t_srs "+wktext +proj=qsc +units=m +ellps=WGS84 +lat_0=0 +lon_0=-90" \ + * -wo SOURCE_EXTRA=100 -wo SAMPLE_GRID=YES -te -6378137 -6378137 6378137 6378137 \ + * worldmap.tiff leftside.tiff + * + * gdalwarp -t_srs "+wktext +proj=qsc +units=m +ellps=WGS84 +lat_0=90 +lon_0=0" \ + * -wo SOURCE_EXTRA=100 -wo SAMPLE_GRID=YES -te -6378137 -6378137 6378137 6378137 \ + * worldmap.tiff topside.tiff + * + * gdalwarp -t_srs "+wktext +proj=qsc +units=m +ellps=WGS84 +lat_0=-90 +lon_0=0" \ + * -wo SOURCE_EXTRA=100 -wo SAMPLE_GRID=YES -te -6378137 -6378137 6378137 6378137 \ + * worldmap.tiff bottomside.tiff + * ``` + * + * ## Further Reading + * - [Wikipedia](https://en.wikipedia.org/wiki/Quadrilateralized_spherical_cube) + * - [NASA](https://lambda.gsfc.nasa.gov/product/cobe/skymap_info_new.cfm) + * + * ![Quadrilateralized Spherical Cube](./images/qsc_concept.jpg) + */ +export class QuadrilateralizedSphericalCube extends ProjectionBase implements ProjectionTransform { + name = 'QuadrilateralizedSphericalCube'; + static names = [ + 'QuadrilateralizedSphericalCube', + 'Quadrilateralized Spherical Cube', + 'Quadrilateralized_Spherical_Cube', + 'qsc', + ]; + // QuadrilateralizedSphericalCube specific variables + declare x0: number; + declare y0: number; + declare lat0: number; + declare long0: number; + declare latTs: number; + oneMinusF = 0; + oneMinusFSquared = 0; + face: FACE_ENUM; + declare area: AREA_ENUM; + + /** + * Preps an QuadrilateralizedSphericalCube projection + * QSC projection rewritten from the original PROJ4 + * https://github.com/OSGeo/proj.4/blob/master/src/PJ_qsc.c + * @param params - projection specific parameters + */ + constructor(params?: ProjectionParams) { + super(params); + + this.x0 = this.x0 ?? 0; + this.y0 = this.y0 ?? 0; + this.lat0 = this.lat0 ?? 0; + this.long0 = this.long0 ?? 0; + this.latTs = this.latTs ?? 0; + + /* Determine the cube face from the center of projection. */ + if (this.lat0 >= HALF_PI - QUART_PI / 2.0) { + this.face = FACE_ENUM.TOP; + } else if (this.lat0 <= -(HALF_PI - QUART_PI / 2.0)) { + this.face = FACE_ENUM.BOTTOM; + } else if (abs(this.long0) <= QUART_PI) { + this.face = FACE_ENUM.FRONT; + } else if (abs(this.long0) <= HALF_PI + QUART_PI) { + 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.oneMinusF = 1 - (this.a - this.b) / this.a; + this.oneMinusFSquared = this.oneMinusF * this.oneMinusF; + } + } + + /** + * QuadrilateralizedSphericalCube forward equations--mapping lon-lat to x-y + * @param p - lon-lat WGS84 point + */ + forward(p: VectorPoint): void { + const xy = { x: 0, y: 0 }; + let lat, lon; + let theta, phi; + let mu; + /* nu; */ + const 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 = atan(this.oneMinusFSquared * 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 >= QUART_PI && lon <= HALF_PI + QUART_PI) { + area.value = AREA_ENUM.AREA_0; + theta = lon - HALF_PI; + } else if (lon > HALF_PI + QUART_PI || lon <= -(HALF_PI + QUART_PI)) { + area.value = AREA_ENUM.AREA_1; + theta = lon > 0.0 ? lon - SPI : lon + SPI; + } else if (lon > -(HALF_PI + QUART_PI) && lon <= -QUART_PI) { + 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 >= QUART_PI && lon <= HALF_PI + QUART_PI) { + area.value = AREA_ENUM.AREA_0; + theta = -lon + HALF_PI; + } else if (lon < QUART_PI && lon >= -QUART_PI) { + area.value = AREA_ENUM.AREA_1; + theta = -lon; + } else if (lon < -QUART_PI && lon >= -(HALF_PI + QUART_PI)) { + 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 { + if (this.face === FACE_ENUM.RIGHT) { + lon = qscShiftLonOrigin(lon, HALF_PI); + } else if (this.face === FACE_ENUM.BACK) { + lon = qscShiftLonOrigin(lon, SPI); + } else if (this.face === FACE_ENUM.LEFT) { + lon = qscShiftLonOrigin(lon, -HALF_PI); + } + const sinlat = sin(lat); + const coslat = cos(lat); + const sinlon = sin(lon); + const coslon = cos(lon); + const q = coslat * coslon; + const r = coslat * sinlon; + const s = sinlat; + if (this.face === FACE_ENUM.FRONT) { + phi = acos(q); + theta = qscFwdEquatFaceTheta(phi, s, r, area); + } else if (this.face === FACE_ENUM.RIGHT) { + phi = acos(r); + theta = qscFwdEquatFaceTheta(phi, s, -q, area); + } else if (this.face === FACE_ENUM.BACK) { + phi = acos(-q); + theta = qscFwdEquatFaceTheta(phi, s, -r, area); + } else if (this.face === FACE_ENUM.LEFT) { + phi = acos(-r); + theta = qscFwdEquatFaceTheta(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 = atan((12 / SPI) * (theta + acos(sin(theta) * cos(QUART_PI)) - HALF_PI)); + const t = sqrt((1 - cos(phi)) / (cos(mu) * cos(mu)) / (1 - cos(atan(1 / 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 * cos(mu); + xy.y = t * 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; + } + + /** + * QuadrilateralizedSphericalCube inverse equations--mapping x-y to lon-lat + * @param p - QuadrilateralizedSphericalCube point + */ + inverse(p: VectorPoint): void { + const lp = { lam: 0, phi: 0 }; + let mu; + let t; + let phi; + const 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. */ + const nu = atan(sqrt(p.x * p.x + p.y * p.y)); + mu = atan2(p.y, p.x); + if (p.x >= 0.0 && p.x >= abs(p.y)) { + area.value = AREA_ENUM.AREA_0; + } else if (p.y >= 0.0 && p.y >= abs(p.x)) { + area.value = AREA_ENUM.AREA_1; + mu -= HALF_PI; + } else if (p.x < 0.0 && -p.x >= 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) * tan(mu); + const tantheta = sin(t) / (cos(t) - 1 / sqrt(2)); + const theta = atan(tantheta); + const cosmu = cos(mu); + const tannu = tan(nu); + let cosphi = 1 - cosmu * cosmu * tannu * tannu * (1 - cos(atan(1 / 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 = 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; + } /* area.value == AREA_ENUM.AREA_3 */ else { + lp.lam = theta; + } + } else if (this.face === FACE_ENUM.BOTTOM) { + phi = 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; + } /* area.value == AREA_ENUM.AREA_3 */ else { + lp.lam = theta < 0.0 ? -theta - SPI : -theta + SPI; + } + } else { + /* Compute phi and lam via cartesian unit sphere coordinates. */ + let q, r, s; + q = cosphi; + t = q * q; + if (t >= 1) { + s = 0; + } else { + s = sqrt(1 - t) * sin(theta); + } + t += s * s; + if (t >= 1) { + r = 0; + } else { + r = 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 = acos(-s) - HALF_PI; + lp.lam = atan2(r, q); + if (this.face === FACE_ENUM.RIGHT) { + lp.lam = qscShiftLonOrigin(lp.lam, -HALF_PI); + } else if (this.face === FACE_ENUM.BACK) { + lp.lam = qscShiftLonOrigin(lp.lam, -SPI); + } else if (this.face === FACE_ENUM.LEFT) { + lp.lam = qscShiftLonOrigin(lp.lam, HALF_PI); + } + } + /* Apply the shift from the sphere to the ellipsoid as described + * in [LK12]. */ + if (this.es !== 0) { + const invert_sign = lp.phi < 0 ? 1 : 0; + const tanphi = tan(lp.phi); + const xa = this.b / sqrt(tanphi * tanphi + this.oneMinusFSquared); + lp.phi = atan(sqrt(this.a * this.a - xa * xa) / (this.oneMinusF * xa)); + if (invert_sign) { + lp.phi = -lp.phi; + } + } + lp.lam += this.long0; + p.x = lp.lam; + p.y = lp.phi; + } +} + +/** + * Helper function for forward projection: compute the theta angle + * @param phi - phi + * @param y - y + * @param x - x + * @param area - area + * @returns - theta + */ +function qscFwdEquatFaceTheta(phi: number, y: number, x: number, area: Area): number { + let theta; + if (phi < EPSLN) { + area.value = AREA_ENUM.AREA_0; + theta = 0.0; + } else { + theta = atan2(y, x); + if (abs(theta) <= QUART_PI) { + area.value = AREA_ENUM.AREA_0; + } else if (theta > QUART_PI && theta <= HALF_PI + QUART_PI) { + area.value = AREA_ENUM.AREA_1; + theta -= HALF_PI; + } else if (theta > HALF_PI + QUART_PI || theta <= -(HALF_PI + QUART_PI)) { + 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. + * @param lon - longitude + * @param offset - shift amount + * @returns - shifted longitude + */ +function qscShiftLonOrigin(lon: number, offset: number): number { + let slon = lon + offset; + if (slon < -SPI) { + slon += TWO_PI; + } else if (slon > SPI) { + slon -= TWO_PI; + } + + return slon; +} diff --git a/src/proj4/projections/references.ts b/src/proj4/projections/references.ts index c7810e08..e39b372c 100644 --- a/src/proj4/projections/references.ts +++ b/src/proj4/projections/references.ts @@ -29,3 +29,48 @@ export const NAD83 = */ export const PseudoMercator = '+title=WGS 84 / Pseudo-Mercator +proj=merc +a=6378137 +b=6378137 +lat_ts=0.0 +lon_0=0.0 +x_0=0.0 +y_0=0 +k=1.0 +units=m +nadgrids=@null +no_defs'; + +/** + * **UTM Zone 33N** projection (Northern Hemisphere) + * - UTM Zone 33N + * - EPSG:32633 + */ +export const UTM33N = '+title=UTM Zone 33N +proj=utm +zone=33 +datum=WGS84 +units=m +no_defs'; + +/** + * **UTM Zone 33S** projection (Southern Hemisphere) + * - UTM Zone 33S + * - EPSG:32733 + */ +export const UTM33S = '+title=UTM Zone 33S +proj=utm +zone=33 +datum=WGS84 +units=m +no_defs'; + +/** + * **British National Grid** projection + * - OSGB36 + * - EPSG:27700 + */ +export const BritishNationalGrid = + '+title=OSGB 1936 / British National Grid +proj=tmerc +lat_0=49 +lon_0=-2 +k=0.9996012717 +x_0=400000 +y_0=-100000 +datum=OSGB36 +units=m +no_defs'; + +/** + * **NAD27** projection + * - NAD27 + * - EPSG:4267 + */ +export const NAD27 = '+title=NAD27 (long/lat) +proj=longlat +datum=NAD27 +no_defs'; + +/** + * **Lambert Conformal Conic** projection (France) + * - Lambert 93 + * - EPSG:2154 + */ +export const Lambert93 = + '+title=RGF93 / Lambert-93 +proj=lcc +lat_1=44 +lat_2=49 +lat_0=46.5 +lon_0=3 +x_0=700000 +y_0=6600000 +datum=RGF93 +units=m +no_defs'; + +/** + * **Swiss Grid** projection + * - CH1903+ + * - EPSG:2056 + */ +export const SwissGrid = + '+title=CH1903+ / LV95 +proj=somerc +lat_0=46.95240555555556 +lon_0=7.439583333333333 +k_0=1 +x_0=2600000 +y_0=1200000 +ellps=bessel +units=m +no_defs'; diff --git a/src/proj4/projections/robin.ts b/src/proj4/projections/robin.ts new file mode 100644 index 00000000..b6c76ed9 --- /dev/null +++ b/src/proj4/projections/robin.ts @@ -0,0 +1,237 @@ +import { ProjectionBase } from '.'; +import { adjustLon } from '../common'; +import { D2R, EPSLN, HALF_PI, R2D } from '../constants'; + +import type { VectorPoint } from 's2-tools/geometry'; +import type { ProjectionParams, ProjectionTransform } from '.'; + +const { abs, floor } = Math; + +const COEFS_X = [ + [1.0, 2.2199e-17, -7.15515e-5, 3.1103e-6], + [0.9986, -0.000482243, -2.4897e-5, -1.3309e-6], + [0.9954, -0.00083103, -4.48605e-5, -9.86701e-7], + [0.99, -0.00135364, -5.9661e-5, 3.6777e-6], + [0.9822, -0.00167442, -4.49547e-6, -5.72411e-6], + [0.973, -0.00214868, -9.03571e-5, 1.8736e-8], + [0.96, -0.00305085, -9.00761e-5, 1.64917e-6], + [0.9427, -0.00382792, -6.53386e-5, -2.6154e-6], + [0.9216, -0.00467746, -0.00010457, 4.81243e-6], + [0.8962, -0.00536223, -3.23831e-5, -5.43432e-6], + [0.8679, -0.00609363, -0.000113898, 3.32484e-6], + [0.835, -0.00698325, -6.40253e-5, 9.34959e-7], + [0.7986, -0.00755338, -5.00009e-5, 9.35324e-7], + [0.7597, -0.00798324, -3.5971e-5, -2.27626e-6], + [0.7186, -0.00851367, -7.01149e-5, -8.6303e-6], + [0.6732, -0.00986209, -0.000199569, 1.91974e-5], + [0.6213, -0.010418, 8.83923e-5, 6.24051e-6], + [0.5722, -0.00906601, 0.000182, 6.24051e-6], + [0.5322, -0.00677797, 0.000275608, 6.24051e-6], +]; + +const COEFS_Y = [ + [-5.20417e-18, 0.0124, 1.21431e-18, -8.45284e-11], + [0.062, 0.0124, -1.26793e-9, 4.22642e-10], + [0.124, 0.0124, 5.07171e-9, -1.60604e-9], + [0.186, 0.0123999, -1.90189e-8, 6.00152e-9], + [0.248, 0.0124002, 7.10039e-8, -2.24e-8], + [0.31, 0.0123992, -2.64997e-7, 8.35986e-8], + [0.372, 0.0124029, 9.88983e-7, -3.11994e-7], + [0.434, 0.0123893, -3.69093e-6, -4.35621e-7], + [0.4958, 0.0123198, -1.02252e-5, -3.45523e-7], + [0.5571, 0.0121916, -1.54081e-5, -5.82288e-7], + [0.6176, 0.0119938, -2.41424e-5, -5.25327e-7], + [0.6769, 0.011713, -3.20223e-5, -5.16405e-7], + [0.7346, 0.0113541, -3.97684e-5, -6.09052e-7], + [0.7903, 0.0109107, -4.89042e-5, -1.04739e-6], + [0.8435, 0.0103431, -6.4615e-5, -1.40374e-9], + [0.8936, 0.00969686, -6.4636e-5, -8.547e-6], + [0.9394, 0.00840947, -0.000192841, -4.2106e-6], + [0.9761, 0.00616527, -0.000256, -4.2106e-6], + [1.0, 0.00328947, -0.000319159, -4.2106e-6], +]; + +const FXC = 0.8487; +const FYC = 1.3523; +const C1 = R2D / 5; // rad to 5-degree interval +const RC1 = 1 / C1; +const NODES = 18; + +/** + * # Robinson + * + * **Classification**: Pseudocylindrical + * + * **Available forms**: Forward and inverse, spherical projection + * + * **Defined area**: Global + * + * **Alias**: robin + * + * **Domain**: 2D + * + * **Input type**: Geodetic coordinates + * + * **Output type**: Projected coordinates + * + * ## Projection String + * ``` + * +proj=robin + * ``` + * + * ## Required Parameters + * - None + * + * ## Optional Parameters + * - `+lon_0=`: Central meridian. + * - `+R=`: Radius of the projection sphere. + * - `+x_0=`: False easting. + * - `+y_0=`: False northing. + * + * ![Robinson](./images/robin.png) + */ +export class Robinson extends ProjectionBase implements ProjectionTransform { + name = 'Robinson'; + static names = ['Robinson', 'robin']; + // Robinson specific variables + declare x0: number; + declare y0: number; + declare long0: number; + + /** + * Preps an Robinson projection + * Based on https://github.com/OSGeo/proj.4/blob/master/src/PJ_robin.c + * Polynomial coeficients from http://article.gmane.org/gmane.comp.gis.proj-4.devel/6039 + * @param params - projection specific parameters + */ + constructor(params?: ProjectionParams) { + super(params); + + this.x0 = this.x0 ?? 0; + this.y0 = this.y0 ?? 0; + this.long0 = this.long0 ?? 0; + } + + /** + * Robinson forward equations--mapping lon-lat to x-y + * @param p - lon-lat WGS84 point + */ + forward(p: VectorPoint): void { + const lon = adjustLon(p.x - this.long0); + let dphi = abs(p.y); + let i = floor(dphi * C1); + if (i < 0) { + i = 0; + } else if (i >= NODES) { + i = NODES - 1; + } + dphi = R2D * (dphi - RC1 * i); + const xy = { + x: poly3Val(COEFS_X[i], dphi) * lon, + y: poly3Val(COEFS_Y[i], dphi), + z: p.z, + m: p.m, + }; + if (p.y < 0) { + xy.y = -xy.y; + } + p.x = xy.x * this.a * FXC + this.x0; + p.y = xy.y * this.a * FYC + this.y0; + } + + /** + * Robinson inverse equations--mapping x-y to lon-lat + * @param p - Robinson point + */ + inverse(p: VectorPoint): void { + const ll = { + x: (p.x - this.x0) / (this.a * FXC), + y: abs(p.y - this.y0) / (this.a * FYC), + }; + if (ll.y >= 1) { + // pathologic case + ll.x /= COEFS_X[NODES][0]; + ll.y = p.y < 0 ? -HALF_PI : HALF_PI; + } else { + // find table interval + let i = floor(ll.y * NODES); + if (i < 0) { + i = 0; + } else if (i >= NODES) { + i = NODES - 1; + } + while (true) { + if (COEFS_Y[i][0] > ll.y) { + --i; + } else if (COEFS_Y[i + 1][0] <= ll.y) { + ++i; + } else { + break; + } + } + // linear interpolation in 5 degree interval + const coefs = COEFS_Y[i]; + let t = (5 * (ll.y - coefs[0])) / (COEFS_Y[i + 1][0] - coefs[0]); + // find t so that poly3Val(coefs, t) = ll.y + t = newtonRapshon( + (x: number): number => { + return (poly3Val(coefs, x) - ll.y) / poly3Der(coefs, x); + }, + t, + EPSLN, + 100, + ); + ll.x /= poly3Val(COEFS_X[i], t); + ll.y = (5 * i + t) * D2R; + if (p.y < 0) { + ll.y = -ll.y; + } + } + ll.x = adjustLon(ll.x + this.long0); + + p.x = ll.x; + p.y = ll.y; + } +} + +/** + * @param coefs - coefficient array + * @param x - argument + * @returns - value + */ +function poly3Val(coefs: number[], x: number): number { + return coefs[0] + x * (coefs[1] + x * (coefs[2] + x * coefs[3])); +} + +/** + * @param coefs - coefficient array + * @param x - argument + * @returns - derivative + */ +function poly3Der(coefs: number[], x: number): number { + return coefs[1] + x * (2 * coefs[2] + x * 3 * coefs[3]); +} + +/** + * @param fDf - derivative function of f + * @param start - starting guess + * @param max_err - maximum error + * @param iters - maximum number of iterations + * @returns - new guess + */ +function newtonRapshon( + fDf: (x: number) => number, + start: number, + max_err: number, + iters: number, +): number { + let x = start; + for (; iters; --iters) { + const upd = fDf(x); + x -= upd; + if (abs(upd) < max_err) { + break; + } + } + return x; +} diff --git a/src/proj4/projections/sinu.ts b/src/proj4/projections/sinu.ts new file mode 100644 index 00000000..afc99c01 --- /dev/null +++ b/src/proj4/projections/sinu.ts @@ -0,0 +1,169 @@ +import { ProjectionBase } from '.'; +import { EPSLN, HALF_PI } from '../constants'; +import { adjustLat, adjustLon, asinz, pjEnfn, pjInvMlfn, pjMlfn } from '../common'; + +import type { En } from '../common'; +import type { VectorPoint } from 's2-tools/geometry'; +import type { ProjectionParams, ProjectionTransform } from '.'; + +const { abs, sin, cos, sqrt, asin } = Math; + +/** + * # Sinusoidal (Sanson-Flamsteed) + * + * **Classification**: Pseudocylindrical + * + * **Available forms**: Forward and inverse, spherical and ellipsoidal + * + * **Defined area**: Global + * + * **Alias**: sinu + * + * **Domain**: 2D + * + * **Input type**: Geodetic coordinates + * + * **Output type**: Projected coordinates + * + * ## Projection String + * ``` + * +proj=sinu + * ``` + * + * ## Parameters + * + * All parameters are optional. + * + * - `+lon_0=`: Central meridian. + * - `+R=`: Radius of the sphere or semi-major axis of the ellipsoid. + * - `+x_0=`: False easting. + * - `+y_0=`: False northing. + * + * ## Mathematical Definition + * + * MacBryde and Thomas developed generalized formulas for several of the + * pseudocylindricals with sinusoidal meridians. The formulas describing the Sinusoidal + * projection are: + * + * Forward projection: + * ``` + * x = C * λ * (m + cos(θ)) / (m + 1) + * y = C * θ + * ``` + * + * Inverse projection: + * ``` + * λ = x * (m + 1) / (C * (m + cos(y / C))) + * θ = y / C + * ``` + * + * Where: + * ``` + * C = sqrt((m + 1) / n) + * ``` + * + * ## Further Reading + * - [Wikipedia](https://en.wikipedia.org/wiki/Sinusoidal_projection) + * + * ![Sinusoidal (Sanson-Flamsteed)](./images/sinu.png) + */ +export class Sinusoidal extends ProjectionBase implements ProjectionTransform { + name = 'Sinusoidal'; + static names = ['Sinusoidal', 'sinu']; + // Sinusoidal specific variables + declare en: En; + declare n: number; + declare m: number; + declare Cy: number; + declare Cx: number; + + /** + * Preps an Sinusoidal projection + * @param params - projection specific parameters + */ + constructor(params?: ProjectionParams) { + super(params); + + if (!this.sphere) { + this.en = pjEnfn(this.es); + } else { + this.n = 1; + this.m = 0; + this.es = 0; + this.Cy = sqrt((this.m + 1) / this.n); + this.Cx = this.Cy / (this.m + 1); + } + } + + /** + * Sinusoidal forward equations--mapping lon-lat to x-y + * @param p - lon-lat WGS84 point + */ + forward(p: VectorPoint): void { + let x, y; + let lon = p.x; + let lat = p.y; + /* Forward equations + -----------------*/ + lon = adjustLon(lon - this.long0); + if (this.sphere) { + if (!this.m) { + lat = this.n !== 1 ? asin(this.n * sin(lat)) : lat; + } else { + const k = this.n * sin(lat); + for (let i = 20; i; --i) { + const V = (this.m * lat + sin(lat) - k) / (this.m + cos(lat)); + lat -= V; + if (abs(V) < EPSLN) { + break; + } + } + } + x = this.a * this.Cx * lon * (this.m + cos(lat)); + y = this.a * this.Cy * lat; + } else { + const s = sin(lat); + const c = cos(lat); + y = this.a * pjMlfn(lat, s, c, this.en); + x = (this.a * lon * c) / sqrt(1 - this.es * s * s); + } + p.x = x; + p.y = y; + } + + /** + * Sinusoidal inverse equations--mapping x-y to lon-lat + * @param p - Sinusoidal point + */ + inverse(p: VectorPoint): void { + let lat, temp, lon, s; + p.x -= this.x0; + lon = p.x / this.a; + p.y -= this.y0; + lat = p.y / this.a; + if (this.sphere) { + lat /= this.Cy; + lon = lon / (this.Cx * (this.m + cos(lat))); + if (this.m) { + lat = asinz((this.m * lat + sin(lat)) / this.n); + } else if (this.n !== 1) { + lat = asinz(sin(lat) / this.n); + } + lon = adjustLon(lon + this.long0); + lat = adjustLat(lat); + } else { + lat = pjInvMlfn(p.y / this.a, this.es, this.en); + s = abs(lat); + if (s < HALF_PI) { + s = sin(lat); + temp = this.long0 + (p.x * sqrt(1 - this.es * s * s)) / (this.a * cos(lat)); + //temp = this.long0 + p.x / (this.a * cos(lat)); + lon = adjustLon(temp); + } else if (s - EPSLN < HALF_PI) { + lon = this.long0; + } + } + p.x = lon; + p.y = lat; + } +} diff --git a/src/proj4/projections/somerc.ts b/src/proj4/projections/somerc.ts new file mode 100644 index 00000000..2418402a --- /dev/null +++ b/src/proj4/projections/somerc.ts @@ -0,0 +1,132 @@ +import { ProjectionBase } from '.'; + +import type { VectorPoint } from 's2-tools/geometry'; +import type { ProjectionParams, ProjectionTransform } from '.'; + +const { abs, pow, sin, cos, sqrt, asin, atan, exp, log, tan, PI } = Math; + +/** + * # Swiss Oblique Mercator + * + * **Classification**: Oblique Mercator + * + * **Available forms**: Forward and inverse, ellipsoidal only + * + * **Defined area**: Global + * + * **Alias**: somerc + * + * **Domain**: 2D + * + * **Input type**: Geodetic coordinates + * + * **Output type**: Projected coordinates + * + * ## Projection String + * ``` + * +proj=somerc + * ``` + * + * ## Required Parameters + * - None + * + * ## Optional Parameters + * - `+lon_0=`: Central meridian. + * - `+ellps=`: Ellipsoid used. + * - `+R=`: Radius of the projection sphere. + * - `+k_0=`: Scale factor. + * - `+x_0=`: False easting. + * - `+y_0=`: False northing. + * + * ## References: + * Formules et constantes pour le Calcul pour la + * projection cylindrique conforme à axe oblique et pour la transformation entre + * des systèmes de référence. + * http://www.swisstopo.admin.ch/internet/swisstopo/fr/home/topics/survey/sys/refsys/switzerland.parsysrelated1.31216.downloadList.77004.DownloadFile.tmp/swissprojectionfr.pdf + * + * ![Swiss Oblique Mercator](./images/somerc.png) + */ +export class SwissObliqueMercator extends ProjectionBase implements ProjectionTransform { + name = 'SwissObliqueMercator'; + static names = ['SwissObliqueMercator', 'Swiss Oblique Mercator', 'somerc']; + // SwissObliqueMercator specific variables + lambda0: number; + alpha: number; + b0: number; + K: number; + R: number; + + /** + * Preps an SwissObliqueMercator projection + * @param params - projection specific parameters + */ + constructor(params?: ProjectionParams) { + super(params); + + const phy0 = this.lat0; + this.lambda0 = this.long0; + const sinPhy0 = sin(phy0); + const semiMajorAxis = this.a; + const invF = this.rf; + const flattening = 1 / invF; + const e2 = 2 * flattening - pow(flattening, 2); + const e = (this.e = sqrt(e2)); + this.R = (this.k0 * semiMajorAxis * sqrt(1 - e2)) / (1 - e2 * pow(sinPhy0, 2)); + this.alpha = sqrt(1 + (e2 / (1 - e2)) * pow(cos(phy0), 4)); + this.b0 = asin(sinPhy0 / this.alpha); + const k1 = log(tan(PI / 4 + this.b0 / 2)); + const k2 = log(tan(PI / 4 + phy0 / 2)); + const k3 = log((1 + e * sinPhy0) / (1 - e * sinPhy0)); + this.K = k1 - this.alpha * k2 + ((this.alpha * e) / 2) * k3; + } + + /** + * SwissObliqueMercator forward equations--mapping lon-lat to x-y + * @param p - lon-lat WGS84 point + */ + forward(p: VectorPoint): void { + const Sa1 = log(tan(PI / 4 - p.y / 2)); + const Sa2 = (this.e / 2) * log((1 + this.e * sin(p.y)) / (1 - this.e * sin(p.y))); + const S = -this.alpha * (Sa1 + Sa2) + this.K; + // spheric latitude + const b = 2 * (atan(exp(S)) - PI / 4); + // spheric longitude + const I = this.alpha * (p.x - this.lambda0); + // psoeudo equatorial rotation + const rotI = atan(sin(I) / (sin(this.b0) * tan(b) + cos(this.b0) * cos(I))); + const rotB = asin(cos(this.b0) * sin(b) - sin(this.b0) * cos(b) * cos(I)); + p.y = (this.R / 2) * log((1 + sin(rotB)) / (1 - sin(rotB))) + this.y0; + p.x = this.R * rotI + this.x0; + } + + /** + * SwissObliqueMercator inverse equations--mapping x-y to lon-lat + * @param p - SwissObliqueMercator point + */ + inverse(p: VectorPoint): void { + const Y = p.x - this.x0; + const X = p.y - this.y0; + const rotI = Y / this.R; + const rotB = 2 * (atan(exp(X / this.R)) - PI / 4); + const b = asin(cos(this.b0) * sin(rotB) + sin(this.b0) * cos(rotB) * cos(rotI)); + const I = atan(sin(rotI) / (cos(this.b0) * cos(rotI) - sin(this.b0) * tan(rotB))); + const lambda = this.lambda0 + I / this.alpha; + let S = 0; + let phy = b; + let prevPhy = -1000; + let iteration = 0; + while (abs(phy - prevPhy) > 0.0000001) { + if (++iteration > 20) { + throw new Error('omercFwdInfinity'); + } + //S = log(tan(PI / 4 + phy / 2)); + S = + (1 / this.alpha) * (log(tan(PI / 4 + b / 2)) - this.K) + + this.e * log(tan(PI / 4 + asin(this.e * sin(phy)) / 2)); + prevPhy = phy; + phy = 2 * atan(exp(S)) - PI / 2; + } + p.x = lambda; + p.y = phy; + } +} diff --git a/src/proj4/projections/stere.ts b/src/proj4/projections/stere.ts new file mode 100644 index 00000000..95554ea4 --- /dev/null +++ b/src/proj4/projections/stere.ts @@ -0,0 +1,247 @@ +import { ProjectionBase } from '.'; +import { EPSLN, HALF_PI } from '../constants'; +import { adjustLon, msfnz, phi2z, sign, tsfnz } from '../common'; + +import type { VectorPoint } from 's2-tools/geometry'; +import type { ProjectionParams, ProjectionTransform } from '.'; + +const { abs, pow, sin, cos, sqrt, atan2, asin, tan, atan, PI } = Math; + +/** + * # Stereographic + * + * **Classification**: Azimuthal + * + * **Available forms**: Forward and inverse, spherical and ellipsoidal + * + * **Defined area**: Global + * + * **Alias**: stere + * + * **Domain**: 2D + * + * **Input type**: Geodetic coordinates + * + * **Output type**: Projected coordinates + * + * ## Projection String + * ``` + * +proj=stere +lat_0=90 +latTs=75 + * ``` + * + * Note: + * This projection method gives different results than the :ref:`sterea` + * method in the non-polar cases (i.e. the oblique and equatorial case). The later + * projection method is the one referenced by EPSG as "Oblique Stereographic". + * + * ## Required Parameters + * - None + * + * ## Optional Parameters + * - `+lat_0=`: Latitude of origin. + * - `+latTs=`: Latitude where scale is not distorted. + * - `+k_0=`: Scale factor. + * - `+lon_0=`: Central meridian. + * - `+ellps=`: Ellipsoid used. + * - `+R=`: Radius of the projection sphere. + * - `+x_0=`: False easting. + * - `+y_0=`: False northing. + * + * ![Stereographic](./images/stere.png) + */ +export class StereographicSouthPole extends ProjectionBase implements ProjectionTransform { + name = 'StereographicSouthPole'; + static names = [ + 'StereographicSouthPole', + 'stere', + 'Stereographic_South_Pole', + 'Polar Stereographic (variant B)', + 'Polar_Stereographic', + ]; + // StereographicSouthPole specific variables + coslat0: number; + sinlat0: number; + con = 0; + cons = 0; + ms1 = 0; + X0 = 0; + cosX0 = 0; + sinX0 = 0; + + /** + * Preps an StereographicSouthPole projection + * @param params - projection specific parameters + */ + constructor(params?: ProjectionParams) { + super(params); + + this.x0 = this.x0 ?? 0; + this.y0 = this.y0 ?? 0; + this.lat0 = this.lat0 ?? 0; + this.long0 = this.long0 ?? 0; + + this.coslat0 = cos(this.lat0); + this.sinlat0 = sin(this.lat0); + if (this.sphere) { + if (this.k0 === 1 && this.latTs !== undefined && abs(this.coslat0) <= EPSLN) { + this.k0 = 0.5 * (1 + sign(this.lat0) * sin(this.latTs)); + } + } else { + if (abs(this.coslat0) <= EPSLN) { + if (this.lat0 > 0) { + //North pole + //trace('stere:north pole'); + this.con = 1; + } else { + //South pole + //trace('stere:south pole'); + this.con = -1; + } + } + this.cons = sqrt(pow(1 + this.e, 1 + this.e) * pow(1 - this.e, 1 - this.e)); + if ( + this.k0 === 1 && + this.latTs !== undefined && + abs(this.coslat0) <= EPSLN && + abs(cos(this.latTs)) > EPSLN + ) { + // When k0 is 1 (default value) and latTs is a vaild number and lat0 is at a pole and latTs is not at a pole + // Recalculate k0 using formula 21-35 from p161 of Snyder, 1987 + this.k0 = + (0.5 * this.cons * msfnz(this.e, sin(this.latTs), cos(this.latTs))) / + tsfnz(this.e, this.con * this.latTs, this.con * sin(this.latTs)); + } + this.ms1 = msfnz(this.e, this.sinlat0, this.coslat0); + this.X0 = 2 * atan(this.#ssfn(this.lat0, this.sinlat0, this.e)) - HALF_PI; + this.cosX0 = cos(this.X0); + this.sinX0 = sin(this.X0); + } + } + + /** + * StereographicSouthPole forward equations--mapping lon-lat to x-y + * @param p - lon-lat WGS84 point + */ + forward(p: VectorPoint): void { + const { x: lon, y: lat } = p; + const sinlat = sin(lat); + const coslat = cos(lat); + let A, X, sinX, cosX, ts, rh; + const dlon = adjustLon(lon - this.long0); + if (abs(abs(lon - this.long0) - PI) <= EPSLN && abs(lat + this.lat0) <= EPSLN) { + //case of the origine point + throw new Error('cass:pj_init_stere: lon == long0 == 180'); + } + if (this.sphere) { + //trace('stere:sphere case'); + A = (2 * this.k0) / (1 + this.sinlat0 * sinlat + this.coslat0 * coslat * cos(dlon)); + p.x = this.a * A * coslat * sin(dlon) + this.x0; + p.y = this.a * A * (this.coslat0 * sinlat - this.sinlat0 * coslat * cos(dlon)) + this.y0; + return; + } else { + X = 2 * atan(this.#ssfn(lat, sinlat, this.e)) - HALF_PI; + cosX = cos(X); + sinX = sin(X); + if (abs(this.coslat0) <= EPSLN) { + ts = tsfnz(this.e, lat * this.con, this.con * sinlat); + rh = (2 * this.a * this.k0 * ts) / this.cons; + p.x = this.x0 + rh * sin(lon - this.long0); + p.y = this.y0 - this.con * rh * cos(lon - this.long0); + //trace(p.toString()); + return; + } else if (abs(this.sinlat0) < EPSLN) { + //Eq + //trace('stere:equateur'); + A = (2 * this.a * this.k0) / (1 + cosX * cos(dlon)); + p.y = A * sinX; + } else { + //other case + //trace('stere:normal case'); + A = + (2 * this.a * this.k0 * this.ms1) / + (this.cosX0 * (1 + this.sinX0 * sinX + this.cosX0 * cosX * cos(dlon))); + p.y = A * (this.cosX0 * sinX - this.sinX0 * cosX * cos(dlon)) + this.y0; + } + p.x = A * cosX * sin(dlon) + this.x0; + } + } + + /** + * StereographicSouthPole inverse equations--mapping x-y to lon-lat + * @param p - StereographicSouthPole point + */ + inverse(p: VectorPoint): void { + p.x -= this.x0; + p.y -= this.y0; + let lon, lat, ts, ce, Chi; + const rh = sqrt(p.x * p.x + p.y * p.y); + if (this.sphere) { + const c = 2 * atan(rh / (2 * this.a * this.k0)); + lon = this.long0; + lat = this.lat0; + if (rh <= EPSLN) { + p.x = lon; + p.y = lat; + return; + } + lat = asin(cos(c) * this.sinlat0 + (p.y * sin(c) * this.coslat0) / rh); + if (abs(this.coslat0) < EPSLN) { + if (this.lat0 > 0) { + lon = adjustLon(this.long0 + atan2(p.x, -1 * p.y)); + } else { + lon = adjustLon(this.long0 + atan2(p.x, p.y)); + } + } else { + lon = adjustLon( + this.long0 + + atan2(p.x * sin(c), rh * this.coslat0 * cos(c) - p.y * this.sinlat0 * sin(c)), + ); + } + p.x = lon; + p.y = lat; + return; + } else { + if (abs(this.coslat0) <= EPSLN) { + if (rh <= EPSLN) { + lat = this.lat0; + lon = this.long0; + p.x = lon; + p.y = lat; + //trace(p.toString()); + return; + } + p.x *= this.con; + p.y *= this.con; + ts = (rh * this.cons) / (2 * this.a * this.k0); + lat = this.con * phi2z(this.e, ts); + lon = this.con * adjustLon(this.con * this.long0 + atan2(p.x, -1 * p.y)); + } else { + ce = 2 * atan((rh * this.cosX0) / (2 * this.a * this.k0 * this.ms1)); + lon = this.long0; + if (rh <= EPSLN) { + Chi = this.X0; + } else { + Chi = asin(cos(ce) * this.sinX0 + (p.y * sin(ce) * this.cosX0) / rh); + lon = adjustLon( + this.long0 + + atan2(p.x * sin(ce), rh * this.cosX0 * cos(ce) - p.y * this.sinX0 * sin(ce)), + ); + } + lat = -1 * phi2z(this.e, tan(0.5 * (HALF_PI + Chi))); + } + } + p.x = lon; + p.y = lat; + } + + /** + * @param phit - phi + * @param sinphi - sin(phi) + * @param eccen - eccentricity + * @returns - tan(0.5*(HALF_PI+phit)) + */ + #ssfn(phit: number, sinphi: number, eccen: number): number { + sinphi *= eccen; + return tan(0.5 * (HALF_PI + phit)) * pow((1 - sinphi) / (1 + sinphi), 0.5 * eccen); + } +} diff --git a/src/proj4/projections/sterea.ts b/src/proj4/projections/sterea.ts new file mode 100644 index 00000000..27c49395 --- /dev/null +++ b/src/proj4/projections/sterea.ts @@ -0,0 +1,121 @@ +import { GaussKruger } from './gauss'; +import { adjustLon, hypot } from '../common'; + +import type { VectorPoint } from 's2-tools/geometry'; +import type { ProjectionParams, ProjectionTransform } from '.'; + +const { sin, cos, atan2, asin } = Math; + +/** + * # Oblique Stereographic Alternative + * + * **Classification**: Azimuthal + * + * **Available forms**: Forward and inverse, spherical and ellipsoidal + * + * **Defined area**: Global + * + * **Alias**: sterea + * + * **Domain**: 2D + * + * **Input type**: Geodetic coordinates + * + * **Output type**: Projected coordinates + * + * ## Projection String + * ``` + * +proj=sterea +lat_0=52.1561605555556 +lon_0=5.38763888888889 +k=0.9999079 +x_0=155000 +y_0=463000 +ellps=bessel + * ``` + * + * ## Note + * This projection method, referenced by EPSG as "Oblique Stereographic", is + * for example used for the Netherlands "Amersfoort / RD New" projected CRS. + * It gives different results than the :ref:`stere` method in the non-polar cases + * (i.e. the oblique and equatorial case). + * + * ## Required Parameters + * - None + * + * ## Optional Parameters + * - `+lat_0=`: Latitude of origin. + * - `+lon_0=`: Central meridian. + * - `+k=`: Scale factor. + * - `+x_0=`: False easting. + * - `+y_0=`: False northing. + * - `+ellps=`: Ellipsoid used. + * - `+R=`: Radius of the projection sphere. + * + * ![Oblique Stereographic Alternative](./images/sterea.png) + */ +export class StereographicNorthPole extends GaussKruger implements ProjectionTransform { + name = 'StereographicNorthPole'; + static names = [ + 'StereographicNorthPole', + 'Stereographic_North_Pole', + 'Oblique_Stereographic', + 'sterea', + 'Oblique Stereographic Alternative', + 'Double_Stereographic', + ]; + // StereographicNorthPole specific variables + R2: number; + sinc0: number; + cosc0: number; + + /** + * Preps an StereographicNorthPole projection + * @param params - projection specific parameters + */ + constructor(params?: ProjectionParams) { + super(params); + + if (!this.rc) throw new Error('rc must be defined'); + this.sinc0 = sin(this.phic0); + this.cosc0 = cos(this.phic0); + this.R2 = 2 * this.rc; + } + + /** + * StereographicNorthPole forward equations--mapping lon-lat to x-y + * @param p - lon-lat WGS84 point + */ + forward(p: VectorPoint): void { + p.x = adjustLon(p.x - this.long0); + super.forward(p); + const sinc = sin(p.y); + const cosc = cos(p.y); + const cosl = cos(p.x); + const k = (this.k0 * this.R2) / (1 + this.sinc0 * sinc + this.cosc0 * cosc * cosl); + p.x = k * cosc * sin(p.x); + p.y = k * (this.cosc0 * sinc - this.sinc0 * cosc * cosl); + p.x = this.a * p.x + this.x0; + p.y = this.a * p.y + this.y0; + } + + /** + * StereographicNorthPole inverse equations--mapping x-y to lon-lat + * @param p - StereographicNorthPole point + */ + inverse(p: VectorPoint): void { + let sinc, cosc, lon, lat, rho; + p.x = (p.x - this.x0) / this.a; + p.y = (p.y - this.y0) / this.a; + p.x /= this.k0; + p.y /= this.k0; + if ((rho = hypot(p.x, p.y))) { + const c = 2 * atan2(rho, this.R2); + sinc = sin(c); + cosc = cos(c); + lat = asin(cosc * this.sinc0 + (p.y * sinc * this.cosc0) / rho); + lon = atan2(p.x * sinc, rho * this.cosc0 * cosc - p.y * this.sinc0 * sinc); + } else { + lat = this.phic0; + lon = 0; + } + p.x = lon; + p.y = lat; + super.inverse(p); + p.x = adjustLon(p.x + this.long0); + } +} diff --git a/src/proj4/projections/template.ts b/src/proj4/projections/template.ts deleted file mode 100644 index 51f6c768..00000000 --- a/src/proj4/projections/template.ts +++ /dev/null @@ -1,39 +0,0 @@ -import { EPSLN } from '../constants'; -import { ProjectionBase } from '.'; -import { adjustLat, adjustLon } from '../common'; - -import type { VectorPoint } from 's2-tools/geometry'; -import type { ProjectionParams, ProjectionTransform } from '.'; - -const { abs, pow, sin, cos, sqrt, atan2, asin, log } = Math; - -/** - * - */ -export class XXXXXXXXXXXX extends ProjectionBase implements ProjectionTransform { - name = 'XXXXXXXXXXXX'; - names = [this.name, 'XXXXXXXXXXXX', 'XXXXXXXXXXXX']; - // XXXXXXXXXXXX specific variables - - /** - * Preps an XXXXXXXXXXXX projection - * @param params - projection specific parameters - */ - constructor(params?: ProjectionParams) { - super(params); - } - - /** - * XXXXXXXXXXXX forward equations--mapping lon-lat to x-y - * @param p - lon-lat WGS84 point - * @returns - XXXXXXXXXXXX point - */ - forward(p: VectorPoint): VectorPoint {} - - /** - * XXXXXXXXXXXX inverse equations--mapping x-y to lon-lat - * @param p - XXXXXXXXXXXX point - * @returns - lon-lat WGS84 point - */ - inverse(p: VectorPoint): VectorPoint {} -} diff --git a/src/proj4/projections/tmerc.ts b/src/proj4/projections/tmerc.ts new file mode 100644 index 00000000..6eef125c --- /dev/null +++ b/src/proj4/projections/tmerc.ts @@ -0,0 +1,247 @@ +// Heavily based on this tmerc projection implementation +// https://github.com/mbloch/mapshaper-proj/blob/master/src/projections/tmerc.js + +import { ProjectionBase } from '.'; +import { EPSLN, HALF_PI } from '../constants'; +import { adjustLon, pjEnfn, pjInvMlfn, pjMlfn, sign } from '../common'; + +import type { En } from '../common'; +import type { VectorPoint } from 's2-tools/geometry'; +import type { ProjectionParams, ProjectionTransform } from '.'; + +const { abs, pow, sin, cos, sqrt, atan2, asin, log, tan, acos, exp } = Math; + +/** + * # Transverse Mercator + * + * **Classification**: Transverse and oblique cylindrical + * + * **Available forms**: Forward and inverse, spherical and ellipsoidal + * + * **Defined area**: Global, with full accuracy within 3900 km of the central meridian + * + * **Alias**: tmerc + * + * **Domain**: 2D + * + * **Input type**: Geodetic coordinates + * + * **Output type**: Projected coordinates + * + * ## Projection String + * ``` + * +proj=tmerc + * ``` + * + * ## Required Parameters + * - `+lon_0`: Longitude of the central meridian. + * + * ## Optional Parameters + * - `+approx`: Use the faster Evenden-Snyder algorithm, less accurate beyond 3°. + * - `+algo`: Select algorithm from "auto", "evenden_snyder", or "poder_engsager". + * - `+lat_0`: Latitude of origin. + * - `+k_0`: Scale factor on the central meridian. + * - `+x_0`: False easting. + * - `+y_0`: False northing. + * + * ![Transverse Mercator](./images/tmerc.png) + */ +export class TransverseMercator extends ProjectionBase implements ProjectionTransform { + name = 'Transverse_Mercator'; + static names = ['Transverse_Mercator', 'tmerc', 'Transverse_Mercator', 'Transverse Mercator']; + // TransverseMercator specific variables + en: En = [0, 0, 0, 0, 0]; + ml0 = 0; + + /** + * Preps an TransverseMercator projection + * @param params - projection specific parameters + */ + constructor(params?: ProjectionParams) { + super(params); + + this.x0 = this.x0 !== undefined ? this.x0 : 0; + this.y0 = this.y0 !== undefined ? this.y0 : 0; + this.long0 = this.long0 !== undefined ? this.long0 : 0; + this.lat0 = this.lat0 !== undefined ? this.lat0 : 0; + + if (this.es) { + this.en = pjEnfn(this.es); + this.ml0 = pjMlfn(this.lat0, sin(this.lat0), cos(this.lat0), this.en); + } + } + + /** + * TransverseMercator forward equations--mapping lon-lat to x-y + * @param p - lon-lat WGS84 point + */ + forward(p: VectorPoint): void { + const { x: lon, y: lat } = p; + const delta_lon = adjustLon(lon - this.long0); + let con; + let x, y; + const sin_phi = sin(lat); + const cos_phi = cos(lat); + if (this.ep2 === Infinity) { + let b = cos_phi * sin(delta_lon); + if (abs(abs(b) - 1) < EPSLN) { + throw new Error( + 'Incorrect elliptical usage. Try using the +approx option in the proj string, or PROJECTION["Transverse_Mercator"] in the WKT.', + ); + } else { + x = 0.5 * this.a * this.k0 * log((1 + b) / (1 - b)) + this.x0; + y = (cos_phi * cos(delta_lon)) / sqrt(1 - pow(b, 2)); + b = abs(y); + if (b >= 1) { + if (b - 1 > EPSLN) { + throw new Error( + 'Incorrect elliptical usage. Try using the +approx option in the proj string, or PROJECTION["Transverse_Mercator"] in the WKT.', + ); + } else { + y = 0; + } + } else { + y = acos(y); + } + if (lat < 0) { + y = -y; + } + y = this.a * this.k0 * (y - this.lat0) + this.y0; + } + } else { + let al = cos_phi * delta_lon; + const als = pow(al, 2); + const c = this.ep2 * pow(cos_phi, 2); + const cs = pow(c, 2); + const tq = abs(cos_phi) > EPSLN ? tan(lat) : 0; + const t = pow(tq, 2); + const ts = pow(t, 2); + con = 1 - this.es * pow(sin_phi, 2); + al = al / sqrt(con); + const ml = pjMlfn(lat, sin_phi, cos_phi, this.en); + x = + this.a * + (this.k0 * + al * + (1 + + (als / 6) * + (1 - + t + + c + + (als / 20) * + (5 - + 18 * t + + ts + + 14 * c - + 58 * t * c + + (als / 42) * (61 + 179 * ts - ts * t - 479 * t))))) + + this.x0; + y = + this.a * + (this.k0 * + (ml - + this.ml0 + + ((sin_phi * delta_lon * al) / 2) * + (1 + + (als / 12) * + (5 - + t + + 9 * c + + 4 * cs + + (als / 30) * + (61 + + ts - + 58 * t + + 270 * c - + 330 * t * c + + (als / 56) * (1385 + 543 * ts - ts * t - 3111 * t)))))) + + this.y0; + } + p.x = x; + p.y = y; + } + + /** + * TransverseMercator inverse equations--mapping x-y to lon-lat + * @param p - TransverseMercator point + */ + inverse(p: VectorPoint): void { + let con, phi; + let lat, lon; + const x = (p.x - this.x0) * (1 / this.a); + const y = (p.y - this.y0) * (1 / this.a); + if (this.ep2 === Infinity) { + const f = exp(x / this.k0); + const g = 0.5 * (f - 1 / f); + const temp = this.lat0 + y / this.k0; + const h = cos(temp); + con = sqrt((1 - pow(h, 2)) / (1 + pow(g, 2))); + lat = asin(con); + if (y < 0) { + lat = -lat; + } + if (g === 0 && h === 0) { + lon = 0; + } else { + lon = adjustLon(atan2(g, h) + this.long0); + } + } else { + // ellipsoidal form + con = this.ml0 + y / this.k0; + phi = pjInvMlfn(con, this.es, this.en); + if (abs(phi) < HALF_PI) { + const sin_phi = sin(phi); + const cos_phi = cos(phi); + const tan_phi = abs(cos_phi) > EPSLN ? tan(phi) : 0; + const c = this.ep2 * pow(cos_phi, 2); + const cs = pow(c, 2); + const t = pow(tan_phi, 2); + const ts = pow(t, 2); + con = 1 - this.es * pow(sin_phi, 2); + const d = (x * sqrt(con)) / this.k0; + const ds = pow(d, 2); + con = con * tan_phi; + lat = + phi - + ((con * ds) / (1 - this.es)) * + 0.5 * + (1 - + (ds / 12) * + (5 + + 3 * t - + 9 * c * t + + c - + 4 * cs - + (ds / 30) * + (61 + + 90 * t - + 252 * c * t + + 45 * ts + + 46 * c - + (ds / 56) * (1385 + 3633 * t + 4095 * ts + 1574 * ts * t)))); + lon = adjustLon( + this.long0 + + (d * + (1 - + (ds / 6) * + (1 + + 2 * t + + c - + (ds / 20) * + (5 + + 28 * t + + 24 * ts + + 8 * c * t + + 6 * c - + (ds / 42) * (61 + 662 * t + 1320 * ts + 720 * ts * t))))) / + cos_phi, + ); + } else { + lat = HALF_PI * sign(y); + lon = 0; + } + } + p.x = lon; + p.y = lat; + } +} diff --git a/src/proj4/projections/tpers.ts b/src/proj4/projections/tpers.ts new file mode 100644 index 00000000..b340efa4 --- /dev/null +++ b/src/proj4/projections/tpers.ts @@ -0,0 +1,224 @@ +import { ProjectionBase } from '.'; +import { hypot } from '../common'; +import { D2R, EPSLN, HALF_PI } from '../constants'; + +import type { VectorPoint } from 's2-tools/geometry'; +import type { ProjectionParams, ProjectionTransform } from '.'; + +const { abs, sin, cos, sqrt, atan2, asin } = Math; + +/** The 4 possible modes of the TiltedPerspective projection */ +export enum MODE { + N_POLE = 0, + S_POLE = 1, + EQUIT = 2, + OBLIQ = 3, +} + +/** + * # Tilted Perspective + * + * Tilted Perspective is similar to `nsper` in that it simulates a + * perspective view from a height. Where `nsper` projects onto a plane tangent to + * the surface, Tilted Perspective orients the plane towards the direction of the + * view. Thus, extra parameters specifying azimuth and tilt are required beyond + * `nsper`'s `h`. As with `nsper`, `lat_0` & `lon_0` are + * also required for satellite position. + * + * **Classification**: Azimuthal + * + * **Available forms**: Forward and inverse, spherical projection + * + * **Defined area**: Global + * + * **Alias**: tpers + * + * **Domain**: 2D + * + * **Input type**: Geodetic coordinates + * + * **Output type**: Projected coordinates + * + * ## Projection String + * ``` + * +proj=tpers +h=5500000 +lat_0=40 + * ``` + * + * ## Required Parameters + * - `+h`: Height of the perspective view. + * - `+lat_0`: Latitude of the projection center. + * - `+lon_0`: Longitude of the projection center. + * + * ## Optional Parameters + * - `+azi=`: Bearing in degrees away from north. *Defaults to 0.0.* + * - `+tilt=`: Angle in degrees away from nadir. *Defaults to 0.0.* + * + * ![Tilted perspective](./images/tpers.png) + */ +export class TiltedPerspective extends ProjectionBase implements ProjectionTransform { + name = 'TiltedPerspective'; + static names = ['TiltedPerspective', 'Tilted_Perspective', 'tpers']; + // TiltedPerspective specific variables + mode: MODE; + declare h: number; // default is Karman line, no default in PROJ.7 + declare azi: number; // default is North + declare tilt: number; // default is Nadir + declare long0: number; // default is Greenwich, conversion to rad is automatic + declare lat0: number; // default is Equator, conversion to rad is automatic + declare sinph0: number; + declare cosph0: number; + pn1: number; + p: number; + rp: number; + h1: number; + pfact: number; + cg: number; + sg: number; + cw: number; + sw: number; + + /** + * Preps an TiltedPerspective projection + * @param params - projection specific parameters + */ + constructor(params?: ProjectionParams) { + super(params); + + if (this.h === undefined) this.h = 100_000; + if (this.azi === undefined) this.azi = 0; + if (this.long0 === undefined) this.long0 = 0; + if (this.lat0 === undefined) this.lat0 = 0; + if (this.sinph0 === undefined) this.sinph0 = 0; + if (this.cosph0 === undefined) this.cosph0 = 1; + if (this.tilt === undefined) this.tilt = 0; + // azi and tilt are in radians + this.azi *= D2R; + this.tilt *= D2R; + + if (abs(abs(this.lat0) - HALF_PI) < EPSLN) { + this.mode = this.lat0 < 0 ? MODE.S_POLE : MODE.N_POLE; + } else if (abs(this.lat0) < EPSLN) { + this.mode = MODE.EQUIT; + } else { + this.mode = MODE.OBLIQ; + this.sinph0 = sin(this.lat0); + this.cosph0 = cos(this.lat0); + } + + this.pn1 = this.h / this.a; // Normalize relative to the Earth's radius + + if (this.pn1 <= 0 || this.pn1 > 1e10) { + throw new Error('Invalid height'); + } + + this.p = 1 + this.pn1; + this.rp = 1 / this.p; + this.h1 = 1 / this.pn1; + this.pfact = (this.p + 1) * this.h1; + this.es = 0; + + const omega = this.tilt; + const gamma = this.azi; + this.cg = cos(gamma); + this.sg = sin(gamma); + this.cw = cos(omega); + this.sw = sin(omega); + } + + /** + * TiltedPerspective forward equations--mapping lon-lat to x-y + * @param p - lon-lat WGS84 point + */ + forward(p: VectorPoint): void { + p.x -= this.long0; + const sinphi = sin(p.y); + const cosphi = cos(p.y); + const coslam = cos(p.x); + let x, y; + switch (this.mode) { + case MODE.OBLIQ: + y = this.sinph0 * sinphi + this.cosph0 * cosphi * coslam; + break; + case MODE.EQUIT: + y = cosphi * coslam; + break; + case MODE.S_POLE: + y = -sinphi; + break; + case MODE.N_POLE: + y = sinphi; + break; + } + y = this.pn1 / (this.p - y); + x = y * cosphi * sin(p.x); + switch (this.mode) { + case MODE.OBLIQ: + y *= this.cosph0 * sinphi - this.sinph0 * cosphi * coslam; + break; + case MODE.EQUIT: + y *= sinphi; + break; + case MODE.N_POLE: + y *= -(cosphi * coslam); + break; + case MODE.S_POLE: + y *= cosphi * coslam; + break; + } + // Tilt + const yt = y * this.cg + x * this.sg; + const ba = 1 / (yt * this.sw * this.h1 + this.cw); + x = (x * this.cg - y * this.sg) * this.cw * ba; + y = yt * ba; + p.x = x * this.a; + p.y = y * this.a; + } + + /** + * TiltedPerspective inverse equations--mapping x-y to lon-lat + * @param p - TiltedPerspective point + */ + inverse(p: VectorPoint): void { + p.x /= this.a; + p.y /= this.a; + const r = { x: p.x, y: p.y }; + // Un-Tilt + const yt = 1 / (this.pn1 - p.y * this.sw); + const bm = this.pn1 * p.x * yt; + const bq = this.pn1 * p.y * this.cw * yt; + p.x = bm * this.cg + bq * this.sg; + p.y = bq * this.cg - bm * this.sg; + const rh = hypot(p.x, p.y); + if (abs(rh) < EPSLN) { + r.x = 0; + r.y = p.y; + } else { + let sinz; + sinz = 1 - rh * rh * this.pfact; + sinz = (this.p - sqrt(sinz)) / (this.pn1 / rh + rh / this.pn1); + const cosz = sqrt(1 - sinz * sinz); + switch (this.mode) { + case MODE.OBLIQ: + r.y = asin(cosz * this.sinph0 + (p.y * sinz * this.cosph0) / rh); + p.y = (cosz - this.sinph0 * sin(r.y)) * rh; + p.x *= sinz * this.cosph0; + break; + case MODE.EQUIT: + r.y = asin((p.y * sinz) / rh); + p.y = cosz * rh; + p.x *= sinz; + break; + case MODE.N_POLE: + r.y = asin(cosz); + p.y = -p.y; + break; + case MODE.S_POLE: + r.y = -asin(cosz); + break; + } + r.x = atan2(p.x, p.y); + } + p.x = r.x + this.long0; + p.y = r.y; + } +} diff --git a/src/proj4/projections/utm.ts b/src/proj4/projections/utm.ts new file mode 100644 index 00000000..7e514046 --- /dev/null +++ b/src/proj4/projections/utm.ts @@ -0,0 +1,93 @@ +import { D2R } from '../constants'; +import { ExtendedTransverseMercator } from './etmerc'; +import { adjustZone } from '../common'; + +import type { ProjectionParams, ProjectionTransform } from '.'; + +/** + * # Universal Transverse Mercator (UTM) + * + * **Classification**: Transverse cylindrical, conformal + * + * **Available forms**: Forward and inverse, ellipsoidal only + * + * **Defined area**: Within the used zone, but transformations of coordinates in adjacent zones can be accurate + * + * **Alias**: utm + * + * **Domain**: 2D + * + * **Input type**: Geodetic coordinates + * + * **Output type**: Projected coordinates + * + * ## Projection String + * ``` + * +proj=utm + * ``` + * + * ## Required Parameters + * - `+zone=`: Select which UTM zone to use. Can be a value between 1-60. + * + * ## Optional Parameters + * - `+south`: Add this flag when using the UTM on the southern hemisphere. + * - `+approx`: Use a faster, less accurate algorithm for the Transverse Mercator. (added in PROJ 6.0.0) + * - `+algo=auto/evenden_snyder/poder_engsager`: Selects the algorithm to use. Defaults to `poder_engsager`. (added in PROJ 7.1) + * - `+ellps=` + * + * ## Usage Examples + * + * Convert geodetic coordinates to UTM Zone 32 on the northern hemisphere: + * ``` + * $ echo 12 56 | proj +proj=utm +zone=32 + * 687071.44 6210141.33 + * ``` + * + * Convert geodetic coordinates to UTM Zone 59 on the southern hemisphere: + * ``` + * $ echo 174 -44 | proj +proj=utm +zone=59 +south + * 740526.32 5123750.87 + * ``` + * + * Show the relationship of UTM to TM: + * ``` + * $ echo 121 24 | proj +proj=utm +lon_0=123 | proj -I +proj=tmerc +lon_0=123 +x_0=500000 +k=0.9996 + * 121dE 24dN + * ``` + * + * ## Further Reading + * - [Wikipedia](https://en.wikipedia.org/wiki/Universal_Transverse_Mercator_coordinate_system) + * + * ![Universal Transverse Mercator (UTM) zones](../../../images/utm_zones.png) + */ +export class UniversalTransverseMercator + extends ExtendedTransverseMercator + implements ProjectionTransform +{ + name = 'UniversalTransverseMercator'; + static names = [ + 'UniversalTransverseMercator', + 'Universal Transverse Mercator', + 'Universal Transverse Mercator System', + 'utm', + ]; + // UniversalTransverseMercator specific variables + + /** + * Preps an UniversalTransverseMercator projection + * @param params - projection specific parameters + */ + constructor(params?: ProjectionParams) { + super(params, (etmerc: ExtendedTransverseMercator) => { + const zone = adjustZone(etmerc.zone, etmerc.long0); + if (zone === undefined) { + throw new Error('unknown utm zone'); + } + etmerc.lat0 = 0; + etmerc.long0 = (6 * Math.abs(zone) - 183) * D2R; + etmerc.x0 = 500000; + etmerc.y0 = etmerc.utmSouth ? 10000000 : 0; + etmerc.k0 = 0.9996; + }); + } +} diff --git a/src/proj4/projections/vandg.ts b/src/proj4/projections/vandg.ts new file mode 100644 index 00000000..f23d6400 --- /dev/null +++ b/src/proj4/projections/vandg.ts @@ -0,0 +1,155 @@ +import { ProjectionBase } from '.'; +import { EPSLN, HALF_PI } from '../constants'; +import { adjustLon, asinz } from '../common'; + +import type { VectorPoint } from 's2-tools/geometry'; +import type { ProjectionParams, ProjectionTransform } from '.'; + +const { abs, PI, sin, cos, sqrt, tan, acos } = Math; + +/** + * # van der Grinten (I) + * + * **Classification**: Miscellaneous + * + * **Available forms**: Forward and inverse, spherical projection + * + * **Defined area**: Global + * + * **Alias**: vandg + * + * **Domain**: 2D + * + * **Input type**: Geodetic coordinates + * + * **Output type**: Projected coordinates + * + * ## Projection String + * ``` + * +proj=vandg + * ``` + * + * ## Parameters + * + * All parameters are optional. + * + * - `+lon_0=`: Central meridian. + * - `+R=`: Radius of the sphere. + * - `+x_0=`: False easting. + * - `+y_0=`: False northing. + * + * ![van der Grinten (I)](./images/vandg.png) + */ +export class VanDerGrinten extends ProjectionBase implements ProjectionTransform { + name = 'VanDerGrinten'; + static names = ['VanDerGrinten', 'Van_der_Grinten_I', 'vandg']; + // VanDerGrinten specific variables + R: number; + + /** + * Preps an VanDerGrinten projection + * @param params - projection specific parameters + */ + constructor(params?: ProjectionParams) { + super(params); + + // R = 6370997 -> Radius of earth + this.R = this.a; + } + + /** + * VanDerGrinten forward equations--mapping lon-lat to x-y + * @param p - lon-lat WGS84 point + */ + forward(p: VectorPoint): void { + const { x: lon, y: lat } = p; + const dlon = adjustLon(lon - this.long0); + let x, y; + if (abs(lat) <= EPSLN) { + x = this.x0 + this.R * dlon; + y = this.y0; + } + const theta = asinz(2 * abs(lat / PI)); + if (abs(dlon) <= EPSLN || abs(abs(lat) - HALF_PI) <= EPSLN) { + x = this.x0; + if (lat >= 0) { + y = this.y0 + PI * this.R * tan(0.5 * theta); + } else { + y = this.y0 + PI * this.R * -tan(0.5 * theta); + } + // return(OK); + } + const al = 0.5 * abs(PI / dlon - dlon / PI); + const asq = al * al; + const sinth = sin(theta); + const costh = cos(theta); + const g = costh / (sinth + costh - 1); + const gsq = g * g; + const m = g * (2 / sinth - 1); + const msq = m * m; + let con = + (PI * + this.R * + (al * (g - msq) + sqrt(asq * (g - msq) * (g - msq) - (msq + asq) * (gsq - msq)))) / + (msq + asq); + if (dlon < 0) { + con = -con; + } + x = this.x0 + con; + //con = abs(con / (PI * this.R)); + const q = asq + g; + con = (PI * this.R * (m * q - al * sqrt((msq + asq) * (asq + 1) - q * q))) / (msq + asq); + if (lat >= 0) { + //y = this.y0 + PI * this.R * sqrt(1 - con * con - 2 * al * con); + y = this.y0 + con; + } else { + //y = this.y0 - PI * this.R * sqrt(1 - con * con - 2 * al * con); + y = this.y0 - con; + } + p.x = x; + p.y = y; + } + + /** + * VanDerGrinten inverse equations--mapping x-y to lon-lat + * @param p - VanDerGrinten point + */ + inverse(p: VectorPoint): void { + let lon, lat; + p.x -= this.x0; + p.y -= this.y0; + let con = PI * this.R; + const xx = p.x / con; + const yy = p.y / con; + const xys = xx * xx + yy * yy; + const c1 = -abs(yy) * (1 + xys); + const c2 = c1 - 2 * yy * yy + xx * xx; + const c3 = -2 * c1 + 1 + 2 * yy * yy + xys * xys; + const d = (yy * yy) / c3 + ((2 * c2 * c2 * c2) / c3 / c3 / c3 - (9 * c1 * c2) / c3 / c3) / 27; + const a1 = (c1 - (c2 * c2) / 3 / c3) / c3; + const m1 = 2 * sqrt(-a1 / 3); + con = (3 * d) / a1 / m1; + if (abs(con) > 1) { + if (con >= 0) { + con = 1; + } else { + con = -1; + } + } + const th1 = acos(con) / 3; + if (p.y >= 0) { + lat = (-m1 * cos(th1 + PI / 3) - c2 / 3 / c3) * PI; + } else { + lat = -(-m1 * cos(th1 + PI / 3) - c2 / 3 / c3) * PI; + } + if (abs(xx) < EPSLN) { + lon = this.long0; + } else { + lon = adjustLon( + this.long0 + (PI * (xys - 1 + sqrt(1 + 2 * (xx * xx - yy * yy) + xys * xys))) / 2 / xx, + ); + } + p.x = lon; + p.y = lat; + } +} diff --git a/src/proj4/projectionsBackup/eqdc.js b/src/proj4/projectionsBackup/eqdc.js deleted file mode 100644 index fcf03e1d..00000000 --- a/src/proj4/projectionsBackup/eqdc.js +++ /dev/null @@ -1,117 +0,0 @@ -import e0fn from '../common/e0fn'; -import e1fn from '../common/e1fn'; -import e2fn from '../common/e2fn'; -import e3fn from '../common/e3fn'; -import msfnz from '../common/msfnz'; -import mlfn from '../common/mlfn'; -import adjust_lon from '../common/adjust_lon'; -import adjust_lat from '../common/adjust_lat'; -import imlfn from '../common/imlfn'; -import {EPSLN} from '../constants/values'; - -export function init() { - - /* Place parameters in static storage for common use - -------------------------------------------------*/ - // Standard Parallels cannot be equal and on opposite sides of the equator - if (Math.abs(this.lat1 + this.lat2) < EPSLN) { - return; - } - this.lat2 = this.lat2 || this.lat1; - this.temp = this.b / this.a; - this.es = 1 - Math.pow(this.temp, 2); - this.e = Math.sqrt(this.es); - this.e0 = e0fn(this.es); - this.e1 = e1fn(this.es); - this.e2 = e2fn(this.es); - this.e3 = e3fn(this.es); - - this.sinphi = Math.sin(this.lat1); - this.cosphi = Math.cos(this.lat1); - - this.ms1 = msfnz(this.e, this.sinphi, this.cosphi); - this.ml1 = mlfn(this.e0, this.e1, this.e2, this.e3, this.lat1); - - if (Math.abs(this.lat1 - this.lat2) < EPSLN) { - this.ns = this.sinphi; - } - else { - this.sinphi = Math.sin(this.lat2); - this.cosphi = Math.cos(this.lat2); - this.ms2 = msfnz(this.e, this.sinphi, this.cosphi); - this.ml2 = mlfn(this.e0, this.e1, this.e2, this.e3, this.lat2); - this.ns = (this.ms1 - this.ms2) / (this.ml2 - this.ml1); - } - this.g = this.ml1 + this.ms1 / this.ns; - this.ml0 = mlfn(this.e0, this.e1, this.e2, this.e3, this.lat0); - this.rh = this.a * (this.g - this.ml0); -} - -/* Equidistant Conic forward equations--mapping lat,long to x,y - -----------------------------------------------------------*/ -export function forward(p) { - var lon = p.x; - var lat = p.y; - var rh1; - - /* Forward equations - -----------------*/ - if (this.sphere) { - rh1 = this.a * (this.g - lat); - } - else { - var ml = mlfn(this.e0, this.e1, this.e2, this.e3, lat); - rh1 = this.a * (this.g - ml); - } - var theta = this.ns * adjust_lon(lon - this.long0); - var x = this.x0 + rh1 * Math.sin(theta); - var y = this.y0 + this.rh - rh1 * Math.cos(theta); - p.x = x; - p.y = y; - return p; -} - -/* Inverse equations - -----------------*/ -export function inverse(p) { - p.x -= this.x0; - p.y = this.rh - p.y + this.y0; - var con, rh1, lat, lon; - if (this.ns >= 0) { - rh1 = Math.sqrt(p.x * p.x + p.y * p.y); - con = 1; - } - else { - rh1 = -Math.sqrt(p.x * p.x + p.y * p.y); - con = -1; - } - var theta = 0; - if (rh1 !== 0) { - theta = Math.atan2(con * p.x, con * p.y); - } - - if (this.sphere) { - lon = adjust_lon(this.long0 + theta / this.ns); - lat = adjust_lat(this.g - rh1 / this.a); - p.x = lon; - p.y = lat; - return p; - } - else { - var ml = this.g - rh1 / this.a; - lat = imlfn(ml, this.e0, this.e1, this.e2, this.e3); - lon = adjust_lon(this.long0 + theta / this.ns); - p.x = lon; - p.y = lat; - return p; - } - -} - -export var names = ["Equidistant_Conic", "eqdc"]; -export default { - init: init, - forward: forward, - inverse: inverse, - names: names -}; diff --git a/src/proj4/projectionsBackup/eqearth.js b/src/proj4/projectionsBackup/eqearth.js deleted file mode 100644 index e407e1b1..00000000 --- a/src/proj4/projectionsBackup/eqearth.js +++ /dev/null @@ -1,93 +0,0 @@ -/** - * Copyright 2018 Bernie Jenny, Monash University, Melbourne, Australia. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - * - * Equal Earth is a projection inspired by the Robinson projection, but unlike - * the Robinson projection retains the relative size of areas. The projection - * was designed in 2018 by Bojan Savric, Tom Patterson and Bernhard Jenny. - * - * Publication: - * Bojan Savric, Tom Patterson & Bernhard Jenny (2018). The Equal Earth map - * projection, International Journal of Geographical Information Science, - * DOI: 10.1080/13658816.2018.1504949 - * - * Code released August 2018 - * Ported to JavaScript and adapted for mapshaper-proj by Matthew Bloch August 2018 - * Modified for proj4js by Andreas Hocevar by Andreas Hocevar March 2024 - */ - -import adjust_lon from "../common/adjust_lon"; - -var A1 = 1.340264, - A2 = -0.081106, - A3 = 0.000893, - A4 = 0.003796, - M = Math.sqrt(3) / 2.0; - -export function init() { - this.es = 0; - this.long0 = this.long0 !== undefined ? this.long0 : 0; -} - -export function forward(p) { - var lam = adjust_lon(p.x - this.long0); - var phi = p.y; - var paramLat = Math.asin(M * Math.sin(phi)), - paramLatSq = paramLat * paramLat, - paramLatPow6 = paramLatSq * paramLatSq * paramLatSq; - p.x = lam * Math.cos(paramLat) / - (M * (A1 + 3 * A2 * paramLatSq + paramLatPow6 * (7 * A3 + 9 * A4 * paramLatSq))); - p.y = paramLat * (A1 + A2 * paramLatSq + paramLatPow6 * (A3 + A4 * paramLatSq)); - - p.x = this.a * p.x + this.x0; - p.y = this.a * p.y + this.y0; - return p; -} - -export function inverse(p) { - p.x = (p.x - this.x0) / this.a; - p.y = (p.y - this.y0) / this.a; - - var EPS = 1e-9, - NITER = 12, - paramLat = p.y, - paramLatSq, paramLatPow6, fy, fpy, dlat, i; - - for (i = 0; i < NITER; ++i) { - paramLatSq = paramLat * paramLat; - paramLatPow6 = paramLatSq * paramLatSq * paramLatSq; - fy = paramLat * (A1 + A2 * paramLatSq + paramLatPow6 * (A3 + A4 * paramLatSq)) - p.y; - fpy = A1 + 3 * A2 * paramLatSq + paramLatPow6 * (7 * A3 + 9 * A4 * paramLatSq); - paramLat -= dlat = fy / fpy; - if (Math.abs(dlat) < EPS) { - break; - } - } - paramLatSq = paramLat * paramLat; - paramLatPow6 = paramLatSq * paramLatSq * paramLatSq; - p.x = M * p.x * (A1 + 3 * A2 * paramLatSq + paramLatPow6 * (7 * A3 + 9 * A4 * paramLatSq)) / - Math.cos(paramLat); - p.y = Math.asin(Math.sin(paramLat) / M); - - p.x = adjust_lon(p.x + this.long0); - return p; -} - -export var names = ["eqearth", "Equal Earth", "Equal_Earth"]; -export default { - init: init, - forward: forward, - inverse: inverse, - names: names -}; \ No newline at end of file diff --git a/src/proj4/projectionsBackup/equi.js b/src/proj4/projectionsBackup/equi.js deleted file mode 100644 index f91a522a..00000000 --- a/src/proj4/projectionsBackup/equi.js +++ /dev/null @@ -1,48 +0,0 @@ -import adjust_lon from '../common/adjust_lon'; - -export function init() { - this.x0 = this.x0 || 0; - this.y0 = this.y0 || 0; - this.lat0 = this.lat0 || 0; - this.long0 = this.long0 || 0; - ///this.t2; -} - -/* Equirectangular forward equations--mapping lat,long to x,y - ---------------------------------------------------------*/ -export function forward(p) { - - var lon = p.x; - var lat = p.y; - - var dlon = adjust_lon(lon - this.long0); - var x = this.x0 + this.a * dlon * Math.cos(this.lat0); - var y = this.y0 + this.a * lat; - - this.t1 = x; - this.t2 = Math.cos(this.lat0); - p.x = x; - p.y = y; - return p; -} - -/* Equirectangular inverse equations--mapping x,y to lat/long - ---------------------------------------------------------*/ -export function inverse(p) { - - p.x -= this.x0; - p.y -= this.y0; - var lat = p.y / this.a; - - var lon = adjust_lon(this.long0 + p.x / (this.a * Math.cos(this.lat0))); - p.x = lon; - p.y = lat; -} - -export var names = ["equi"]; -export default { - init: init, - forward: forward, - inverse: inverse, - names: names -}; diff --git a/src/proj4/projectionsBackup/etmerc.js b/src/proj4/projectionsBackup/etmerc.js deleted file mode 100644 index 0d32f5bc..00000000 --- a/src/proj4/projectionsBackup/etmerc.js +++ /dev/null @@ -1,172 +0,0 @@ -// Heavily based on this etmerc projection implementation -// https://github.com/mbloch/mapshaper-proj/blob/master/src/projections/etmerc.js - -import tmerc from '../projections/tmerc'; -import sinh from '../common/sinh'; -import hypot from '../common/hypot'; -import asinhy from '../common/asinhy'; -import gatg from '../common/gatg'; -import clens from '../common/clens'; -import clens_cmplx from '../common/clens_cmplx'; -import adjust_lon from '../common/adjust_lon'; - -export function init() { - if (!this.approx && (isNaN(this.es) || this.es <= 0)) { - throw new Error('Incorrect elliptical usage. Try using the +approx option in the proj string, or PROJECTION["Fast_Transverse_Mercator"] in the WKT.'); - } - if (this.approx) { - // When '+approx' is set, use tmerc instead - tmerc.init.apply(this); - this.forward = tmerc.forward; - this.inverse = tmerc.inverse; - } - - this.x0 = this.x0 !== undefined ? this.x0 : 0; - this.y0 = this.y0 !== undefined ? this.y0 : 0; - this.long0 = this.long0 !== undefined ? this.long0 : 0; - this.lat0 = this.lat0 !== undefined ? this.lat0 : 0; - - this.cgb = []; - this.cbg = []; - this.utg = []; - this.gtu = []; - - var f = this.es / (1 + Math.sqrt(1 - this.es)); - var n = f / (2 - f); - var np = n; - - this.cgb[0] = n * (2 + n * (-2 / 3 + n * (-2 + n * (116 / 45 + n * (26 / 45 + n * (-2854 / 675 )))))); - this.cbg[0] = n * (-2 + n * ( 2 / 3 + n * ( 4 / 3 + n * (-82 / 45 + n * (32 / 45 + n * (4642 / 4725)))))); - - np = np * n; - this.cgb[1] = np * (7 / 3 + n * (-8 / 5 + n * (-227 / 45 + n * (2704 / 315 + n * (2323 / 945))))); - this.cbg[1] = np * (5 / 3 + n * (-16 / 15 + n * ( -13 / 9 + n * (904 / 315 + n * (-1522 / 945))))); - - np = np * n; - this.cgb[2] = np * (56 / 15 + n * (-136 / 35 + n * (-1262 / 105 + n * (73814 / 2835)))); - this.cbg[2] = np * (-26 / 15 + n * (34 / 21 + n * (8 / 5 + n * (-12686 / 2835)))); - - np = np * n; - this.cgb[3] = np * (4279 / 630 + n * (-332 / 35 + n * (-399572 / 14175))); - this.cbg[3] = np * (1237 / 630 + n * (-12 / 5 + n * ( -24832 / 14175))); - - np = np * n; - this.cgb[4] = np * (4174 / 315 + n * (-144838 / 6237)); - this.cbg[4] = np * (-734 / 315 + n * (109598 / 31185)); - - np = np * n; - this.cgb[5] = np * (601676 / 22275); - this.cbg[5] = np * (444337 / 155925); - - np = Math.pow(n, 2); - this.Qn = this.k0 / (1 + n) * (1 + np * (1 / 4 + np * (1 / 64 + np / 256))); - - this.utg[0] = n * (-0.5 + n * ( 2 / 3 + n * (-37 / 96 + n * ( 1 / 360 + n * (81 / 512 + n * (-96199 / 604800)))))); - this.gtu[0] = n * (0.5 + n * (-2 / 3 + n * (5 / 16 + n * (41 / 180 + n * (-127 / 288 + n * (7891 / 37800)))))); - - this.utg[1] = np * (-1 / 48 + n * (-1 / 15 + n * (437 / 1440 + n * (-46 / 105 + n * (1118711 / 3870720))))); - this.gtu[1] = np * (13 / 48 + n * (-3 / 5 + n * (557 / 1440 + n * (281 / 630 + n * (-1983433 / 1935360))))); - - np = np * n; - this.utg[2] = np * (-17 / 480 + n * (37 / 840 + n * (209 / 4480 + n * (-5569 / 90720 )))); - this.gtu[2] = np * (61 / 240 + n * (-103 / 140 + n * (15061 / 26880 + n * (167603 / 181440)))); - - np = np * n; - this.utg[3] = np * (-4397 / 161280 + n * (11 / 504 + n * (830251 / 7257600))); - this.gtu[3] = np * (49561 / 161280 + n * (-179 / 168 + n * (6601661 / 7257600))); - - np = np * n; - this.utg[4] = np * (-4583 / 161280 + n * (108847 / 3991680)); - this.gtu[4] = np * (34729 / 80640 + n * (-3418889 / 1995840)); - - np = np * n; - this.utg[5] = np * (-20648693 / 638668800); - this.gtu[5] = np * (212378941 / 319334400); - - var Z = gatg(this.cbg, this.lat0); - this.Zb = -this.Qn * (Z + clens(this.gtu, 2 * Z)); -} - -export function forward(p) { - var Ce = adjust_lon(p.x - this.long0); - var Cn = p.y; - - Cn = gatg(this.cbg, Cn); - var sin_Cn = Math.sin(Cn); - var cos_Cn = Math.cos(Cn); - var sin_Ce = Math.sin(Ce); - var cos_Ce = Math.cos(Ce); - - Cn = Math.atan2(sin_Cn, cos_Ce * cos_Cn); - Ce = Math.atan2(sin_Ce * cos_Cn, hypot(sin_Cn, cos_Cn * cos_Ce)); - Ce = asinhy(Math.tan(Ce)); - - var tmp = clens_cmplx(this.gtu, 2 * Cn, 2 * Ce); - - Cn = Cn + tmp[0]; - Ce = Ce + tmp[1]; - - var x; - var y; - - if (Math.abs(Ce) <= 2.623395162778) { - x = this.a * (this.Qn * Ce) + this.x0; - y = this.a * (this.Qn * Cn + this.Zb) + this.y0; - } - else { - x = Infinity; - y = Infinity; - } - - p.x = x; - p.y = y; - - return p; -} - -export function inverse(p) { - var Ce = (p.x - this.x0) * (1 / this.a); - var Cn = (p.y - this.y0) * (1 / this.a); - - Cn = (Cn - this.Zb) / this.Qn; - Ce = Ce / this.Qn; - - var lon; - var lat; - - if (Math.abs(Ce) <= 2.623395162778) { - var tmp = clens_cmplx(this.utg, 2 * Cn, 2 * Ce); - - Cn = Cn + tmp[0]; - Ce = Ce + tmp[1]; - Ce = Math.atan(sinh(Ce)); - - var sin_Cn = Math.sin(Cn); - var cos_Cn = Math.cos(Cn); - var sin_Ce = Math.sin(Ce); - var cos_Ce = Math.cos(Ce); - - Cn = Math.atan2(sin_Cn * cos_Ce, hypot(sin_Ce, cos_Ce * cos_Cn)); - Ce = Math.atan2(sin_Ce, cos_Ce * cos_Cn); - - lon = adjust_lon(Ce + this.long0); - lat = gatg(this.cgb, Cn); - } - else { - lon = Infinity; - lat = Infinity; - } - - p.x = lon; - p.y = lat; - - return p; -} - -export var names = ["Extended_Transverse_Mercator", "Extended Transverse Mercator", "etmerc", "Transverse_Mercator", "Transverse Mercator", "Gauss Kruger", "Gauss_Kruger", "tmerc"]; -export default { - init: init, - forward: forward, - inverse: inverse, - names: names -}; diff --git a/src/proj4/projectionsBackup/gauss.js b/src/proj4/projectionsBackup/gauss.js deleted file mode 100644 index 0d9fef55..00000000 --- a/src/proj4/projectionsBackup/gauss.js +++ /dev/null @@ -1,52 +0,0 @@ -import srat from '../common/srat'; -var MAX_ITER = 20; -import {HALF_PI, QUART_PI} from '../constants/values'; - -export function init() { - var sphi = Math.sin(this.lat0); - var cphi = Math.cos(this.lat0); - cphi *= cphi; - this.rc = Math.sqrt(1 - this.es) / (1 - this.es * sphi * sphi); - this.C = Math.sqrt(1 + this.es * cphi * cphi / (1 - this.es)); - this.phic0 = Math.asin(sphi / this.C); - this.ratexp = 0.5 * this.C * this.e; - this.K = Math.tan(0.5 * this.phic0 + QUART_PI) / (Math.pow(Math.tan(0.5 * this.lat0 + QUART_PI), this.C) * srat(this.e * sphi, this.ratexp)); -} - -export function forward(p) { - var lon = p.x; - var lat = p.y; - - p.y = 2 * Math.atan(this.K * Math.pow(Math.tan(0.5 * lat + QUART_PI), this.C) * srat(this.e * Math.sin(lat), this.ratexp)) - HALF_PI; - p.x = this.C * lon; - return p; -} - -export function inverse(p) { - var DEL_TOL = 1e-14; - var lon = p.x / this.C; - var lat = p.y; - var num = Math.pow(Math.tan(0.5 * lat + QUART_PI) / this.K, 1 / this.C); - for (var i = MAX_ITER; i > 0; --i) { - lat = 2 * Math.atan(num * srat(this.e * Math.sin(p.y), - 0.5 * this.e)) - HALF_PI; - if (Math.abs(lat - p.y) < DEL_TOL) { - break; - } - p.y = lat; - } - /* convergence failed */ - if (!i) { - return null; - } - p.x = lon; - p.y = lat; - return p; -} - -export var names = ["gauss"]; -export default { - init: init, - forward: forward, - inverse: inverse, - names: names -}; diff --git a/src/proj4/projectionsBackup/geocent.js b/src/proj4/projectionsBackup/geocent.js deleted file mode 100644 index d501f0af..00000000 --- a/src/proj4/projectionsBackup/geocent.js +++ /dev/null @@ -1,27 +0,0 @@ -import { - geodeticToGeocentric, - geocentricToGeodetic -} from '../datumUtils'; - -export function init() { - this.name = 'geocent'; - -} - -export function forward(p) { - var point = geodeticToGeocentric(p, this.es, this.a); - return point; -} - -export function inverse(p) { - var point = geocentricToGeodetic(p, this.es, this.a, this.b); - return point; -} - -export var names = ["Geocentric", 'geocentric', "geocent", "Geocent"]; -export default { - init: init, - forward: forward, - inverse: inverse, - names: names -}; diff --git a/src/proj4/projectionsBackup/geos.js b/src/proj4/projectionsBackup/geos.js deleted file mode 100644 index 9fa70b9d..00000000 --- a/src/proj4/projectionsBackup/geos.js +++ /dev/null @@ -1,158 +0,0 @@ -import hypot from '../common/hypot'; - -export function init() { - this.flip_axis = (this.sweep === 'x' ? 1 : 0); - this.h = Number(this.h); - this.radius_g_1 = this.h / this.a; - - if (this.radius_g_1 <= 0 || this.radius_g_1 > 1e10) { - throw new Error(); - } - - this.radius_g = 1.0 + this.radius_g_1; - this.C = this.radius_g * this.radius_g - 1.0; - - if (this.es !== 0.0) { - var one_es = 1.0 - this.es; - var rone_es = 1 / one_es; - - this.radius_p = Math.sqrt(one_es); - this.radius_p2 = one_es; - this.radius_p_inv2 = rone_es; - - this.shape = 'ellipse'; // Use as a condition in the forward and inverse functions. - } else { - this.radius_p = 1.0; - this.radius_p2 = 1.0; - this.radius_p_inv2 = 1.0; - - this.shape = 'sphere'; // Use as a condition in the forward and inverse functions. - } - - if (!this.title) { - this.title = "Geostationary Satellite View"; - } -} - -function forward(p) { - var lon = p.x; - var lat = p.y; - var tmp, v_x, v_y, v_z; - lon = lon - this.long0; - - if (this.shape === 'ellipse') { - lat = Math.atan(this.radius_p2 * Math.tan(lat)); - var r = this.radius_p / hypot(this.radius_p * Math.cos(lat), Math.sin(lat)); - - v_x = r * Math.cos(lon) * Math.cos(lat); - v_y = r * Math.sin(lon) * Math.cos(lat); - v_z = r * Math.sin(lat); - - if (((this.radius_g - v_x) * v_x - v_y * v_y - v_z * v_z * this.radius_p_inv2) < 0.0) { - p.x = Number.NaN; - p.y = Number.NaN; - return p; - } - - tmp = this.radius_g - v_x; - if (this.flip_axis) { - p.x = this.radius_g_1 * Math.atan(v_y / hypot(v_z, tmp)); - p.y = this.radius_g_1 * Math.atan(v_z / tmp); - } else { - p.x = this.radius_g_1 * Math.atan(v_y / tmp); - p.y = this.radius_g_1 * Math.atan(v_z / hypot(v_y, tmp)); - } - } else if (this.shape === 'sphere') { - tmp = Math.cos(lat); - v_x = Math.cos(lon) * tmp; - v_y = Math.sin(lon) * tmp; - v_z = Math.sin(lat); - tmp = this.radius_g - v_x; - - if (this.flip_axis) { - p.x = this.radius_g_1 * Math.atan(v_y / hypot(v_z, tmp)); - p.y = this.radius_g_1 * Math.atan(v_z / tmp); - } else { - p.x = this.radius_g_1 * Math.atan(v_y / tmp); - p.y = this.radius_g_1 * Math.atan(v_z / hypot(v_y, tmp)); - } - } - p.x = p.x * this.a; - p.y = p.y * this.a; - return p; -} - -function inverse(p) { - var v_x = -1.0; - var v_y = 0.0; - var v_z = 0.0; - var a, b, det, k; - - p.x = p.x / this.a; - p.y = p.y / this.a; - - if (this.shape === 'ellipse') { - if (this.flip_axis) { - v_z = Math.tan(p.y / this.radius_g_1); - v_y = Math.tan(p.x / this.radius_g_1) * hypot(1.0, v_z); - } else { - v_y = Math.tan(p.x / this.radius_g_1); - v_z = Math.tan(p.y / this.radius_g_1) * hypot(1.0, v_y); - } - - var v_zp = v_z / this.radius_p; - a = v_y * v_y + v_zp * v_zp + v_x * v_x; - b = 2 * this.radius_g * v_x; - det = (b * b) - 4 * a * this.C; - - if (det < 0.0) { - p.x = Number.NaN; - p.y = Number.NaN; - return p; - } - - k = (-b - Math.sqrt(det)) / (2.0 * a); - v_x = this.radius_g + k * v_x; - v_y *= k; - v_z *= k; - - p.x = Math.atan2(v_y, v_x); - p.y = Math.atan(v_z * Math.cos(p.x) / v_x); - p.y = Math.atan(this.radius_p_inv2 * Math.tan(p.y)); - } else if (this.shape === 'sphere') { - if (this.flip_axis) { - v_z = Math.tan(p.y / this.radius_g_1); - v_y = Math.tan(p.x / this.radius_g_1) * Math.sqrt(1.0 + v_z * v_z); - } else { - v_y = Math.tan(p.x / this.radius_g_1); - v_z = Math.tan(p.y / this.radius_g_1) * Math.sqrt(1.0 + v_y * v_y); - } - - a = v_y * v_y + v_z * v_z + v_x * v_x; - b = 2 * this.radius_g * v_x; - det = (b * b) - 4 * a * this.C; - if (det < 0.0) { - p.x = Number.NaN; - p.y = Number.NaN; - return p; - } - - k = (-b - Math.sqrt(det)) / (2.0 * a); - v_x = this.radius_g + k * v_x; - v_y *= k; - v_z *= k; - - p.x = Math.atan2(v_y, v_x); - p.y = Math.atan(v_z * Math.cos(p.x) / v_x); - } - p.x = p.x + this.long0; - return p; -} - -export var names = ["Geostationary Satellite View", "Geostationary_Satellite", "geos"]; -export default { - init: init, - forward: forward, - inverse: inverse, - names: names, -}; diff --git a/src/proj4/projectionsBackup/gnom.js b/src/proj4/projectionsBackup/gnom.js deleted file mode 100644 index 066cff86..00000000 --- a/src/proj4/projectionsBackup/gnom.js +++ /dev/null @@ -1,104 +0,0 @@ -import adjust_lon from '../common/adjust_lon'; -import asinz from '../common/asinz'; -import {EPSLN} from '../constants/values'; - -/* - reference: - Wolfram Mathworld "Gnomonic Projection" - http://mathworld.wolfram.com/GnomonicProjection.html - Accessed: 12th November 2009 - */ -export function init() { - - /* Place parameters in static storage for common use - -------------------------------------------------*/ - this.sin_p14 = Math.sin(this.lat0); - this.cos_p14 = Math.cos(this.lat0); - // Approximation for projecting points to the horizon (infinity) - this.infinity_dist = 1000 * this.a; - this.rc = 1; -} - -/* Gnomonic forward equations--mapping lat,long to x,y - ---------------------------------------------------*/ -export function forward(p) { - var sinphi, cosphi; /* sin and cos value */ - var dlon; /* delta longitude value */ - var coslon; /* cos of longitude */ - var ksp; /* scale factor */ - var g; - var x, y; - var lon = p.x; - var lat = p.y; - /* Forward equations - -----------------*/ - dlon = adjust_lon(lon - this.long0); - - sinphi = Math.sin(lat); - cosphi = Math.cos(lat); - - coslon = Math.cos(dlon); - g = this.sin_p14 * sinphi + this.cos_p14 * cosphi * coslon; - ksp = 1; - if ((g > 0) || (Math.abs(g) <= EPSLN)) { - x = this.x0 + this.a * ksp * cosphi * Math.sin(dlon) / g; - y = this.y0 + this.a * ksp * (this.cos_p14 * sinphi - this.sin_p14 * cosphi * coslon) / g; - } - else { - - // Point is in the opposing hemisphere and is unprojectable - // We still need to return a reasonable point, so we project - // to infinity, on a bearing - // equivalent to the northern hemisphere equivalent - // This is a reasonable approximation for short shapes and lines that - // straddle the horizon. - - x = this.x0 + this.infinity_dist * cosphi * Math.sin(dlon); - y = this.y0 + this.infinity_dist * (this.cos_p14 * sinphi - this.sin_p14 * cosphi * coslon); - - } - p.x = x; - p.y = y; - return p; -} - -export function inverse(p) { - var rh; /* Rho */ - var sinc, cosc; - var c; - var lon, lat; - - /* Inverse equations - -----------------*/ - p.x = (p.x - this.x0) / this.a; - p.y = (p.y - this.y0) / this.a; - - p.x /= this.k0; - p.y /= this.k0; - - if ((rh = Math.sqrt(p.x * p.x + p.y * p.y))) { - c = Math.atan2(rh, this.rc); - sinc = Math.sin(c); - cosc = Math.cos(c); - - lat = asinz(cosc * this.sin_p14 + (p.y * sinc * this.cos_p14) / rh); - lon = Math.atan2(p.x * sinc, rh * this.cos_p14 * cosc - p.y * this.sin_p14 * sinc); - lon = adjust_lon(this.long0 + lon); - } - else { - lat = this.phic0; - lon = 0; - } - - p.x = lon; - p.y = lat; - return p; -} - -export var names = ["gnom"]; -export default { - init: init, - forward: forward, - inverse: inverse, - names: names -}; diff --git a/src/proj4/projectionsBackup/gstmerc.js b/src/proj4/projectionsBackup/gstmerc.js deleted file mode 100644 index 763dc68a..00000000 --- a/src/proj4/projectionsBackup/gstmerc.js +++ /dev/null @@ -1,63 +0,0 @@ -import latiso from '../common/latiso'; -import sinh from '../common/sinh'; -import cosh from '../common/cosh'; -import invlatiso from '../common/invlatiso'; - -export function init() { - - // array of: a, b, lon0, lat0, k0, x0, y0 - var temp = this.b / this.a; - this.e = Math.sqrt(1 - temp * temp); - this.lc = this.long0; - this.rs = Math.sqrt(1 + this.e * this.e * Math.pow(Math.cos(this.lat0), 4) / (1 - this.e * this.e)); - var sinz = Math.sin(this.lat0); - var pc = Math.asin(sinz / this.rs); - var sinzpc = Math.sin(pc); - this.cp = latiso(0, pc, sinzpc) - this.rs * latiso(this.e, this.lat0, sinz); - this.n2 = this.k0 * this.a * Math.sqrt(1 - this.e * this.e) / (1 - this.e * this.e * sinz * sinz); - this.xs = this.x0; - this.ys = this.y0 - this.n2 * pc; - - if (!this.title) { - this.title = "Gauss Schreiber transverse mercator"; - } -} - -// forward equations--mapping lat,long to x,y -// ----------------------------------------------------------------- -export function forward(p) { - - var lon = p.x; - var lat = p.y; - - var L = this.rs * (lon - this.lc); - var Ls = this.cp + (this.rs * latiso(this.e, lat, Math.sin(lat))); - var lat1 = Math.asin(Math.sin(L) / cosh(Ls)); - var Ls1 = latiso(0, lat1, Math.sin(lat1)); - p.x = this.xs + (this.n2 * Ls1); - p.y = this.ys + (this.n2 * Math.atan(sinh(Ls) / Math.cos(L))); - return p; -} - -// inverse equations--mapping x,y to lat/long -// ----------------------------------------------------------------- -export function inverse(p) { - - var x = p.x; - var y = p.y; - - var L = Math.atan(sinh((x - this.xs) / this.n2) / Math.cos((y - this.ys) / this.n2)); - var lat1 = Math.asin(Math.sin((y - this.ys) / this.n2) / cosh((x - this.xs) / this.n2)); - var LC = latiso(0, lat1, Math.sin(lat1)); - p.x = this.lc + L / this.rs; - p.y = invlatiso(this.e, (LC - this.cp) / this.rs); - return p; -} - -export var names = ["gstmerg", "gstmerc"]; -export default { - init: init, - forward: forward, - inverse: inverse, - names: names -}; diff --git a/src/proj4/projectionsBackup/krovak.js b/src/proj4/projectionsBackup/krovak.js deleted file mode 100644 index c26c5ba7..00000000 --- a/src/proj4/projectionsBackup/krovak.js +++ /dev/null @@ -1,106 +0,0 @@ -import { adjustLon } from '../common'; - -export function init() { - this.a = 6377397.155; - this.es = 0.006674372230614; - this.e = Math.sqrt(this.es); - if (!this.lat0) { - this.lat0 = 0.863937979737193; - } - if (!this.long0) { - this.long0 = 0.7417649320975901 - 0.308341501185665; - } - /* if scale not set default to 0.9999 */ - if (!this.k0) { - this.k0 = 0.9999; - } - this.s45 = 0.785398163397448; /* 45 */ - this.s90 = 2 * this.s45; - this.fi0 = this.lat0; - this.e2 = this.es; - this.e = Math.sqrt(this.e2); - this.alfa = Math.sqrt(1 + (this.e2 * Math.pow(Math.cos(this.fi0), 4)) / (1 - this.e2)); - this.uq = 1.04216856380474; - this.u0 = Math.asin(Math.sin(this.fi0) / this.alfa); - this.g = Math.pow((1 + this.e * Math.sin(this.fi0)) / (1 - this.e * Math.sin(this.fi0)), this.alfa * this.e / 2); - this.k = Math.tan(this.u0 / 2 + this.s45) / Math.pow(Math.tan(this.fi0 / 2 + this.s45), this.alfa) * this.g; - this.k1 = this.k0; - this.n0 = this.a * Math.sqrt(1 - this.e2) / (1 - this.e2 * Math.pow(Math.sin(this.fi0), 2)); - this.s0 = 1.37008346281555; - this.n = Math.sin(this.s0); - this.ro0 = this.k1 * this.n0 / Math.tan(this.s0); - this.ad = this.s90 - this.uq; -} - -/* ellipsoid */ -/* calculate xy from lat/lon */ -/* Constants, identical to inverse transform function */ -export function forward(p) { - var gfi, u, deltav, s, d, eps, ro; - var lon = p.x; - var lat = p.y; - var delta_lon = adjustLon(lon - this.long0); - /* Transformation */ - gfi = Math.pow(((1 + this.e * Math.sin(lat)) / (1 - this.e * Math.sin(lat))), (this.alfa * this.e / 2)); - u = 2 * (Math.atan(this.k * Math.pow(Math.tan(lat / 2 + this.s45), this.alfa) / gfi) - this.s45); - deltav = -delta_lon * this.alfa; - s = Math.asin(Math.cos(this.ad) * Math.sin(u) + Math.sin(this.ad) * Math.cos(u) * Math.cos(deltav)); - d = Math.asin(Math.cos(u) * Math.sin(deltav) / Math.cos(s)); - eps = this.n * d; - ro = this.ro0 * Math.pow(Math.tan(this.s0 / 2 + this.s45), this.n) / Math.pow(Math.tan(s / 2 + this.s45), this.n); - p.y = ro * Math.cos(eps) / 1; - p.x = ro * Math.sin(eps) / 1; - - if (!this.czech) { - p.y *= -1; - p.x *= -1; - } - return (p); -} - -/* calculate lat/lon from xy */ -export function inverse(p) { - var u, deltav, s, d, eps, ro, fi1; - var ok; - - /* Transformation */ - /* revert y, x*/ - var tmp = p.x; - p.x = p.y; - p.y = tmp; - if (!this.czech) { - p.y *= -1; - p.x *= -1; - } - ro = Math.sqrt(p.x * p.x + p.y * p.y); - eps = Math.atan2(p.y, p.x); - d = eps / Math.sin(this.s0); - s = 2 * (Math.atan(Math.pow(this.ro0 / ro, 1 / this.n) * Math.tan(this.s0 / 2 + this.s45)) - this.s45); - u = Math.asin(Math.cos(this.ad) * Math.sin(s) - Math.sin(this.ad) * Math.cos(s) * Math.cos(d)); - deltav = Math.asin(Math.cos(s) * Math.sin(d) / Math.cos(u)); - p.x = this.long0 - deltav / this.alfa; - fi1 = u; - ok = 0; - var iter = 0; - do { - p.y = 2 * (Math.atan(Math.pow(this.k, - 1 / this.alfa) * Math.pow(Math.tan(u / 2 + this.s45), 1 / this.alfa) * Math.pow((1 + this.e * Math.sin(fi1)) / (1 - this.e * Math.sin(fi1)), this.e / 2)) - this.s45); - if (Math.abs(fi1 - p.y) < 0.0000000001) { - ok = 1; - } - fi1 = p.y; - iter += 1; - } while (ok === 0 && iter < 15); - if (iter >= 15) { - return null; - } - - return (p); -} - -export var names = ["Krovak", "krovak"]; -export default { - init: init, - forward: forward, - inverse: inverse, - names: names -}; diff --git a/src/proj4/projectionsBackup/laea.js b/src/proj4/projectionsBackup/laea.js deleted file mode 100644 index d9c5de6e..00000000 --- a/src/proj4/projectionsBackup/laea.js +++ /dev/null @@ -1,298 +0,0 @@ - -import {HALF_PI, EPSLN, QUART_PI} from '../constants/values'; - -import qsfnz from '../common/qsfnz'; -import adjust_lon from '../common/adjust_lon'; - -/* - reference - "New Equal-Area Map Projections for Noncircular Regions", John P. Snyder, - The American Cartographer, Vol 15, No. 4, October 1988, pp. 341-355. - */ - -export var S_POLE = 1; - -export var N_POLE = 2; -export var EQUIT = 3; -export var OBLIQ = 4; - -/* Initialize the Lambert Azimuthal Equal Area projection - ------------------------------------------------------*/ -export function init() { - var t = Math.abs(this.lat0); - if (Math.abs(t - HALF_PI) < EPSLN) { - this.mode = this.lat0 < 0 ? this.S_POLE : this.N_POLE; - } - else if (Math.abs(t) < EPSLN) { - this.mode = this.EQUIT; - } - else { - this.mode = this.OBLIQ; - } - if (this.es > 0) { - var sinphi; - - this.qp = qsfnz(this.e, 1); - this.mmf = 0.5 / (1 - this.es); - this.apa = authset(this.es); - switch (this.mode) { - case this.N_POLE: - this.dd = 1; - break; - case this.S_POLE: - this.dd = 1; - break; - case this.EQUIT: - this.rq = Math.sqrt(0.5 * this.qp); - this.dd = 1 / this.rq; - this.xmf = 1; - this.ymf = 0.5 * this.qp; - break; - case this.OBLIQ: - this.rq = Math.sqrt(0.5 * this.qp); - sinphi = Math.sin(this.lat0); - this.sinb1 = qsfnz(this.e, sinphi) / this.qp; - this.cosb1 = Math.sqrt(1 - this.sinb1 * this.sinb1); - this.dd = Math.cos(this.lat0) / (Math.sqrt(1 - this.es * sinphi * sinphi) * this.rq * this.cosb1); - this.ymf = (this.xmf = this.rq) / this.dd; - this.xmf *= this.dd; - break; - } - } - else { - if (this.mode === this.OBLIQ) { - this.sinph0 = Math.sin(this.lat0); - this.cosph0 = Math.cos(this.lat0); - } - } -} - -/* Lambert Azimuthal Equal Area forward equations--mapping lat,long to x,y - -----------------------------------------------------------------------*/ -export function forward(p) { - - /* Forward equations - -----------------*/ - var x, y, coslam, sinlam, sinphi, q, sinb, cosb, b, cosphi; - var lam = p.x; - var phi = p.y; - - lam = adjust_lon(lam - this.long0); - if (this.sphere) { - sinphi = Math.sin(phi); - cosphi = Math.cos(phi); - coslam = Math.cos(lam); - if (this.mode === this.OBLIQ || this.mode === this.EQUIT) { - y = (this.mode === this.EQUIT) ? 1 + cosphi * coslam : 1 + this.sinph0 * sinphi + this.cosph0 * cosphi * coslam; - if (y <= EPSLN) { - return null; - } - y = Math.sqrt(2 / y); - x = y * cosphi * Math.sin(lam); - y *= (this.mode === this.EQUIT) ? sinphi : this.cosph0 * sinphi - this.sinph0 * cosphi * coslam; - } - else if (this.mode === this.N_POLE || this.mode === this.S_POLE) { - if (this.mode === this.N_POLE) { - coslam = -coslam; - } - if (Math.abs(phi + this.lat0) < EPSLN) { - return null; - } - y = QUART_PI - phi * 0.5; - y = 2 * ((this.mode === this.S_POLE) ? Math.cos(y) : Math.sin(y)); - x = y * Math.sin(lam); - y *= coslam; - } - } - else { - sinb = 0; - cosb = 0; - b = 0; - coslam = Math.cos(lam); - sinlam = Math.sin(lam); - sinphi = Math.sin(phi); - q = qsfnz(this.e, sinphi); - if (this.mode === this.OBLIQ || this.mode === this.EQUIT) { - sinb = q / this.qp; - cosb = Math.sqrt(1 - sinb * sinb); - } - switch (this.mode) { - case this.OBLIQ: - b = 1 + this.sinb1 * sinb + this.cosb1 * cosb * coslam; - break; - case this.EQUIT: - b = 1 + cosb * coslam; - break; - case this.N_POLE: - b = HALF_PI + phi; - q = this.qp - q; - break; - case this.S_POLE: - b = phi - HALF_PI; - q = this.qp + q; - break; - } - if (Math.abs(b) < EPSLN) { - return null; - } - switch (this.mode) { - case this.OBLIQ: - case this.EQUIT: - b = Math.sqrt(2 / b); - if (this.mode === this.OBLIQ) { - y = this.ymf * b * (this.cosb1 * sinb - this.sinb1 * cosb * coslam); - } - else { - y = (b = Math.sqrt(2 / (1 + cosb * coslam))) * sinb * this.ymf; - } - x = this.xmf * b * cosb * sinlam; - break; - case this.N_POLE: - case this.S_POLE: - if (q >= 0) { - x = (b = Math.sqrt(q)) * sinlam; - y = coslam * ((this.mode === this.S_POLE) ? b : -b); - } - else { - x = y = 0; - } - break; - } - } - - p.x = this.a * x + this.x0; - p.y = this.a * y + this.y0; - return p; -} - -/* Inverse equations - -----------------*/ -export function inverse(p) { - p.x -= this.x0; - p.y -= this.y0; - var x = p.x / this.a; - var y = p.y / this.a; - var lam, phi, cCe, sCe, q, rho, ab; - if (this.sphere) { - var cosz = 0, - rh, sinz = 0; - - rh = Math.sqrt(x * x + y * y); - phi = rh * 0.5; - if (phi > 1) { - return null; - } - phi = 2 * Math.asin(phi); - if (this.mode === this.OBLIQ || this.mode === this.EQUIT) { - sinz = Math.sin(phi); - cosz = Math.cos(phi); - } - switch (this.mode) { - case this.EQUIT: - phi = (Math.abs(rh) <= EPSLN) ? 0 : Math.asin(y * sinz / rh); - x *= sinz; - y = cosz * rh; - break; - case this.OBLIQ: - phi = (Math.abs(rh) <= EPSLN) ? this.lat0 : Math.asin(cosz * this.sinph0 + y * sinz * this.cosph0 / rh); - x *= sinz * this.cosph0; - y = (cosz - Math.sin(phi) * this.sinph0) * rh; - break; - case this.N_POLE: - y = -y; - phi = HALF_PI - phi; - break; - case this.S_POLE: - phi -= HALF_PI; - break; - } - lam = (y === 0 && (this.mode === this.EQUIT || this.mode === this.OBLIQ)) ? 0 : Math.atan2(x, y); - } - else { - ab = 0; - if (this.mode === this.OBLIQ || this.mode === this.EQUIT) { - x /= this.dd; - y *= this.dd; - rho = Math.sqrt(x * x + y * y); - if (rho < EPSLN) { - p.x = this.long0; - p.y = this.lat0; - return p; - } - sCe = 2 * Math.asin(0.5 * rho / this.rq); - cCe = Math.cos(sCe); - x *= (sCe = Math.sin(sCe)); - if (this.mode === this.OBLIQ) { - ab = cCe * this.sinb1 + y * sCe * this.cosb1 / rho; - q = this.qp * ab; - y = rho * this.cosb1 * cCe - y * this.sinb1 * sCe; - } - else { - ab = y * sCe / rho; - q = this.qp * ab; - y = rho * cCe; - } - } - else if (this.mode === this.N_POLE || this.mode === this.S_POLE) { - if (this.mode === this.N_POLE) { - y = -y; - } - q = (x * x + y * y); - if (!q) { - p.x = this.long0; - p.y = this.lat0; - return p; - } - ab = 1 - q / this.qp; - if (this.mode === this.S_POLE) { - ab = -ab; - } - } - lam = Math.atan2(x, y); - phi = authlat(Math.asin(ab), this.apa); - } - - p.x = adjust_lon(this.long0 + lam); - p.y = phi; - return p; -} - -/* determine latitude from authalic latitude */ -var P00 = 0.33333333333333333333; - -var P01 = 0.17222222222222222222; -var P02 = 0.10257936507936507936; -var P10 = 0.06388888888888888888; -var P11 = 0.06640211640211640211; -var P20 = 0.01641501294219154443; - -function authset(es) { - var t; - var APA = []; - APA[0] = es * P00; - t = es * es; - APA[0] += t * P01; - APA[1] = t * P10; - t *= es; - APA[0] += t * P02; - APA[1] += t * P11; - APA[2] = t * P20; - return APA; -} - -function authlat(beta, APA) { - var t = beta + beta; - return (beta + APA[0] * Math.sin(t) + APA[1] * Math.sin(t + t) + APA[2] * Math.sin(t + t + t)); -} - -export var names = ["Lambert Azimuthal Equal Area", "Lambert_Azimuthal_Equal_Area", "laea"]; -export default { - init: init, - forward: forward, - inverse: inverse, - names: names, - S_POLE: S_POLE, - N_POLE: N_POLE, - EQUIT: EQUIT, - OBLIQ: OBLIQ -}; diff --git a/src/proj4/projectionsBackup/lcc.js b/src/proj4/projectionsBackup/lcc.js deleted file mode 100644 index bb92b3f4..00000000 --- a/src/proj4/projectionsBackup/lcc.js +++ /dev/null @@ -1,150 +0,0 @@ -import msfnz from '../common/msfnz'; -import tsfnz from '../common/tsfnz'; -import sign from '../common/sign'; -import adjust_lon from '../common/adjust_lon'; -import phi2z from '../common/phi2z'; -import {HALF_PI, EPSLN} from '../constants/values'; -export function init() { - - //double lat0; /* the reference latitude */ - //double long0; /* the reference longitude */ - //double lat1; /* first standard parallel */ - //double lat2; /* second standard parallel */ - //double r_maj; /* major axis */ - //double r_min; /* minor axis */ - //double false_east; /* x offset in meters */ - //double false_north; /* y offset in meters */ - - //the above value can be set with proj4.defs - //example: proj4.defs("EPSG:2154","+proj=lcc +lat_1=49 +lat_2=44 +lat_0=46.5 +lon_0=3 +x_0=700000 +y_0=6600000 +ellps=GRS80 +towgs84=0,0,0,0,0,0,0 +units=m +no_defs"); - - if (!this.lat2) { - this.lat2 = this.lat1; - } //if lat2 is not defined - if (!this.k0) { - this.k0 = 1; - } - this.x0 = this.x0 || 0; - this.y0 = this.y0 || 0; - // Standard Parallels cannot be equal and on opposite sides of the equator - if (Math.abs(this.lat1 + this.lat2) < EPSLN) { - return; - } - - var temp = this.b / this.a; - this.e = Math.sqrt(1 - temp * temp); - - var sin1 = Math.sin(this.lat1); - var cos1 = Math.cos(this.lat1); - var ms1 = msfnz(this.e, sin1, cos1); - var ts1 = tsfnz(this.e, this.lat1, sin1); - - var sin2 = Math.sin(this.lat2); - var cos2 = Math.cos(this.lat2); - var ms2 = msfnz(this.e, sin2, cos2); - var ts2 = tsfnz(this.e, this.lat2, sin2); - - var ts0 = tsfnz(this.e, this.lat0, Math.sin(this.lat0)); - - if (Math.abs(this.lat1 - this.lat2) > EPSLN) { - this.ns = Math.log(ms1 / ms2) / Math.log(ts1 / ts2); - } - else { - this.ns = sin1; - } - if (isNaN(this.ns)) { - this.ns = sin1; - } - this.f0 = ms1 / (this.ns * Math.pow(ts1, this.ns)); - this.rh = this.a * this.f0 * Math.pow(ts0, this.ns); - if (!this.title) { - this.title = "Lambert Conformal Conic"; - } -} - -// Lambert Conformal conic forward equations--mapping lat,long to x,y -// ----------------------------------------------------------------- -export function forward(p) { - - var lon = p.x; - var lat = p.y; - - // singular cases : - if (Math.abs(2 * Math.abs(lat) - Math.PI) <= EPSLN) { - lat = sign(lat) * (HALF_PI - 2 * EPSLN); - } - - var con = Math.abs(Math.abs(lat) - HALF_PI); - var ts, rh1; - if (con > EPSLN) { - ts = tsfnz(this.e, lat, Math.sin(lat)); - rh1 = this.a * this.f0 * Math.pow(ts, this.ns); - } - else { - con = lat * this.ns; - if (con <= 0) { - return null; - } - rh1 = 0; - } - var theta = this.ns * adjust_lon(lon - this.long0); - p.x = this.k0 * (rh1 * Math.sin(theta)) + this.x0; - p.y = this.k0 * (this.rh - rh1 * Math.cos(theta)) + this.y0; - - return p; -} - -// Lambert Conformal Conic inverse equations--mapping x,y to lat/long -// ----------------------------------------------------------------- -export function inverse(p) { - - var rh1, con, ts; - var lat, lon; - var x = (p.x - this.x0) / this.k0; - var y = (this.rh - (p.y - this.y0) / this.k0); - if (this.ns > 0) { - rh1 = Math.sqrt(x * x + y * y); - con = 1; - } - else { - rh1 = -Math.sqrt(x * x + y * y); - con = -1; - } - var theta = 0; - if (rh1 !== 0) { - theta = Math.atan2((con * x), (con * y)); - } - if ((rh1 !== 0) || (this.ns > 0)) { - con = 1 / this.ns; - ts = Math.pow((rh1 / (this.a * this.f0)), con); - lat = phi2z(this.e, ts); - if (lat === -9999) { - return null; - } - } - else { - lat = -HALF_PI; - } - lon = adjust_lon(theta / this.ns + this.long0); - - p.x = lon; - p.y = lat; - return p; -} - -export var names = [ - "Lambert Tangential Conformal Conic Projection", - "Lambert_Conformal_Conic", - "Lambert_Conformal_Conic_1SP", - "Lambert_Conformal_Conic_2SP", - "lcc", - "Lambert Conic Conformal (1SP)", - "Lambert Conic Conformal (2SP)" -]; - -export default { - init: init, - forward: forward, - inverse: inverse, - names: names -}; diff --git a/src/proj4/projectionsBackup/mill.js b/src/proj4/projectionsBackup/mill.js deleted file mode 100644 index 3d6ff0d6..00000000 --- a/src/proj4/projectionsBackup/mill.js +++ /dev/null @@ -1,52 +0,0 @@ -import adjust_lon from '../common/adjust_lon'; - -/* - reference - "New Equal-Area Map Projections for Noncircular Regions", John P. Snyder, - The American Cartographer, Vol 15, No. 4, October 1988, pp. 341-355. - */ - - -/* Initialize the Miller Cylindrical projection - -------------------------------------------*/ -export function init() { - //no-op -} - -/* Miller Cylindrical forward equations--mapping lat,long to x,y - ------------------------------------------------------------*/ -export function forward(p) { - var lon = p.x; - var lat = p.y; - /* Forward equations - -----------------*/ - var dlon = adjust_lon(lon - this.long0); - var x = this.x0 + this.a * dlon; - var y = this.y0 + this.a * Math.log(Math.tan((Math.PI / 4) + (lat / 2.5))) * 1.25; - - p.x = x; - p.y = y; - return p; -} - -/* Miller Cylindrical inverse equations--mapping x,y to lat/long - ------------------------------------------------------------*/ -export function inverse(p) { - p.x -= this.x0; - p.y -= this.y0; - - var lon = adjust_lon(this.long0 + p.x / this.a); - var lat = 2.5 * (Math.atan(Math.exp(0.8 * p.y / this.a)) - Math.PI / 4); - - p.x = lon; - p.y = lat; - return p; -} - -export var names = ["Miller_Cylindrical", "mill"]; -export default { - init: init, - forward: forward, - inverse: inverse, - names: names -}; diff --git a/src/proj4/projectionsBackup/moll.js b/src/proj4/projectionsBackup/moll.js deleted file mode 100644 index faa2e51e..00000000 --- a/src/proj4/projectionsBackup/moll.js +++ /dev/null @@ -1,83 +0,0 @@ -import adjust_lon from '../common/adjust_lon'; -export function init() {} -import {EPSLN} from '../constants/values'; -/* Mollweide forward equations--mapping lat,long to x,y - ----------------------------------------------------*/ -export function forward(p) { - - /* Forward equations - -----------------*/ - var lon = p.x; - var lat = p.y; - - var delta_lon = adjust_lon(lon - this.long0); - var theta = lat; - var con = Math.PI * Math.sin(lat); - - /* Iterate using the Newton-Raphson method to find theta - -----------------------------------------------------*/ - while (true) { - var delta_theta = -(theta + Math.sin(theta) - con) / (1 + Math.cos(theta)); - theta += delta_theta; - if (Math.abs(delta_theta) < EPSLN) { - break; - } - } - theta /= 2; - - /* If the latitude is 90 deg, force the x coordinate to be "0 + false easting" - this is done here because of precision problems with "cos(theta)" - --------------------------------------------------------------------------*/ - if (Math.PI / 2 - Math.abs(lat) < EPSLN) { - delta_lon = 0; - } - var x = 0.900316316158 * this.a * delta_lon * Math.cos(theta) + this.x0; - var y = 1.4142135623731 * this.a * Math.sin(theta) + this.y0; - - p.x = x; - p.y = y; - return p; -} - -export function inverse(p) { - var theta; - var arg; - - /* Inverse equations - -----------------*/ - p.x -= this.x0; - p.y -= this.y0; - arg = p.y / (1.4142135623731 * this.a); - - /* Because of division by zero problems, 'arg' can not be 1. Therefore - a number very close to one is used instead. - -------------------------------------------------------------------*/ - if (Math.abs(arg) > 0.999999999999) { - arg = 0.999999999999; - } - theta = Math.asin(arg); - var lon = adjust_lon(this.long0 + (p.x / (0.900316316158 * this.a * Math.cos(theta)))); - if (lon < (-Math.PI)) { - lon = -Math.PI; - } - if (lon > Math.PI) { - lon = Math.PI; - } - arg = (2 * theta + Math.sin(2 * theta)) / Math.PI; - if (Math.abs(arg) > 1) { - arg = 1; - } - var lat = Math.asin(arg); - - p.x = lon; - p.y = lat; - return p; -} - -export var names = ["Mollweide", "moll"]; -export default { - init: init, - forward: forward, - inverse: inverse, - names: names -}; diff --git a/src/proj4/projectionsBackup/nzmg.js b/src/proj4/projectionsBackup/nzmg.js deleted file mode 100644 index be0741d7..00000000 --- a/src/proj4/projectionsBackup/nzmg.js +++ /dev/null @@ -1,226 +0,0 @@ -import {SEC_TO_RAD} from '../constants/values'; - -/* - reference - Department of Land and Survey Technical Circular 1973/32 - http://www.linz.govt.nz/docs/miscellaneous/nz-map-definition.pdf - OSG Technical Report 4.1 - http://www.linz.govt.nz/docs/miscellaneous/nzmg.pdf - */ - -/** - * iterations: Number of iterations to refine inverse transform. - * 0 -> km accuracy - * 1 -> m accuracy -- suitable for most mapping applications - * 2 -> mm accuracy - */ -export var iterations = 1; - -export function init() { - this.A = []; - this.A[1] = 0.6399175073; - this.A[2] = -0.1358797613; - this.A[3] = 0.063294409; - this.A[4] = -0.02526853; - this.A[5] = 0.0117879; - this.A[6] = -0.0055161; - this.A[7] = 0.0026906; - this.A[8] = -0.001333; - this.A[9] = 0.00067; - this.A[10] = -0.00034; - - this.B_re = []; - this.B_im = []; - this.B_re[1] = 0.7557853228; - this.B_im[1] = 0; - this.B_re[2] = 0.249204646; - this.B_im[2] = 0.003371507; - this.B_re[3] = -0.001541739; - this.B_im[3] = 0.041058560; - this.B_re[4] = -0.10162907; - this.B_im[4] = 0.01727609; - this.B_re[5] = -0.26623489; - this.B_im[5] = -0.36249218; - this.B_re[6] = -0.6870983; - this.B_im[6] = -1.1651967; - - this.C_re = []; - this.C_im = []; - this.C_re[1] = 1.3231270439; - this.C_im[1] = 0; - this.C_re[2] = -0.577245789; - this.C_im[2] = -0.007809598; - this.C_re[3] = 0.508307513; - this.C_im[3] = -0.112208952; - this.C_re[4] = -0.15094762; - this.C_im[4] = 0.18200602; - this.C_re[5] = 1.01418179; - this.C_im[5] = 1.64497696; - this.C_re[6] = 1.9660549; - this.C_im[6] = 2.5127645; - - this.D = []; - this.D[1] = 1.5627014243; - this.D[2] = 0.5185406398; - this.D[3] = -0.03333098; - this.D[4] = -0.1052906; - this.D[5] = -0.0368594; - this.D[6] = 0.007317; - this.D[7] = 0.01220; - this.D[8] = 0.00394; - this.D[9] = -0.0013; -} - -/** - New Zealand Map Grid Forward - long/lat to x/y - long/lat in radians - */ -export function forward(p) { - var n; - var lon = p.x; - var lat = p.y; - - var delta_lat = lat - this.lat0; - var delta_lon = lon - this.long0; - - // 1. Calculate d_phi and d_psi ... // and d_lambda - // For this algorithm, delta_latitude is in seconds of arc x 10-5, so we need to scale to those units. Longitude is radians. - var d_phi = delta_lat / SEC_TO_RAD * 1E-5; - var d_lambda = delta_lon; - var d_phi_n = 1; // d_phi^0 - - var d_psi = 0; - for (n = 1; n <= 10; n++) { - d_phi_n = d_phi_n * d_phi; - d_psi = d_psi + this.A[n] * d_phi_n; - } - - // 2. Calculate theta - var th_re = d_psi; - var th_im = d_lambda; - - // 3. Calculate z - var th_n_re = 1; - var th_n_im = 0; // theta^0 - var th_n_re1; - var th_n_im1; - - var z_re = 0; - var z_im = 0; - for (n = 1; n <= 6; n++) { - th_n_re1 = th_n_re * th_re - th_n_im * th_im; - th_n_im1 = th_n_im * th_re + th_n_re * th_im; - th_n_re = th_n_re1; - th_n_im = th_n_im1; - z_re = z_re + this.B_re[n] * th_n_re - this.B_im[n] * th_n_im; - z_im = z_im + this.B_im[n] * th_n_re + this.B_re[n] * th_n_im; - } - - // 4. Calculate easting and northing - p.x = (z_im * this.a) + this.x0; - p.y = (z_re * this.a) + this.y0; - - return p; -} - -/** - New Zealand Map Grid Inverse - x/y to long/lat - */ -export function inverse(p) { - var n; - var x = p.x; - var y = p.y; - - var delta_x = x - this.x0; - var delta_y = y - this.y0; - - // 1. Calculate z - var z_re = delta_y / this.a; - var z_im = delta_x / this.a; - - // 2a. Calculate theta - first approximation gives km accuracy - var z_n_re = 1; - var z_n_im = 0; // z^0 - var z_n_re1; - var z_n_im1; - - var th_re = 0; - var th_im = 0; - for (n = 1; n <= 6; n++) { - z_n_re1 = z_n_re * z_re - z_n_im * z_im; - z_n_im1 = z_n_im * z_re + z_n_re * z_im; - z_n_re = z_n_re1; - z_n_im = z_n_im1; - th_re = th_re + this.C_re[n] * z_n_re - this.C_im[n] * z_n_im; - th_im = th_im + this.C_im[n] * z_n_re + this.C_re[n] * z_n_im; - } - - // 2b. Iterate to refine the accuracy of the calculation - // 0 iterations gives km accuracy - // 1 iteration gives m accuracy -- good enough for most mapping applications - // 2 iterations bives mm accuracy - for (var i = 0; i < this.iterations; i++) { - var th_n_re = th_re; - var th_n_im = th_im; - var th_n_re1; - var th_n_im1; - - var num_re = z_re; - var num_im = z_im; - for (n = 2; n <= 6; n++) { - th_n_re1 = th_n_re * th_re - th_n_im * th_im; - th_n_im1 = th_n_im * th_re + th_n_re * th_im; - th_n_re = th_n_re1; - th_n_im = th_n_im1; - num_re = num_re + (n - 1) * (this.B_re[n] * th_n_re - this.B_im[n] * th_n_im); - num_im = num_im + (n - 1) * (this.B_im[n] * th_n_re + this.B_re[n] * th_n_im); - } - - th_n_re = 1; - th_n_im = 0; - var den_re = this.B_re[1]; - var den_im = this.B_im[1]; - for (n = 2; n <= 6; n++) { - th_n_re1 = th_n_re * th_re - th_n_im * th_im; - th_n_im1 = th_n_im * th_re + th_n_re * th_im; - th_n_re = th_n_re1; - th_n_im = th_n_im1; - den_re = den_re + n * (this.B_re[n] * th_n_re - this.B_im[n] * th_n_im); - den_im = den_im + n * (this.B_im[n] * th_n_re + this.B_re[n] * th_n_im); - } - - // Complex division - var den2 = den_re * den_re + den_im * den_im; - th_re = (num_re * den_re + num_im * den_im) / den2; - th_im = (num_im * den_re - num_re * den_im) / den2; - } - - // 3. Calculate d_phi ... // and d_lambda - var d_psi = th_re; - var d_lambda = th_im; - var d_psi_n = 1; // d_psi^0 - - var d_phi = 0; - for (n = 1; n <= 9; n++) { - d_psi_n = d_psi_n * d_psi; - d_phi = d_phi + this.D[n] * d_psi_n; - } - - // 4. Calculate latitude and longitude - // d_phi is calcuated in second of arc * 10^-5, so we need to scale back to radians. d_lambda is in radians. - var lat = this.lat0 + (d_phi * SEC_TO_RAD * 1E5); - var lon = this.long0 + d_lambda; - - p.x = lon; - p.y = lat; - - return p; -} - -export var names = ["New_Zealand_Map_Grid", "nzmg"]; -export default { - init: init, - forward: forward, - inverse: inverse, - names: names -}; diff --git a/src/proj4/projectionsBackup/omerc.js b/src/proj4/projectionsBackup/omerc.js deleted file mode 100644 index 27cddb2e..00000000 --- a/src/proj4/projectionsBackup/omerc.js +++ /dev/null @@ -1,241 +0,0 @@ -import tsfnz from '../common/tsfnz'; -import adjust_lon from '../common/adjust_lon'; -import phi2z from '../common/phi2z'; -import { D2R, EPSLN, HALF_PI, TWO_PI, QUART_PI } from '../constants/values'; - -var TOL = 1e-7; - -function isTypeA(P) { - var typeAProjections = ['Hotine_Oblique_Mercator','Hotine_Oblique_Mercator_Azimuth_Natural_Origin']; - var projectionName = typeof P.PROJECTION === "object" ? Object.keys(P.PROJECTION)[0] : P.PROJECTION; - - return 'no_uoff' in P || 'no_off' in P || typeAProjections.indexOf(projectionName) !== -1; -} - - -/* Initialize the Oblique Mercator projection - ------------------------------------------*/ -export function init() { - var con, com, cosph0, D, F, H, L, sinph0, p, J, gamma = 0, - gamma0, lamc = 0, lam1 = 0, lam2 = 0, phi1 = 0, phi2 = 0, alpha_c = 0, AB; - - // only Type A uses the no_off or no_uoff property - // https://github.com/OSGeo/proj.4/issues/104 - this.no_off = isTypeA(this); - this.no_rot = 'no_rot' in this; - - var alp = false; - if ("alpha" in this) { - alp = true; - } - - var gam = false; - if ("rectified_grid_angle" in this) { - gam = true; - } - - if (alp) { - alpha_c = this.alpha; - } - - if (gam) { - gamma = (this.rectified_grid_angle * D2R); - } - - if (alp || gam) { - lamc = this.longc; - } else { - lam1 = this.long1; - phi1 = this.lat1; - lam2 = this.long2; - phi2 = this.lat2; - - if (Math.abs(phi1 - phi2) <= TOL || (con = Math.abs(phi1)) <= TOL || - Math.abs(con - HALF_PI) <= TOL || Math.abs(Math.abs(this.lat0) - HALF_PI) <= TOL || - Math.abs(Math.abs(phi2) - HALF_PI) <= TOL) { - throw new Error(); - } - } - - var one_es = 1.0 - this.es; - com = Math.sqrt(one_es); - - if (Math.abs(this.lat0) > EPSLN) { - sinph0 = Math.sin(this.lat0); - cosph0 = Math.cos(this.lat0); - con = 1 - this.es * sinph0 * sinph0; - this.B = cosph0 * cosph0; - this.B = Math.sqrt(1 + this.es * this.B * this.B / one_es); - this.A = this.B * this.k0 * com / con; - D = this.B * com / (cosph0 * Math.sqrt(con)); - F = D * D -1; - - if (F <= 0) { - F = 0; - } else { - F = Math.sqrt(F); - if (this.lat0 < 0) { - F = -F; - } - } - - this.E = F += D; - this.E *= Math.pow(tsfnz(this.e, this.lat0, sinph0), this.B); - } else { - this.B = 1 / com; - this.A = this.k0; - this.E = D = F = 1; - } - - if (alp || gam) { - if (alp) { - gamma0 = Math.asin(Math.sin(alpha_c) / D); - if (!gam) { - gamma = alpha_c; - } - } else { - gamma0 = gamma; - alpha_c = Math.asin(D * Math.sin(gamma0)); - } - this.lam0 = lamc - Math.asin(0.5 * (F - 1 / F) * Math.tan(gamma0)) / this.B; - } else { - H = Math.pow(tsfnz(this.e, phi1, Math.sin(phi1)), this.B); - L = Math.pow(tsfnz(this.e, phi2, Math.sin(phi2)), this.B); - F = this.E / H; - p = (L - H) / (L + H); - J = this.E * this.E; - J = (J - L * H) / (J + L * H); - con = lam1 - lam2; - - if (con < -Math.pi) { - lam2 -=TWO_PI; - } else if (con > Math.pi) { - lam2 += TWO_PI; - } - - this.lam0 = adjust_lon(0.5 * (lam1 + lam2) - Math.atan(J * Math.tan(0.5 * this.B * (lam1 - lam2)) / p) / this.B); - gamma0 = Math.atan(2 * Math.sin(this.B * adjust_lon(lam1 - this.lam0)) / (F - 1 / F)); - gamma = alpha_c = Math.asin(D * Math.sin(gamma0)); - } - - this.singam = Math.sin(gamma0); - this.cosgam = Math.cos(gamma0); - this.sinrot = Math.sin(gamma); - this.cosrot = Math.cos(gamma); - - this.rB = 1 / this.B; - this.ArB = this.A * this.rB; - this.BrA = 1 / this.ArB; - AB = this.A * this.B; - - if (this.no_off) { - this.u_0 = 0; - } else { - this.u_0 = Math.abs(this.ArB * Math.atan(Math.sqrt(D * D - 1) / Math.cos(alpha_c))); - - if (this.lat0 < 0) { - this.u_0 = - this.u_0; - } - } - - F = 0.5 * gamma0; - this.v_pole_n = this.ArB * Math.log(Math.tan(QUART_PI - F)); - this.v_pole_s = this.ArB * Math.log(Math.tan(QUART_PI + F)); -} - - -/* Oblique Mercator forward equations--mapping lat,long to x,y - ----------------------------------------------------------*/ -export function forward(p) { - var coords = {}; - var S, T, U, V, W, temp, u, v; - p.x = p.x - this.lam0; - - if (Math.abs(Math.abs(p.y) - HALF_PI) > EPSLN) { - W = this.E / Math.pow(tsfnz(this.e, p.y, Math.sin(p.y)), this.B); - - temp = 1 / W; - S = 0.5 * (W - temp); - T = 0.5 * (W + temp); - V = Math.sin(this.B * p.x); - U = (S * this.singam - V * this.cosgam) / T; - - if (Math.abs(Math.abs(U) - 1.0) < EPSLN) { - throw new Error(); - } - - v = 0.5 * this.ArB * Math.log((1 - U)/(1 + U)); - temp = Math.cos(this.B * p.x); - - if (Math.abs(temp) < TOL) { - u = this.A * p.x; - } else { - u = this.ArB * Math.atan2((S * this.cosgam + V * this.singam), temp); - } - } else { - v = p.y > 0 ? this.v_pole_n : this.v_pole_s; - u = this.ArB * p.y; - } - - if (this.no_rot) { - coords.x = u; - coords.y = v; - } else { - u -= this.u_0; - coords.x = v * this.cosrot + u * this.sinrot; - coords.y = u * this.cosrot - v * this.sinrot; - } - - coords.x = (this.a * coords.x + this.x0); - coords.y = (this.a * coords.y + this.y0); - - return coords; -} - -export function inverse(p) { - var u, v, Qp, Sp, Tp, Vp, Up; - var coords = {}; - - p.x = (p.x - this.x0) * (1.0 / this.a); - p.y = (p.y - this.y0) * (1.0 / this.a); - - if (this.no_rot) { - v = p.y; - u = p.x; - } else { - v = p.x * this.cosrot - p.y * this.sinrot; - u = p.y * this.cosrot + p.x * this.sinrot + this.u_0; - } - - Qp = Math.exp(-this.BrA * v); - Sp = 0.5 * (Qp - 1 / Qp); - Tp = 0.5 * (Qp + 1 / Qp); - Vp = Math.sin(this.BrA * u); - Up = (Vp * this.cosgam + Sp * this.singam) / Tp; - - if (Math.abs(Math.abs(Up) - 1) < EPSLN) { - coords.x = 0; - coords.y = Up < 0 ? -HALF_PI : HALF_PI; - } else { - coords.y = this.E / Math.sqrt((1 + Up) / (1 - Up)); - coords.y = phi2z(this.e, Math.pow(coords.y, 1 / this.B)); - - if (coords.y === Infinity) { - throw new Error(); - } - - coords.x = -this.rB * Math.atan2((Sp * this.cosgam - Vp * this.singam), Math.cos(this.BrA * u)); - } - - coords.x += this.lam0; - - return coords; -} - -export var names = ["Hotine_Oblique_Mercator", "Hotine Oblique Mercator", "Hotine_Oblique_Mercator_Azimuth_Natural_Origin", "Hotine_Oblique_Mercator_Two_Point_Natural_Origin", "Hotine_Oblique_Mercator_Azimuth_Center", "Oblique_Mercator", "omerc"]; -export default { - init: init, - forward: forward, - inverse: inverse, - names: names -}; diff --git a/src/proj4/projectionsBackup/ortho.js b/src/proj4/projectionsBackup/ortho.js deleted file mode 100644 index ed9f32e2..00000000 --- a/src/proj4/projectionsBackup/ortho.js +++ /dev/null @@ -1,91 +0,0 @@ -import adjust_lon from '../common/adjust_lon'; -import asinz from '../common/asinz'; -import {EPSLN, HALF_PI} from '../constants/values'; - -export function init() { - //double temp; /* temporary variable */ - - /* Place parameters in static storage for common use - -------------------------------------------------*/ - this.sin_p14 = Math.sin(this.lat0); - this.cos_p14 = Math.cos(this.lat0); -} - -/* Orthographic forward equations--mapping lat,long to x,y - ---------------------------------------------------*/ -export function forward(p) { - var sinphi, cosphi; /* sin and cos value */ - var dlon; /* delta longitude value */ - var coslon; /* cos of longitude */ - var ksp; /* scale factor */ - var g, x, y; - var lon = p.x; - var lat = p.y; - /* Forward equations - -----------------*/ - dlon = adjust_lon(lon - this.long0); - - sinphi = Math.sin(lat); - cosphi = Math.cos(lat); - - coslon = Math.cos(dlon); - g = this.sin_p14 * sinphi + this.cos_p14 * cosphi * coslon; - ksp = 1; - if ((g > 0) || (Math.abs(g) <= EPSLN)) { - x = this.a * ksp * cosphi * Math.sin(dlon); - y = this.y0 + this.a * ksp * (this.cos_p14 * sinphi - this.sin_p14 * cosphi * coslon); - } - p.x = x; - p.y = y; - return p; -} - -export function inverse(p) { - var rh; /* height above ellipsoid */ - var z; /* angle */ - var sinz, cosz; /* sin of z and cos of z */ - var con; - var lon, lat; - /* Inverse equations - -----------------*/ - p.x -= this.x0; - p.y -= this.y0; - rh = Math.sqrt(p.x * p.x + p.y * p.y); - z = asinz(rh / this.a); - - sinz = Math.sin(z); - cosz = Math.cos(z); - - lon = this.long0; - if (Math.abs(rh) <= EPSLN) { - lat = this.lat0; - p.x = lon; - p.y = lat; - return p; - } - lat = asinz(cosz * this.sin_p14 + (p.y * sinz * this.cos_p14) / rh); - con = Math.abs(this.lat0) - HALF_PI; - if (Math.abs(con) <= EPSLN) { - if (this.lat0 >= 0) { - lon = adjust_lon(this.long0 + Math.atan2(p.x, - p.y)); - } - else { - lon = adjust_lon(this.long0 - Math.atan2(-p.x, p.y)); - } - p.x = lon; - p.y = lat; - return p; - } - lon = adjust_lon(this.long0 + Math.atan2((p.x * sinz), rh * this.cos_p14 * cosz - p.y * this.sin_p14 * sinz)); - p.x = lon; - p.y = lat; - return p; -} - -export var names = ["ortho"]; -export default { - init: init, - forward: forward, - inverse: inverse, - names: names -}; diff --git a/src/proj4/projectionsBackup/poly.js b/src/proj4/projectionsBackup/poly.js deleted file mode 100644 index be6f77bf..00000000 --- a/src/proj4/projectionsBackup/poly.js +++ /dev/null @@ -1,135 +0,0 @@ -import e0fn from '../common/e0fn'; -import e1fn from '../common/e1fn'; -import e2fn from '../common/e2fn'; -import e3fn from '../common/e3fn'; -import adjust_lon from '../common/adjust_lon'; -import adjust_lat from '../common/adjust_lat'; -import mlfn from '../common/mlfn'; -import {EPSLN} from '../constants/values'; - -import gN from '../common/gN'; -var MAX_ITER = 20; - -export function init() { - /* Place parameters in static storage for common use - -------------------------------------------------*/ - this.temp = this.b / this.a; - this.es = 1 - Math.pow(this.temp, 2); // devait etre dans tmerc.js mais n y est pas donc je commente sinon retour de valeurs nulles - this.e = Math.sqrt(this.es); - this.e0 = e0fn(this.es); - this.e1 = e1fn(this.es); - this.e2 = e2fn(this.es); - this.e3 = e3fn(this.es); - this.ml0 = this.a * mlfn(this.e0, this.e1, this.e2, this.e3, this.lat0); //si que des zeros le calcul ne se fait pas -} - -/* Polyconic forward equations--mapping lat,long to x,y - ---------------------------------------------------*/ -export function forward(p) { - var lon = p.x; - var lat = p.y; - var x, y, el; - var dlon = adjust_lon(lon - this.long0); - el = dlon * Math.sin(lat); - if (this.sphere) { - if (Math.abs(lat) <= EPSLN) { - x = this.a * dlon; - y = -1 * this.a * this.lat0; - } - else { - x = this.a * Math.sin(el) / Math.tan(lat); - y = this.a * (adjust_lat(lat - this.lat0) + (1 - Math.cos(el)) / Math.tan(lat)); - } - } - else { - if (Math.abs(lat) <= EPSLN) { - x = this.a * dlon; - y = -1 * this.ml0; - } - else { - var nl = gN(this.a, this.e, Math.sin(lat)) / Math.tan(lat); - x = nl * Math.sin(el); - y = this.a * mlfn(this.e0, this.e1, this.e2, this.e3, lat) - this.ml0 + nl * (1 - Math.cos(el)); - } - - } - p.x = x + this.x0; - p.y = y + this.y0; - return p; -} - -/* Inverse equations - -----------------*/ -export function inverse(p) { - var lon, lat, x, y, i; - var al, bl; - var phi, dphi; - x = p.x - this.x0; - y = p.y - this.y0; - - if (this.sphere) { - if (Math.abs(y + this.a * this.lat0) <= EPSLN) { - lon = adjust_lon(x / this.a + this.long0); - lat = 0; - } - else { - al = this.lat0 + y / this.a; - bl = x * x / this.a / this.a + al * al; - phi = al; - var tanphi; - for (i = MAX_ITER; i; --i) { - tanphi = Math.tan(phi); - dphi = -1 * (al * (phi * tanphi + 1) - phi - 0.5 * (phi * phi + bl) * tanphi) / ((phi - al) / tanphi - 1); - phi += dphi; - if (Math.abs(dphi) <= EPSLN) { - lat = phi; - break; - } - } - lon = adjust_lon(this.long0 + (Math.asin(x * Math.tan(phi) / this.a)) / Math.sin(lat)); - } - } - else { - if (Math.abs(y + this.ml0) <= EPSLN) { - lat = 0; - lon = adjust_lon(this.long0 + x / this.a); - } - else { - - al = (this.ml0 + y) / this.a; - bl = x * x / this.a / this.a + al * al; - phi = al; - var cl, mln, mlnp, ma; - var con; - for (i = MAX_ITER; i; --i) { - con = this.e * Math.sin(phi); - cl = Math.sqrt(1 - con * con) * Math.tan(phi); - mln = this.a * mlfn(this.e0, this.e1, this.e2, this.e3, phi); - mlnp = this.e0 - 2 * this.e1 * Math.cos(2 * phi) + 4 * this.e2 * Math.cos(4 * phi) - 6 * this.e3 * Math.cos(6 * phi); - ma = mln / this.a; - dphi = (al * (cl * ma + 1) - ma - 0.5 * cl * (ma * ma + bl)) / (this.es * Math.sin(2 * phi) * (ma * ma + bl - 2 * al * ma) / (4 * cl) + (al - ma) * (cl * mlnp - 2 / Math.sin(2 * phi)) - mlnp); - phi -= dphi; - if (Math.abs(dphi) <= EPSLN) { - lat = phi; - break; - } - } - - //lat=phi4z(this.e,this.e0,this.e1,this.e2,this.e3,al,bl,0,0); - cl = Math.sqrt(1 - this.es * Math.pow(Math.sin(lat), 2)) * Math.tan(lat); - lon = adjust_lon(this.long0 + Math.asin(x * cl / this.a) / Math.sin(lat)); - } - } - - p.x = lon; - p.y = lat; - return p; -} - -export var names = ["Polyconic", "poly"]; -export default { - init: init, - forward: forward, - inverse: inverse, - names: names -}; diff --git a/src/proj4/projectionsBackup/qsc.js b/src/proj4/projectionsBackup/qsc.js deleted file mode 100644 index 1ef5789d..00000000 --- a/src/proj4/projectionsBackup/qsc.js +++ /dev/null @@ -1,368 +0,0 @@ -// 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, QUART_PI} 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 - QUART_PI / 2.0) { - this.face = FACE_ENUM.TOP; - } else if (this.lat0 <= -(HALF_PI - QUART_PI / 2.0)) { - this.face = FACE_ENUM.BOTTOM; - } else if (Math.abs(this.long0) <= QUART_PI) { - this.face = FACE_ENUM.FRONT; - } else if (Math.abs(this.long0) <= HALF_PI + QUART_PI) { - 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 >= QUART_PI && lon <= HALF_PI + QUART_PI) { - area.value = AREA_ENUM.AREA_0; - theta = lon - HALF_PI; - } else if (lon > HALF_PI + QUART_PI || lon <= -(HALF_PI + QUART_PI)) { - area.value = AREA_ENUM.AREA_1; - theta = (lon > 0.0 ? lon - SPI : lon + SPI); - } else if (lon > -(HALF_PI + QUART_PI) && lon <= -QUART_PI) { - 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 >= QUART_PI && lon <= HALF_PI + QUART_PI) { - area.value = AREA_ENUM.AREA_0; - theta = -lon + HALF_PI; - } else if (lon < QUART_PI && lon >= -QUART_PI) { - area.value = AREA_ENUM.AREA_1; - theta = -lon; - } else if (lon < -QUART_PI && lon >= -(HALF_PI + QUART_PI)) { - 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(QUART_PI)) - 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) <= QUART_PI) { - area.value = AREA_ENUM.AREA_0; - } else if (theta > QUART_PI && theta <= HALF_PI + QUART_PI) { - area.value = AREA_ENUM.AREA_1; - theta -= HALF_PI; - } else if (theta > HALF_PI + QUART_PI || theta <= -(HALF_PI + QUART_PI)) { - 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 -}; - diff --git a/src/proj4/projectionsBackup/robin.js b/src/proj4/projectionsBackup/robin.js deleted file mode 100644 index f335bd7a..00000000 --- a/src/proj4/projectionsBackup/robin.js +++ /dev/null @@ -1,161 +0,0 @@ -// Robinson projection -// Based on https://github.com/OSGeo/proj.4/blob/master/src/PJ_robin.c -// Polynomial coeficients from http://article.gmane.org/gmane.comp.gis.proj-4.devel/6039 - -import {HALF_PI, D2R, R2D, EPSLN} from '../constants/values'; -import adjust_lon from '../common/adjust_lon'; - -var COEFS_X = [ - [1.0000, 2.2199e-17, -7.15515e-05, 3.1103e-06], - [0.9986, -0.000482243, -2.4897e-05, -1.3309e-06], - [0.9954, -0.00083103, -4.48605e-05, -9.86701e-07], - [0.9900, -0.00135364, -5.9661e-05, 3.6777e-06], - [0.9822, -0.00167442, -4.49547e-06, -5.72411e-06], - [0.9730, -0.00214868, -9.03571e-05, 1.8736e-08], - [0.9600, -0.00305085, -9.00761e-05, 1.64917e-06], - [0.9427, -0.00382792, -6.53386e-05, -2.6154e-06], - [0.9216, -0.00467746, -0.00010457, 4.81243e-06], - [0.8962, -0.00536223, -3.23831e-05, -5.43432e-06], - [0.8679, -0.00609363, -0.000113898, 3.32484e-06], - [0.8350, -0.00698325, -6.40253e-05, 9.34959e-07], - [0.7986, -0.00755338, -5.00009e-05, 9.35324e-07], - [0.7597, -0.00798324, -3.5971e-05, -2.27626e-06], - [0.7186, -0.00851367, -7.01149e-05, -8.6303e-06], - [0.6732, -0.00986209, -0.000199569, 1.91974e-05], - [0.6213, -0.010418, 8.83923e-05, 6.24051e-06], - [0.5722, -0.00906601, 0.000182, 6.24051e-06], - [0.5322, -0.00677797, 0.000275608, 6.24051e-06] -]; - -var COEFS_Y = [ - [-5.20417e-18, 0.0124, 1.21431e-18, -8.45284e-11], - [0.0620, 0.0124, -1.26793e-09, 4.22642e-10], - [0.1240, 0.0124, 5.07171e-09, -1.60604e-09], - [0.1860, 0.0123999, -1.90189e-08, 6.00152e-09], - [0.2480, 0.0124002, 7.10039e-08, -2.24e-08], - [0.3100, 0.0123992, -2.64997e-07, 8.35986e-08], - [0.3720, 0.0124029, 9.88983e-07, -3.11994e-07], - [0.4340, 0.0123893, -3.69093e-06, -4.35621e-07], - [0.4958, 0.0123198, -1.02252e-05, -3.45523e-07], - [0.5571, 0.0121916, -1.54081e-05, -5.82288e-07], - [0.6176, 0.0119938, -2.41424e-05, -5.25327e-07], - [0.6769, 0.011713, -3.20223e-05, -5.16405e-07], - [0.7346, 0.0113541, -3.97684e-05, -6.09052e-07], - [0.7903, 0.0109107, -4.89042e-05, -1.04739e-06], - [0.8435, 0.0103431, -6.4615e-05, -1.40374e-09], - [0.8936, 0.00969686, -6.4636e-05, -8.547e-06], - [0.9394, 0.00840947, -0.000192841, -4.2106e-06], - [0.9761, 0.00616527, -0.000256, -4.2106e-06], - [1.0000, 0.00328947, -0.000319159, -4.2106e-06] -]; - -var FXC = 0.8487; -var FYC = 1.3523; -var C1 = R2D/5; // rad to 5-degree interval -var RC1 = 1/C1; -var NODES = 18; - -var poly3_val = function(coefs, x) { - return coefs[0] + x * (coefs[1] + x * (coefs[2] + x * coefs[3])); -}; - -var poly3_der = function(coefs, x) { - return coefs[1] + x * (2 * coefs[2] + x * 3 * coefs[3]); -}; - -function newton_rapshon(f_df, start, max_err, iters) { - var x = start; - for (; iters; --iters) { - var upd = f_df(x); - x -= upd; - if (Math.abs(upd) < max_err) { - break; - } - } - return x; -} - -export function init() { - this.x0 = this.x0 || 0; - this.y0 = this.y0 || 0; - this.long0 = this.long0 || 0; - this.es = 0; - this.title = this.title || "Robinson"; -} - -export function forward(ll) { - var lon = adjust_lon(ll.x - this.long0); - - var dphi = Math.abs(ll.y); - var i = Math.floor(dphi * C1); - if (i < 0) { - i = 0; - } else if (i >= NODES) { - i = NODES - 1; - } - dphi = R2D * (dphi - RC1 * i); - var xy = { - x: poly3_val(COEFS_X[i], dphi) * lon, - y: poly3_val(COEFS_Y[i], dphi) - }; - if (ll.y < 0) { - xy.y = -xy.y; - } - - xy.x = xy.x * this.a * FXC + this.x0; - xy.y = xy.y * this.a * FYC + this.y0; - return xy; -} - -export function inverse(xy) { - var ll = { - x: (xy.x - this.x0) / (this.a * FXC), - y: Math.abs(xy.y - this.y0) / (this.a * FYC) - }; - - if (ll.y >= 1) { // pathologic case - ll.x /= COEFS_X[NODES][0]; - ll.y = xy.y < 0 ? -HALF_PI : HALF_PI; - } else { - // find table interval - var i = Math.floor(ll.y * NODES); - if (i < 0) { - i = 0; - } else if (i >= NODES) { - i = NODES - 1; - } - for (;;) { - if (COEFS_Y[i][0] > ll.y) { - --i; - } else if (COEFS_Y[i+1][0] <= ll.y) { - ++i; - } else { - break; - } - } - // linear interpolation in 5 degree interval - var coefs = COEFS_Y[i]; - var t = 5 * (ll.y - coefs[0]) / (COEFS_Y[i+1][0] - coefs[0]); - // find t so that poly3_val(coefs, t) = ll.y - t = newton_rapshon(function(x) { - return (poly3_val(coefs, x) - ll.y) / poly3_der(coefs, x); - }, t, EPSLN, 100); - - ll.x /= poly3_val(COEFS_X[i], t); - ll.y = (5 * i + t) * D2R; - if (xy.y < 0) { - ll.y = -ll.y; - } - } - - ll.x = adjust_lon(ll.x + this.long0); - return ll; -} - -export var names = ["Robinson", "robin"]; -export default { - init: init, - forward: forward, - inverse: inverse, - names: names -}; diff --git a/src/proj4/projectionsBackup/sinu.js b/src/proj4/projectionsBackup/sinu.js deleted file mode 100644 index 7ff57637..00000000 --- a/src/proj4/projectionsBackup/sinu.js +++ /dev/null @@ -1,115 +0,0 @@ -import adjust_lon from '../common/adjust_lon'; -import adjust_lat from '../common/adjust_lat'; -import pj_enfn from '../common/pj_enfn'; -var MAX_ITER = 20; -import pj_mlfn from '../common/pj_mlfn'; -import pj_inv_mlfn from '../common/pj_inv_mlfn'; -import {EPSLN, HALF_PI} from '../constants/values'; - -import asinz from '../common/asinz'; - - -export function init() { - /* Place parameters in static storage for common use - -------------------------------------------------*/ - - - if (!this.sphere) { - this.en = pj_enfn(this.es); - } - else { - this.n = 1; - this.m = 0; - this.es = 0; - this.C_y = Math.sqrt((this.m + 1) / this.n); - this.C_x = this.C_y / (this.m + 1); - } - -} - -/* Sinusoidal forward equations--mapping lat,long to x,y - -----------------------------------------------------*/ -export function forward(p) { - var x, y; - var lon = p.x; - var lat = p.y; - /* Forward equations - -----------------*/ - lon = adjust_lon(lon - this.long0); - - if (this.sphere) { - if (!this.m) { - lat = this.n !== 1 ? Math.asin(this.n * Math.sin(lat)) : lat; - } - else { - var k = this.n * Math.sin(lat); - for (var i = MAX_ITER; i; --i) { - var V = (this.m * lat + Math.sin(lat) - k) / (this.m + Math.cos(lat)); - lat -= V; - if (Math.abs(V) < EPSLN) { - break; - } - } - } - x = this.a * this.C_x * lon * (this.m + Math.cos(lat)); - y = this.a * this.C_y * lat; - - } - else { - - var s = Math.sin(lat); - var c = Math.cos(lat); - y = this.a * pj_mlfn(lat, s, c, this.en); - x = this.a * lon * c / Math.sqrt(1 - this.es * s * s); - } - - p.x = x; - p.y = y; - return p; -} - -export function inverse(p) { - var lat, temp, lon, s; - - p.x -= this.x0; - lon = p.x / this.a; - p.y -= this.y0; - lat = p.y / this.a; - - if (this.sphere) { - lat /= this.C_y; - lon = lon / (this.C_x * (this.m + Math.cos(lat))); - if (this.m) { - lat = asinz((this.m * lat + Math.sin(lat)) / this.n); - } - else if (this.n !== 1) { - lat = asinz(Math.sin(lat) / this.n); - } - lon = adjust_lon(lon + this.long0); - lat = adjust_lat(lat); - } - else { - lat = pj_inv_mlfn(p.y / this.a, this.es, this.en); - s = Math.abs(lat); - if (s < HALF_PI) { - s = Math.sin(lat); - temp = this.long0 + p.x * Math.sqrt(1 - this.es * s * s) / (this.a * Math.cos(lat)); - //temp = this.long0 + p.x / (this.a * Math.cos(lat)); - lon = adjust_lon(temp); - } - else if ((s - EPSLN) < HALF_PI) { - lon = this.long0; - } - } - p.x = lon; - p.y = lat; - return p; -} - -export var names = ["Sinusoidal", "sinu"]; -export default { - init: init, - forward: forward, - inverse: inverse, - names: names -}; diff --git a/src/proj4/projectionsBackup/somerc.js b/src/proj4/projectionsBackup/somerc.js deleted file mode 100644 index 18cdccb6..00000000 --- a/src/proj4/projectionsBackup/somerc.js +++ /dev/null @@ -1,86 +0,0 @@ -/* - references: - Formules et constantes pour le Calcul pour la - projection cylindrique conforme à axe oblique et pour la transformation entre - des systèmes de référence. - http://www.swisstopo.admin.ch/internet/swisstopo/fr/home/topics/survey/sys/refsys/switzerland.parsysrelated1.31216.downloadList.77004.DownloadFile.tmp/swissprojectionfr.pdf - */ - -export function init() { - var phy0 = this.lat0; - this.lambda0 = this.long0; - var sinPhy0 = Math.sin(phy0); - var semiMajorAxis = this.a; - var invF = this.rf; - var flattening = 1 / invF; - var e2 = 2 * flattening - Math.pow(flattening, 2); - var e = this.e = Math.sqrt(e2); - this.R = this.k0 * semiMajorAxis * Math.sqrt(1 - e2) / (1 - e2 * Math.pow(sinPhy0, 2)); - this.alpha = Math.sqrt(1 + e2 / (1 - e2) * Math.pow(Math.cos(phy0), 4)); - this.b0 = Math.asin(sinPhy0 / this.alpha); - var k1 = Math.log(Math.tan(Math.PI / 4 + this.b0 / 2)); - var k2 = Math.log(Math.tan(Math.PI / 4 + phy0 / 2)); - var k3 = Math.log((1 + e * sinPhy0) / (1 - e * sinPhy0)); - this.K = k1 - this.alpha * k2 + this.alpha * e / 2 * k3; -} - -export function forward(p) { - var Sa1 = Math.log(Math.tan(Math.PI / 4 - p.y / 2)); - var Sa2 = this.e / 2 * Math.log((1 + this.e * Math.sin(p.y)) / (1 - this.e * Math.sin(p.y))); - var S = -this.alpha * (Sa1 + Sa2) + this.K; - - // spheric latitude - var b = 2 * (Math.atan(Math.exp(S)) - Math.PI / 4); - - // spheric longitude - var I = this.alpha * (p.x - this.lambda0); - - // psoeudo equatorial rotation - var rotI = Math.atan(Math.sin(I) / (Math.sin(this.b0) * Math.tan(b) + Math.cos(this.b0) * Math.cos(I))); - - var rotB = Math.asin(Math.cos(this.b0) * Math.sin(b) - Math.sin(this.b0) * Math.cos(b) * Math.cos(I)); - - p.y = this.R / 2 * Math.log((1 + Math.sin(rotB)) / (1 - Math.sin(rotB))) + this.y0; - p.x = this.R * rotI + this.x0; - return p; -} - -export function inverse(p) { - var Y = p.x - this.x0; - var X = p.y - this.y0; - - var rotI = Y / this.R; - var rotB = 2 * (Math.atan(Math.exp(X / this.R)) - Math.PI / 4); - - var b = Math.asin(Math.cos(this.b0) * Math.sin(rotB) + Math.sin(this.b0) * Math.cos(rotB) * Math.cos(rotI)); - var I = Math.atan(Math.sin(rotI) / (Math.cos(this.b0) * Math.cos(rotI) - Math.sin(this.b0) * Math.tan(rotB))); - - var lambda = this.lambda0 + I / this.alpha; - - var S = 0; - var phy = b; - var prevPhy = -1000; - var iteration = 0; - while (Math.abs(phy - prevPhy) > 0.0000001) { - if (++iteration > 20) { - //...reportError("omercFwdInfinity"); - return; - } - //S = Math.log(Math.tan(Math.PI / 4 + phy / 2)); - S = 1 / this.alpha * (Math.log(Math.tan(Math.PI / 4 + b / 2)) - this.K) + this.e * Math.log(Math.tan(Math.PI / 4 + Math.asin(this.e * Math.sin(phy)) / 2)); - prevPhy = phy; - phy = 2 * Math.atan(Math.exp(S)) - Math.PI / 2; - } - - p.x = lambda; - p.y = phy; - return p; -} - -export var names = ["somerc"]; -export default { - init: init, - forward: forward, - inverse: inverse, - names: names -}; diff --git a/src/proj4/projectionsBackup/stere.js b/src/proj4/projectionsBackup/stere.js deleted file mode 100644 index 55bc1d6e..00000000 --- a/src/proj4/projectionsBackup/stere.js +++ /dev/null @@ -1,183 +0,0 @@ -import {EPSLN, HALF_PI} from '../constants/values'; - -import sign from '../common/sign'; -import msfnz from '../common/msfnz'; -import tsfnz from '../common/tsfnz'; -import phi2z from '../common/phi2z'; -import adjust_lon from '../common/adjust_lon'; - -export function ssfn_(phit, sinphi, eccen) { - sinphi *= eccen; - return (Math.tan(0.5 * (HALF_PI + phit)) * Math.pow((1 - sinphi) / (1 + sinphi), 0.5 * eccen)); -} - -export function init() { - - // setting default parameters - this.x0 = this.x0 || 0; - this.y0 = this.y0 || 0; - this.lat0 = this.lat0 || 0; - this.long0 = this.long0 || 0; - - this.coslat0 = Math.cos(this.lat0); - this.sinlat0 = Math.sin(this.lat0); - if (this.sphere) { - if (this.k0 === 1 && !isNaN(this.lat_ts) && Math.abs(this.coslat0) <= EPSLN) { - this.k0 = 0.5 * (1 + sign(this.lat0) * Math.sin(this.lat_ts)); - } - } - else { - if (Math.abs(this.coslat0) <= EPSLN) { - if (this.lat0 > 0) { - //North pole - //trace('stere:north pole'); - this.con = 1; - } - else { - //South pole - //trace('stere:south pole'); - this.con = -1; - } - } - this.cons = Math.sqrt(Math.pow(1 + this.e, 1 + this.e) * Math.pow(1 - this.e, 1 - this.e)); - if (this.k0 === 1 && !isNaN(this.lat_ts) && Math.abs(this.coslat0) <= EPSLN && Math.abs(Math.cos(this.lat_ts)) > EPSLN) { - // When k0 is 1 (default value) and lat_ts is a vaild number and lat0 is at a pole and lat_ts is not at a pole - // Recalculate k0 using formula 21-35 from p161 of Snyder, 1987 - this.k0 = 0.5 * this.cons * msfnz(this.e, Math.sin(this.lat_ts), Math.cos(this.lat_ts)) / tsfnz(this.e, this.con * this.lat_ts, this.con * Math.sin(this.lat_ts)); - } - this.ms1 = msfnz(this.e, this.sinlat0, this.coslat0); - this.X0 = 2 * Math.atan(this.ssfn_(this.lat0, this.sinlat0, this.e)) - HALF_PI; - this.cosX0 = Math.cos(this.X0); - this.sinX0 = Math.sin(this.X0); - } -} - -// Stereographic forward equations--mapping lat,long to x,y -export function forward(p) { - var lon = p.x; - var lat = p.y; - var sinlat = Math.sin(lat); - var coslat = Math.cos(lat); - var A, X, sinX, cosX, ts, rh; - var dlon = adjust_lon(lon - this.long0); - - if (Math.abs(Math.abs(lon - this.long0) - Math.PI) <= EPSLN && Math.abs(lat + this.lat0) <= EPSLN) { - //case of the origine point - //trace('stere:this is the origin point'); - p.x = NaN; - p.y = NaN; - return p; - } - if (this.sphere) { - //trace('stere:sphere case'); - A = 2 * this.k0 / (1 + this.sinlat0 * sinlat + this.coslat0 * coslat * Math.cos(dlon)); - p.x = this.a * A * coslat * Math.sin(dlon) + this.x0; - p.y = this.a * A * (this.coslat0 * sinlat - this.sinlat0 * coslat * Math.cos(dlon)) + this.y0; - return p; - } - else { - X = 2 * Math.atan(this.ssfn_(lat, sinlat, this.e)) - HALF_PI; - cosX = Math.cos(X); - sinX = Math.sin(X); - if (Math.abs(this.coslat0) <= EPSLN) { - ts = tsfnz(this.e, lat * this.con, this.con * sinlat); - rh = 2 * this.a * this.k0 * ts / this.cons; - p.x = this.x0 + rh * Math.sin(lon - this.long0); - p.y = this.y0 - this.con * rh * Math.cos(lon - this.long0); - //trace(p.toString()); - return p; - } - else if (Math.abs(this.sinlat0) < EPSLN) { - //Eq - //trace('stere:equateur'); - A = 2 * this.a * this.k0 / (1 + cosX * Math.cos(dlon)); - p.y = A * sinX; - } - else { - //other case - //trace('stere:normal case'); - A = 2 * this.a * this.k0 * this.ms1 / (this.cosX0 * (1 + this.sinX0 * sinX + this.cosX0 * cosX * Math.cos(dlon))); - p.y = A * (this.cosX0 * sinX - this.sinX0 * cosX * Math.cos(dlon)) + this.y0; - } - p.x = A * cosX * Math.sin(dlon) + this.x0; - } - //trace(p.toString()); - return p; -} - -//* Stereographic inverse equations--mapping x,y to lat/long -export function inverse(p) { - p.x -= this.x0; - p.y -= this.y0; - var lon, lat, ts, ce, Chi; - var rh = Math.sqrt(p.x * p.x + p.y * p.y); - if (this.sphere) { - var c = 2 * Math.atan(rh / (2 * this.a * this.k0)); - lon = this.long0; - lat = this.lat0; - if (rh <= EPSLN) { - p.x = lon; - p.y = lat; - return p; - } - lat = Math.asin(Math.cos(c) * this.sinlat0 + p.y * Math.sin(c) * this.coslat0 / rh); - if (Math.abs(this.coslat0) < EPSLN) { - if (this.lat0 > 0) { - lon = adjust_lon(this.long0 + Math.atan2(p.x, - 1 * p.y)); - } - else { - lon = adjust_lon(this.long0 + Math.atan2(p.x, p.y)); - } - } - else { - lon = adjust_lon(this.long0 + Math.atan2(p.x * Math.sin(c), rh * this.coslat0 * Math.cos(c) - p.y * this.sinlat0 * Math.sin(c))); - } - p.x = lon; - p.y = lat; - return p; - } - else { - if (Math.abs(this.coslat0) <= EPSLN) { - if (rh <= EPSLN) { - lat = this.lat0; - lon = this.long0; - p.x = lon; - p.y = lat; - //trace(p.toString()); - return p; - } - p.x *= this.con; - p.y *= this.con; - ts = rh * this.cons / (2 * this.a * this.k0); - lat = this.con * phi2z(this.e, ts); - lon = this.con * adjust_lon(this.con * this.long0 + Math.atan2(p.x, - 1 * p.y)); - } - else { - ce = 2 * Math.atan(rh * this.cosX0 / (2 * this.a * this.k0 * this.ms1)); - lon = this.long0; - if (rh <= EPSLN) { - Chi = this.X0; - } - else { - Chi = Math.asin(Math.cos(ce) * this.sinX0 + p.y * Math.sin(ce) * this.cosX0 / rh); - lon = adjust_lon(this.long0 + Math.atan2(p.x * Math.sin(ce), rh * this.cosX0 * Math.cos(ce) - p.y * this.sinX0 * Math.sin(ce))); - } - lat = -1 * phi2z(this.e, Math.tan(0.5 * (HALF_PI + Chi))); - } - } - p.x = lon; - p.y = lat; - - //trace(p.toString()); - return p; - -} - -export var names = ["stere", "Stereographic_South_Pole", "Polar Stereographic (variant B)", "Polar_Stereographic"]; -export default { - init: init, - forward: forward, - inverse: inverse, - names: names, - ssfn_: ssfn_ -}; diff --git a/src/proj4/projectionsBackup/sterea.js b/src/proj4/projectionsBackup/sterea.js deleted file mode 100644 index a90f43c9..00000000 --- a/src/proj4/projectionsBackup/sterea.js +++ /dev/null @@ -1,65 +0,0 @@ -import gauss from './gauss'; -import adjust_lon from '../common/adjust_lon'; -import hypot from '../common/hypot'; - -export function init() { - gauss.init.apply(this); - if (!this.rc) { - return; - } - this.sinc0 = Math.sin(this.phic0); - this.cosc0 = Math.cos(this.phic0); - this.R2 = 2 * this.rc; - if (!this.title) { - this.title = "Oblique Stereographic Alternative"; - } -} - -export function forward(p) { - var sinc, cosc, cosl, k; - p.x = adjust_lon(p.x - this.long0); - gauss.forward.apply(this, [p]); - sinc = Math.sin(p.y); - cosc = Math.cos(p.y); - cosl = Math.cos(p.x); - k = this.k0 * this.R2 / (1 + this.sinc0 * sinc + this.cosc0 * cosc * cosl); - p.x = k * cosc * Math.sin(p.x); - p.y = k * (this.cosc0 * sinc - this.sinc0 * cosc * cosl); - p.x = this.a * p.x + this.x0; - p.y = this.a * p.y + this.y0; - return p; -} - -export function inverse(p) { - var sinc, cosc, lon, lat, rho; - p.x = (p.x - this.x0) / this.a; - p.y = (p.y - this.y0) / this.a; - - p.x /= this.k0; - p.y /= this.k0; - if ((rho = hypot(p.x, p.y))) { - var c = 2 * Math.atan2(rho, this.R2); - sinc = Math.sin(c); - cosc = Math.cos(c); - lat = Math.asin(cosc * this.sinc0 + p.y * sinc * this.cosc0 / rho); - lon = Math.atan2(p.x * sinc, rho * this.cosc0 * cosc - p.y * this.sinc0 * sinc); - } - else { - lat = this.phic0; - lon = 0; - } - - p.x = lon; - p.y = lat; - gauss.inverse.apply(this, [p]); - p.x = adjust_lon(p.x + this.long0); - return p; -} - -export var names = ["Stereographic_North_Pole", "Oblique_Stereographic", "sterea","Oblique Stereographic Alternative","Double_Stereographic"]; -export default { - init: init, - forward: forward, - inverse: inverse, - names: names -}; diff --git a/src/proj4/projectionsBackup/tmerc.js b/src/proj4/projectionsBackup/tmerc.js deleted file mode 100644 index 738c3927..00000000 --- a/src/proj4/projectionsBackup/tmerc.js +++ /dev/null @@ -1,173 +0,0 @@ -// Heavily based on this tmerc projection implementation -// https://github.com/mbloch/mapshaper-proj/blob/master/src/projections/tmerc.js - -import pj_enfn from '../common/pj_enfn'; -import pj_mlfn from '../common/pj_mlfn'; -import pj_inv_mlfn from '../common/pj_inv_mlfn'; -import adjust_lon from '../common/adjust_lon'; - -import {EPSLN, HALF_PI} from '../constants/values'; -import sign from '../common/sign'; - -export function init() { - this.x0 = this.x0 !== undefined ? this.x0 : 0; - this.y0 = this.y0 !== undefined ? this.y0 : 0; - this.long0 = this.long0 !== undefined ? this.long0 : 0; - this.lat0 = this.lat0 !== undefined ? this.lat0 : 0; - - if (this.es) { - this.en = pj_enfn(this.es); - this.ml0 = pj_mlfn(this.lat0, Math.sin(this.lat0), Math.cos(this.lat0), this.en); - } -} - -/** - Transverse Mercator Forward - long/lat to x/y - long/lat in radians - */ -export function forward(p) { - var lon = p.x; - var lat = p.y; - - var delta_lon = adjust_lon(lon - this.long0); - var con; - var x, y; - var sin_phi = Math.sin(lat); - var cos_phi = Math.cos(lat); - - if (!this.es) { - var b = cos_phi * Math.sin(delta_lon); - - if ((Math.abs(Math.abs(b) - 1)) < EPSLN) { - return (93); - } - else { - x = 0.5 * this.a * this.k0 * Math.log((1 + b) / (1 - b)) + this.x0; - y = cos_phi * Math.cos(delta_lon) / Math.sqrt(1 - Math.pow(b, 2)); - b = Math.abs(y); - - if (b >= 1) { - if ((b - 1) > EPSLN) { - return (93); - } - else { - y = 0; - } - } - else { - y = Math.acos(y); - } - - if (lat < 0) { - y = -y; - } - - y = this.a * this.k0 * (y - this.lat0) + this.y0; - } - } - else { - var al = cos_phi * delta_lon; - var als = Math.pow(al, 2); - var c = this.ep2 * Math.pow(cos_phi, 2); - var cs = Math.pow(c, 2); - var tq = Math.abs(cos_phi) > EPSLN ? Math.tan(lat) : 0; - var t = Math.pow(tq, 2); - var ts = Math.pow(t, 2); - con = 1 - this.es * Math.pow(sin_phi, 2); - al = al / Math.sqrt(con); - var ml = pj_mlfn(lat, sin_phi, cos_phi, this.en); - - x = this.a * (this.k0 * al * (1 + - als / 6 * (1 - t + c + - als / 20 * (5 - 18 * t + ts + 14 * c - 58 * t * c + - als / 42 * (61 + 179 * ts - ts * t - 479 * t))))) + - this.x0; - - y = this.a * (this.k0 * (ml - this.ml0 + - sin_phi * delta_lon * al / 2 * (1 + - als / 12 * (5 - t + 9 * c + 4 * cs + - als / 30 * (61 + ts - 58 * t + 270 * c - 330 * t * c + - als / 56 * (1385 + 543 * ts - ts * t - 3111 * t)))))) + - this.y0; - } - - p.x = x; - p.y = y; - - return p; -} - -/** - Transverse Mercator Inverse - x/y to long/lat - */ -export function inverse(p) { - var con, phi; - var lat, lon; - var x = (p.x - this.x0) * (1 / this.a); - var y = (p.y - this.y0) * (1 / this.a); - - if (!this.es) { - var f = Math.exp(x / this.k0); - var g = 0.5 * (f - 1 / f); - var temp = this.lat0 + y / this.k0; - var h = Math.cos(temp); - con = Math.sqrt((1 - Math.pow(h, 2)) / (1 + Math.pow(g, 2))); - lat = Math.asin(con); - - if (y < 0) { - lat = -lat; - } - - if ((g === 0) && (h === 0)) { - lon = 0; - } - else { - lon = adjust_lon(Math.atan2(g, h) + this.long0); - } - } - else { // ellipsoidal form - con = this.ml0 + y / this.k0; - phi = pj_inv_mlfn(con, this.es, this.en); - - if (Math.abs(phi) < HALF_PI) { - var sin_phi = Math.sin(phi); - var cos_phi = Math.cos(phi); - var tan_phi = Math.abs(cos_phi) > EPSLN ? Math.tan(phi) : 0; - var c = this.ep2 * Math.pow(cos_phi, 2); - var cs = Math.pow(c, 2); - var t = Math.pow(tan_phi, 2); - var ts = Math.pow(t, 2); - con = 1 - this.es * Math.pow(sin_phi, 2); - var d = x * Math.sqrt(con) / this.k0; - var ds = Math.pow(d, 2); - con = con * tan_phi; - - lat = phi - (con * ds / (1 - this.es)) * 0.5 * (1 - - ds / 12 * (5 + 3 * t - 9 * c * t + c - 4 * cs - - ds / 30 * (61 + 90 * t - 252 * c * t + 45 * ts + 46 * c - - ds / 56 * (1385 + 3633 * t + 4095 * ts + 1574 * ts * t)))); - - lon = adjust_lon(this.long0 + (d * (1 - - ds / 6 * (1 + 2 * t + c - - ds / 20 * (5 + 28 * t + 24 * ts + 8 * c * t + 6 * c - - ds / 42 * (61 + 662 * t + 1320 * ts + 720 * ts * t)))) / cos_phi)); - } - else { - lat = HALF_PI * sign(y); - lon = 0; - } - } - - p.x = lon; - p.y = lat; - - return p; -} - -export var names = ["Fast_Transverse_Mercator", "Fast Transverse Mercator"]; -export default { - init: init, - forward: forward, - inverse: inverse, - names: names -}; diff --git a/src/proj4/projectionsBackup/tpers.js b/src/proj4/projectionsBackup/tpers.js deleted file mode 100644 index 45141429..00000000 --- a/src/proj4/projectionsBackup/tpers.js +++ /dev/null @@ -1,169 +0,0 @@ - -var mode = { - N_POLE: 0, - S_POLE: 1, - EQUIT: 2, - OBLIQ: 3 -}; - -import { D2R, HALF_PI, EPSLN } from "../constants/values"; -import hypot from "../common/hypot"; - -var params = { - h: { def: 100000, num: true }, // default is Karman line, no default in PROJ.7 - azi: { def: 0, num: true, degrees: true }, // default is North - tilt: { def: 0, num: true, degrees: true }, // default is Nadir - long0: { def: 0, num: true }, // default is Greenwich, conversion to rad is automatic - lat0: { def: 0, num: true } // default is Equator, conversion to rad is automatic -}; - -export function init() { - Object.keys(params).forEach(function (p) { - if (typeof this[p] === "undefined") { - this[p] = params[p].def; - } else if (params[p].num && isNaN(this[p])) { - throw new Error("Invalid parameter value, must be numeric " + p + " = " + this[p]); - } else if (params[p].num) { - this[p] = parseFloat(this[p]); - } - if (params[p].degrees) { - this[p] = this[p] * D2R; - } - }.bind(this)); - - if (Math.abs((Math.abs(this.lat0) - HALF_PI)) < EPSLN) { - this.mode = this.lat0 < 0 ? mode.S_POLE : mode.N_POLE; - } else if (Math.abs(this.lat0) < EPSLN) { - this.mode = mode.EQUIT; - } else { - this.mode = mode.OBLIQ; - this.sinph0 = Math.sin(this.lat0); - this.cosph0 = Math.cos(this.lat0); - } - - this.pn1 = this.h / this.a; // Normalize relative to the Earth's radius - - if (this.pn1 <= 0 || this.pn1 > 1e10) { - throw new Error("Invalid height"); - } - - this.p = 1 + this.pn1; - this.rp = 1 / this.p; - this.h1 = 1 / this.pn1; - this.pfact = (this.p + 1) * this.h1; - this.es = 0; - - var omega = this.tilt; - var gamma = this.azi; - this.cg = Math.cos(gamma); - this.sg = Math.sin(gamma); - this.cw = Math.cos(omega); - this.sw = Math.sin(omega); -} - -export function forward(p) { - p.x -= this.long0; - var sinphi = Math.sin(p.y); - var cosphi = Math.cos(p.y); - var coslam = Math.cos(p.x); - var x, y; - switch (this.mode) { - case mode.OBLIQ: - y = this.sinph0 * sinphi + this.cosph0 * cosphi * coslam; - break; - case mode.EQUIT: - y = cosphi * coslam; - break; - case mode.S_POLE: - y = -sinphi; - break; - case mode.N_POLE: - y = sinphi; - break; - } - y = this.pn1 / (this.p - y); - x = y * cosphi * Math.sin(p.x); - - switch (this.mode) { - case mode.OBLIQ: - y *= this.cosph0 * sinphi - this.sinph0 * cosphi * coslam; - break; - case mode.EQUIT: - y *= sinphi; - break; - case mode.N_POLE: - y *= -(cosphi * coslam); - break; - case mode.S_POLE: - y *= cosphi * coslam; - break; - } - - // Tilt - var yt, ba; - yt = y * this.cg + x * this.sg; - ba = 1 / (yt * this.sw * this.h1 + this.cw); - x = (x * this.cg - y * this.sg) * this.cw * ba; - y = yt * ba; - - p.x = x * this.a; - p.y = y * this.a; - return p; -} - -export function inverse(p) { - p.x /= this.a; - p.y /= this.a; - var r = { x: p.x, y: p.y }; - - // Un-Tilt - var bm, bq, yt; - yt = 1 / (this.pn1 - p.y * this.sw); - bm = this.pn1 * p.x * yt; - bq = this.pn1 * p.y * this.cw * yt; - p.x = bm * this.cg + bq * this.sg; - p.y = bq * this.cg - bm * this.sg; - - var rh = hypot(p.x, p.y); - if (Math.abs(rh) < EPSLN) { - r.x = 0; - r.y = p.y; - } else { - var cosz, sinz; - sinz = 1 - rh * rh * this.pfact; - sinz = (this.p - Math.sqrt(sinz)) / (this.pn1 / rh + rh / this.pn1); - cosz = Math.sqrt(1 - sinz * sinz); - switch (this.mode) { - case mode.OBLIQ: - r.y = Math.asin(cosz * this.sinph0 + p.y * sinz * this.cosph0 / rh); - p.y = (cosz - this.sinph0 * Math.sin(r.y)) * rh; - p.x *= sinz * this.cosph0; - break; - case mode.EQUIT: - r.y = Math.asin(p.y * sinz / rh); - p.y = cosz * rh; - p.x *= sinz; - break; - case mode.N_POLE: - r.y = Math.asin(cosz); - p.y = -p.y; - break; - case mode.S_POLE: - r.y = -Math.asin(cosz); - break; - } - r.x = Math.atan2(p.x, p.y); - } - - p.x = r.x + this.long0; - p.y = r.y; - return p; -} - -export var names = ["Tilted_Perspective", "tpers"]; -export default { - init: init, - forward: forward, - inverse: inverse, - names: names -}; diff --git a/src/proj4/projectionsBackup/utm.js b/src/proj4/projectionsBackup/utm.js deleted file mode 100644 index 5b126a00..00000000 --- a/src/proj4/projectionsBackup/utm.js +++ /dev/null @@ -1,28 +0,0 @@ -import adjust_zone from '../common/adjust_zone'; -import etmerc from './etmerc'; -export var dependsOn = 'etmerc'; -import {D2R} from '../constants/values'; - - -export function init() { - var zone = adjust_zone(this.zone, this.long0); - if (zone === undefined) { - throw new Error('unknown utm zone'); - } - this.lat0 = 0; - this.long0 = ((6 * Math.abs(zone)) - 183) * D2R; - this.x0 = 500000; - this.y0 = this.utmSouth ? 10000000 : 0; - this.k0 = 0.9996; - - etmerc.init.apply(this); - this.forward = etmerc.forward; - this.inverse = etmerc.inverse; -} - -export var names = ["Universal Transverse Mercator System", "utm"]; -export default { - init: init, - names: names, - dependsOn: dependsOn -}; diff --git a/src/proj4/projectionsBackup/vandg.js b/src/proj4/projectionsBackup/vandg.js deleted file mode 100644 index 1f1b5030..00000000 --- a/src/proj4/projectionsBackup/vandg.js +++ /dev/null @@ -1,129 +0,0 @@ -import adjust_lon from '../common/adjust_lon'; - -import {HALF_PI, EPSLN} from '../constants/values'; - -import asinz from '../common/asinz'; - -/* Initialize the Van Der Grinten projection - ----------------------------------------*/ -export function init() { - //this.R = 6370997; //Radius of earth - this.R = this.a; -} - -export function forward(p) { - - var lon = p.x; - var lat = p.y; - - /* Forward equations - -----------------*/ - var dlon = adjust_lon(lon - this.long0); - var x, y; - - if (Math.abs(lat) <= EPSLN) { - x = this.x0 + this.R * dlon; - y = this.y0; - } - var theta = asinz(2 * Math.abs(lat / Math.PI)); - if ((Math.abs(dlon) <= EPSLN) || (Math.abs(Math.abs(lat) - HALF_PI) <= EPSLN)) { - x = this.x0; - if (lat >= 0) { - y = this.y0 + Math.PI * this.R * Math.tan(0.5 * theta); - } - else { - y = this.y0 + Math.PI * this.R * -Math.tan(0.5 * theta); - } - // return(OK); - } - var al = 0.5 * Math.abs((Math.PI / dlon) - (dlon / Math.PI)); - var asq = al * al; - var sinth = Math.sin(theta); - var costh = Math.cos(theta); - - var g = costh / (sinth + costh - 1); - var gsq = g * g; - var m = g * (2 / sinth - 1); - var msq = m * m; - var con = Math.PI * this.R * (al * (g - msq) + Math.sqrt(asq * (g - msq) * (g - msq) - (msq + asq) * (gsq - msq))) / (msq + asq); - if (dlon < 0) { - con = -con; - } - x = this.x0 + con; - //con = Math.abs(con / (Math.PI * this.R)); - var q = asq + g; - con = Math.PI * this.R * (m * q - al * Math.sqrt((msq + asq) * (asq + 1) - q * q)) / (msq + asq); - if (lat >= 0) { - //y = this.y0 + Math.PI * this.R * Math.sqrt(1 - con * con - 2 * al * con); - y = this.y0 + con; - } - else { - //y = this.y0 - Math.PI * this.R * Math.sqrt(1 - con * con - 2 * al * con); - y = this.y0 - con; - } - p.x = x; - p.y = y; - return p; -} - -/* Van Der Grinten inverse equations--mapping x,y to lat/long - ---------------------------------------------------------*/ -export function inverse(p) { - var lon, lat; - var xx, yy, xys, c1, c2, c3; - var a1; - var m1; - var con; - var th1; - var d; - - /* inverse equations - -----------------*/ - p.x -= this.x0; - p.y -= this.y0; - con = Math.PI * this.R; - xx = p.x / con; - yy = p.y / con; - xys = xx * xx + yy * yy; - c1 = -Math.abs(yy) * (1 + xys); - c2 = c1 - 2 * yy * yy + xx * xx; - c3 = -2 * c1 + 1 + 2 * yy * yy + xys * xys; - d = yy * yy / c3 + (2 * c2 * c2 * c2 / c3 / c3 / c3 - 9 * c1 * c2 / c3 / c3) / 27; - a1 = (c1 - c2 * c2 / 3 / c3) / c3; - m1 = 2 * Math.sqrt(-a1 / 3); - con = ((3 * d) / a1) / m1; - if (Math.abs(con) > 1) { - if (con >= 0) { - con = 1; - } - else { - con = -1; - } - } - th1 = Math.acos(con) / 3; - if (p.y >= 0) { - lat = (-m1 * Math.cos(th1 + Math.PI / 3) - c2 / 3 / c3) * Math.PI; - } - else { - lat = -(-m1 * Math.cos(th1 + Math.PI / 3) - c2 / 3 / c3) * Math.PI; - } - - if (Math.abs(xx) < EPSLN) { - lon = this.long0; - } - else { - lon = adjust_lon(this.long0 + Math.PI * (xys - 1 + Math.sqrt(1 + 2 * (xx * xx - yy * yy) + xys * xys)) / 2 / xx); - } - - p.x = lon; - p.y = lat; - return p; -} - -export var names = ["Van_der_Grinten_I", "VanDerGrinten", "vandg"]; -export default { - init: init, - forward: forward, - inverse: inverse, - names: names -}; diff --git a/src/proj4/transform.js b/src/proj4/transform.js deleted file mode 100644 index e0b9f91c..00000000 --- a/src/proj4/transform.js +++ /dev/null @@ -1,167 +0,0 @@ -import {D2R, R2D, PJD_3PARAM, PJD_7PARAM, PJD_GRIDSHIFT} from './constants'; -import datum_transform from '../constants/datum'; -import proj from './Proj'; -import toPoint from './common/toPoint'; -import { sanityCheck } from './Point'; - -function checkNotWGS(source, dest) { - return ( - (source.datum.datum_type === PJD_3PARAM || source.datum.datum_type === PJD_7PARAM || source.datum.datum_type === PJD_GRIDSHIFT) && dest.datumCode !== 'WGS84') || - ((dest.datum.datum_type === PJD_3PARAM || dest.datum.datum_type === PJD_7PARAM || dest.datum.datum_type === PJD_GRIDSHIFT) && source.datumCode !== 'WGS84'); -} - -export default function transform(source, dest, point, enforceAxis) { - var wgs84; - if (Array.isArray(point)) { - point = toPoint(point); - } else { - // Clone the point object so inputs don't get modified - point = { - x: point.x, - y: point.y, - z: point.z, - m: point.m - }; - } - var hasZ = point.z !== undefined; - sanityCheck(point); - // Workaround for datum shifts towgs84, if either source or destination projection is not wgs84 - if (source.datum && dest.datum && checkNotWGS(source, dest)) { - wgs84 = new proj('WGS84'); - point = transform(source, wgs84, point, enforceAxis); - source = wgs84; - } - // DGR, 2010/11/12 - if (enforceAxis && source.axis !== 'enu') { - point = adjustAxis(source, false, point); - } - // Transform source points to long/lat, if they aren't already. - if (source.projName === 'longlat') { - point = { - x: point.x * D2R, - y: point.y * D2R, - z: point.z || 0 - }; - } else { - if (source.to_meter) { - point = { - x: point.x * source.to_meter, - y: point.y * source.to_meter, - z: point.z || 0 - }; - } - point = source.inverse(point); // Convert Cartesian to longlat - if (!point) { - return; - } - } - // Adjust for the prime meridian if necessary - if (source.from_greenwich) { - point.x += source.from_greenwich; - } - - // Convert datums if needed, and if possible. - point = datum_transform(source.datum, dest.datum, point); - if (!point) { - return; - } - - // Adjust for the prime meridian if necessary - if (dest.from_greenwich) { - point = { - x: point.x - dest.from_greenwich, - y: point.y, - z: point.z || 0 - }; - } - - if (dest.projName === 'longlat') { - // convert radians to decimal degrees - point = { - x: point.x * R2D, - y: point.y * R2D, - z: point.z || 0 - }; - } else { // else project - point = dest.forward(point); - if (dest.to_meter) { - point = { - x: point.x / dest.to_meter, - y: point.y / dest.to_meter, - z: point.z || 0 - }; - } - } - - // DGR, 2010/11/12 - if (enforceAxis && dest.axis !== 'enu') { - return adjustAxis(dest, true, point); - } - - if (point && !hasZ) { - delete point.z; - } - return point; -} - -export function adjustAxis(crs, denorm, point) { - var xin = point.x, - yin = point.y, - zin = point.z || 0.0; - var v, t, i; - var out = {}; - for (i = 0; i < 3; i++) { - if (denorm && i === 2 && point.z === undefined) { - continue; - } - if (i === 0) { - v = xin; - if ("ew".indexOf(crs.axis[i]) !== -1) { - t = 'x'; - } else { - t = 'y'; - } - - } - else if (i === 1) { - v = yin; - if ("ns".indexOf(crs.axis[i]) !== -1) { - t = 'y'; - } else { - t = 'x'; - } - } - else { - v = zin; - t = 'z'; - } - switch (crs.axis[i]) { - case 'e': - out[t] = v; - break; - case 'w': - out[t] = -v; - break; - case 'n': - out[t] = v; - break; - case 's': - out[t] = -v; - break; - case 'u': - if (point[t] !== undefined) { - out.z = v; - } - break; - case 'd': - if (point[t] !== undefined) { - out.z = -v; - } - break; - default: - //console.log("ERROR: unknow axis ("+crs.axis[i]+") - check definition of "+crs.projName); - return null; - } - } - return out; -} diff --git a/src/proj4/transformer.ts b/src/proj4/transformer.ts index 7a239a17..f75b2cc3 100644 --- a/src/proj4/transformer.ts +++ b/src/proj4/transformer.ts @@ -1,5 +1,6 @@ import { parseProjStr } from './parseCode'; import { ALL_DEFINITIONS, DEFAULT_DEFINITIONS, WGS84 } from './projections'; +import { checkNotWGS, datumTransform } from './datum'; import type { VectorPoint } from 's2-tools/geometry'; import type { ProjectionTransform, ProjectionTransformDefinition } from './projections'; @@ -14,6 +15,7 @@ export class Transformer { // Definitions are descriptions of projections definitions = new Map(); // source and destination projections + wgs84: ProjectionTransform; source: ProjectionTransform; destination: ProjectionTransform; @@ -25,7 +27,7 @@ export class Transformer { constructor(sourceCode?: string, destCode?: string) { for (const def of DEFAULT_DEFINITIONS) this.insertDefinition(def); // defaults to a standard WGS84 lon-lat projection transform - this.source = this.destination = this.#buildTransformer(WGS84); + this.source = this.destination = this.wgs84 = this.#buildTransformer(WGS84); if (sourceCode) this.setSource(sourceCode); if (destCode) this.setDestination(destCode); } @@ -46,7 +48,12 @@ export class Transformer { */ #buildTransformer(code: string): ProjectionTransform { const params = parseProjStr(code); - const def = this.definitions.get(params.name ?? ''); + // search + let def: ProjectionTransformDefinition | undefined; + for (const name of [params.projName, params.name]) { + def = this.definitions.get(name?.toLowerCase() ?? ''); + if (def !== undefined) break; + } if (def === undefined) throw Error(`${params.name} invalid, unsupported, or not loaded`); return new def(params); } @@ -56,30 +63,88 @@ export class Transformer { * @param names - optionally add projection reference names to add lookups to the definition */ insertDefinition(def: ProjectionTransformDefinition, names: string[] = []): void { - for (const name of def.names) this.definitions.set(name, def); - for (const name of names) this.definitions.set(name, def); + for (const name of def.names) this.definitions.set(name.toLowerCase(), def); + for (const name of names) this.definitions.set(name.toLowerCase(), def); } /** * @param p - vector point currently in the "source" projection - * @param _enforceAxis - enforce axis ensures axis consistency relative to the final projection + * @param enforceAxis - enforce axis ensures axis consistency relative to the final projection * @returns - vector point in the "destination" projection */ - forward(p: VectorPoint, _enforceAxis?: boolean): VectorPoint { - // TODO: apply enforceAxis if true - if (this.source.name === this.destination.name) return p; - return this.destination.forward(this.source.inverse(p)); + forward(p: VectorPoint, enforceAxis = false): VectorPoint { + return this.#transformPoint(p, this.source, this.destination, enforceAxis); } /** * @param p - vector point currently in the "destination" projection - * @param _enforceAxis - enforce axis ensures axis consistency relative to the final projection + * @param enforceAxis - enforce axis ensures axis consistency relative to the final projection * @returns - vector point in the "source" projection */ - inverse(p: VectorPoint, _enforceAxis?: boolean): VectorPoint { - // TODO: apply enforceAxis if true - if (this.source.name === this.destination.name) return p; - return this.source.forward(this.destination.inverse(p)); + inverse(p: VectorPoint, enforceAxis = false): VectorPoint { + return this.#transformPoint(p, this.destination, this.source, enforceAxis); + } + + /** + * @param sourcePoint - point to start transforming + * @param src - source projection + * @param dest - destination projection + * @param enforceAxis - enforce axis ensures axis consistency relative to the final projection + * @returns - transformed point + */ + #transformPoint( + sourcePoint: VectorPoint, + src: ProjectionTransform, + dest: ProjectionTransform, + enforceAxis: boolean, + ): VectorPoint { + const hasZ = sourcePoint.z !== undefined; + // Workaround for datum shifts towgs84, if either source or destination projection is not wgs84 + let res = { ...sourcePoint }; + if (checkNotWGS(src, dest)) { + const wgs84 = this.wgs84; + res = this.#transformPoint(res, src, wgs84, enforceAxis); + src = wgs84; + } + // STEP 1: SOURCE -> WGS84 + // if needed, adjust axis + if (enforceAxis && src.axis !== 'enu') { + adjustAxis(res, src, true); + } + // adjust for meters if necessary + if (src.name !== 'longlat' && src.projName !== 'longlat' && src.toMeter) { + res.x *= src.toMeter; + res.y *= src.toMeter; + } + + // transform forward + src.inverse(res); + // Adjust for the prime meridian if necessary + res.x += src.fromGreenwich; + + // STEP 2: MID-POINT. Convert datums if needed, and if possible. + datumTransform(res, src, dest); + + // console.log('STEP 2: MID DATUM B', res); + + // STEP 3: WGS84 -> DEST + // Adjust for the prime meridian if necessary + res.x -= dest.fromGreenwich; + // transform forward + dest.forward(res); + // adjust for meters if necessary + if (dest.name !== 'longlat' && dest.projName !== 'longlat' && dest.toMeter) { + res.x /= dest.toMeter; + res.y /= dest.toMeter; + } + // if needed, adjust axis + if (enforceAxis && dest.axis !== 'enu') { + adjustAxis(res, dest, true); + } + + if (!hasZ) delete res.z; + + return res; } } @@ -87,3 +152,63 @@ export class Transformer { export function injectAllDefinitions(transformer: Transformer) { for (const proj of ALL_DEFINITIONS) transformer.insertDefinition(proj); } + +/** + * @param point - the vector point to adjust + * @param prj - the projection + * @param denorm - denormalizes z if true + */ +function adjustAxis(point: VectorPoint, prj: ProjectionTransform, denorm: boolean): void { + const xin = point.x; + const yin = point.y; + const zin = point.z ?? 0; + let v, i; + let t: 'x' | 'y' | 'z'; + for (i = 0; i < 3; i++) { + if (denorm && i === 2 && point.z === undefined) continue; + if (i === 0) { + v = xin; + if ('ew'.indexOf(prj.axis[i]) !== -1) { + t = 'x'; + } else { + t = 'y'; + } + } else if (i === 1) { + v = yin; + if ('ns'.indexOf(prj.axis[i]) !== -1) { + t = 'y'; + } else { + t = 'x'; + } + } else { + v = zin; + t = 'z'; + } + switch (prj.axis[i]) { + case 'e': + point[t] = v; + break; + case 'w': + point[t] = -v; + break; + case 'n': + point[t] = v; + break; + case 's': + point[t] = -v; + break; + case 'u': + if (point[t] !== undefined) { + point.z = v; + } + break; + case 'd': + if (point[t] !== undefined) { + point.z = -v; + } + break; + default: + throw Error(`unknown axis (${prj.axis[i]}) - check definition of ${prj.name}`); + } + } +} diff --git a/src/readers/README.md b/src/readers/README.md index bf5bba73..d3c6c4fc 100644 --- a/src/readers/README.md +++ b/src/readers/README.md @@ -2,15 +2,17 @@ ## Readers I am interested in eventually supporting -- [ ] CSV +### XML BASED + - [ ] KML & KMZ -- [ ] XML -- [ ] [flatgeobuf](https://flatgeobuf.org/) & flatbuffers +- [ ] GML +- [ ] GPX + +### OTHERS + - [ ] GTFS - [ ] netCDF (currently only partially supported via nadgrid) -- [ ] gpkg (Geopackage) -- [ ] gml - [ ] gdb (in gzipped form) -- [ ] TIFFs (.tif, .tiff, .dem ) +- [ ] TIFFs (.tif, .tiff, .dem) - [ ] Images (.png, .jpg, .gif) - [ ] LAS/LAZ diff --git a/src/readers/bufferReader.ts b/src/readers/bufferReader.ts deleted file mode 100644 index b35ed7fc..00000000 --- a/src/readers/bufferReader.ts +++ /dev/null @@ -1,49 +0,0 @@ -import type { Reader } from '.'; - -/** A buffer reader is an extension of a DataView with some extra methods */ -export class BufferReader extends DataView implements Reader { - textDecoder = new TextDecoder('utf-8'); - - /** - * @param buffer - the input buffer - * @param byteOffset - offset in the buffer - * @param byteLength - length of the buffer - */ - constructor( - buffer: ArrayBufferLike & { - BYTES_PER_ELEMENT?: never; - }, - byteOffset?: number, - byteLength?: number, - ) { - super(buffer, byteOffset, byteLength); - } - - /** - * @param begin - beginning of the slice - * @param end - end of the slice. If not provided, the end of the data is used - * @returns - a DataView of the slice - */ - slice(begin: number, end: number): DataView { - return new DataView( - this.buffer.slice(this.byteOffset + begin, this.byteOffset + (end ?? this.byteLength)), - ); - } - - /** @param encoding - update the text decoder's encoding */ - setStringEncoding(encoding: string) { - this.textDecoder = new TextDecoder(encoding); - } - - /** - * @param byteOffset - Start of the string - * @param byteLength - Length of the string - * @returns - The string - */ - parseString(byteOffset: number, byteLength: number): string { - const { textDecoder } = this; - const data = this.slice(byteOffset, byteOffset + byteLength).buffer; - const out = textDecoder.decode(data, { stream: true }) + textDecoder.decode(); - return out.replace(/\0/g, '').trim(); - } -} diff --git a/src/readers/csv/index.ts b/src/readers/csv/index.ts new file mode 100644 index 00000000..a08f6218 --- /dev/null +++ b/src/readers/csv/index.ts @@ -0,0 +1,119 @@ +import type { FeatureIterator, Reader } from '..'; +import type { Properties, VectorFeature, VectorPoint } from 's2-tools/geometry'; + +/** User defined options on how to parse the CSV file */ +export interface CSVReaderOptions { + /** The delimiter to use to separate lines [Default=','] */ + delimiter?: string; + /** The lineDelimiter to use to separate lines [Default='\n'] */ + lineDelimiter?: string; + /** If provided the lookup of the longitude [Default='lon'] */ + lonKey?: string; + /** If provided the lookup of the latitude [Default='lat'] */ + latKey?: string; + /** If provided the lookup for the height value [Default=undefined] */ + heightKey?: string; +} + +/** Parse (Geo|S2)JSON from a file that is in the CSV format */ +export class CSVReader implements FeatureIterator { + #delimiter = ','; + #lineDelimiter = '\n'; + #lonKey = 'lon'; + #latKey = 'lat'; + #heightKey?: string; + #firstLine = true; + #fields: string[] = []; + /** + * @param reader - the reader to parse from + * @param options - user defined options on how to parse the CSV file + */ + constructor( + public reader: Reader, + options?: CSVReaderOptions, + ) { + if (options?.delimiter) this.#delimiter = options.delimiter; + if (options?.lineDelimiter) this.#lineDelimiter = options.lineDelimiter; + if (options?.lonKey) this.#lonKey = options.lonKey; + if (options?.latKey) this.#latKey = options.latKey; + if (options?.heightKey) this.#heightKey = options.heightKey; + } + + /** + * Generator to iterate over each (Geo|S2)JSON object in the file + * @yields {VectorFeature} + */ + async *[Symbol.asyncIterator](): AsyncGenerator { + const { reader } = this; + let cursor = 0; + let offset = 0; + let partialLine = ''; + + while (offset < reader.byteLength) { + const length = Math.min(65_536, reader.byteLength - cursor); + // Prepend any partial line to the new chunk + const chunk = partialLine + reader.parseString(offset, length); + partialLine = ''; + // Split the chunk by newlines and yield each complete line + const lines = chunk.split(this.#lineDelimiter); + for (let i = 0; i < lines.length - 1; i++) { + if (this.#firstLine) { + this.#parseFirstLine(lines[i]); + this.#firstLine = false; + } else { + yield this.#parseLine(lines[i]); + } + } + // Store the remaining partial line for the next iteration + partialLine = lines[lines.length - 1]; + // Update the cursor and offset + offset += length; + cursor += length; + } + + // Yield any remaining partial line after the loop + if (partialLine.length > 0) yield this.#parseLine(partialLine); + } + + /** + * @param line - the values mapped to the first lines fields + * @returns - a GeoJSON Vector Feature + */ + #parseLine(line: string): VectorFeature { + const values = line.split(this.#delimiter).map((v) => v.trim()); + + let is3D = false; + const properties: Properties = {}; + const coordinates: VectorPoint = { x: 0, y: 0 }; + + for (let i = 0; i < this.#fields.length; i++) { + const field = this.#fields[i]; + const value = values[i]; + if (field.length === 0 || value.length === 0) continue; + + if (field === this.#lonKey) coordinates.x = parseFloat(value); + else if (field === this.#latKey) coordinates.y = parseFloat(value); + else if (this.#heightKey && field === this.#heightKey) { + is3D = true; + coordinates.z = parseFloat(value); + } else properties[field] = value; + } + if (isNaN(coordinates.x) || isNaN(coordinates.y)) + throw new Error('coordinates must be finite numbers'); + + return { + type: 'VectorFeature', + geometry: { + type: 'Point', + is3D, + coordinates, + }, + properties, + }; + } + + /** @param line - the fields in the first line split by the delimiter */ + #parseFirstLine(line: string): void { + this.#fields = line.split(this.#delimiter).map((v) => v.trim()); + } +} diff --git a/src/readers/fetch.ts b/src/readers/fetch.ts new file mode 100644 index 00000000..2f70f07c --- /dev/null +++ b/src/readers/fetch.ts @@ -0,0 +1,141 @@ +import type { Reader } from '.'; + +/** The browser reader that fetches data from a URL. */ +export class FetchReader implements Reader { + byteLength = 0; + byteOffset = 0; + /** + * @param path - the location of the PMTiles data + * @param rangeRequests - FetchReader specific; enable range requests or use urlParam "bytes" + */ + constructor( + public path: string, + public rangeRequests: boolean, + ) {} + + /** + * Not applicable for FetchReader + * @param _byteOffset - offset + * @param _littleEndian - le or be + * @returns - 0 + */ + getBigInt64(_byteOffset: number, _littleEndian?: boolean): bigint { + return 0n; + } + /** + * Not applicable for FetchReader + * @param _byteOffset - offset + * @param _littleEndian - le or be + * @returns - 0 + */ + getBigUint64(_byteOffset: number, _littleEndian?: boolean): bigint { + return 0n; + } + /** + * Not applicable for FetchReader + * @param _byteOffset - offset + * @param _littleEndian - le or be + * @returns - 0 + */ + getFloat32(_byteOffset: number, _littleEndian?: boolean): number { + return 0; + } + /** + * Not applicable for FetchReader + * @param _byteOffset - offset + * @param _littleEndian - le or be + * @returns - 0 + */ + getFloat64(_byteOffset: number, _littleEndian?: boolean): number { + return 0; + } + /** + * Not applicable for FetchReader + * @param _byteOffset - offset + * @param _littleEndian - le or be + * @returns - 0 + */ + getInt16(_byteOffset: number, _littleEndian?: boolean): number { + return 0; + } + /** + * Not applicable for FetchReader + * @param _byteOffset - offset + * @param _littleEndian - le or be + * @returns - 0 + */ + getInt32(_byteOffset: number, _littleEndian?: boolean): number { + return 0; + } + /** + * Not applicable for FetchReader + * @param _byteOffset - offset + * @returns - 0 + */ + getInt8(_byteOffset: number): number { + return 0; + } + /** + * Not applicable for FetchReader + * @param _byteOffset - offset + * @param _littleEndian - le or be + * @returns - 0 + */ + getUint16(_byteOffset: number, _littleEndian?: boolean): number { + return 0; + } + /** + * Not applicable for FetchReader + * @param _byteOffset - offset + * @param _littleEndian - le or be + * @returns - 0 + */ + getUint32(_byteOffset: number, _littleEndian?: boolean): number { + return 0; + } + /** + * Not applicable for FetchReader + * @param _byteOffset - offset + * @returns - 0 + */ + getUint8(_byteOffset: number): number { + return 0; + } + /** + * Not applicable for FetchReader + * @param _begin - beginning + * @param _end - end + * @returns - empty DataView + */ + slice(_begin: number, _end: number): DataView { + return new DataView(new Uint8Array([]).buffer); + } + /** + * Not applicable for FetchReader + * @param _encoding - does nothing + */ + setStringEncoding(_encoding: string): void {} + /** + * Not applicable for FetchReader + * @param _byteOffset - offset + * @param _byteLength - length + * @returns - empty string + */ + parseString(_byteOffset: number, _byteLength: number): string { + return ''; + } + + /** + * @param offset - the offset of the range + * @param length - the length of the range + * @returns - the ranged buffer + */ + async getRange(offset: number, length: number): Promise { + const bytes = String(offset) + '-' + String(offset + length); + const fetchReq = this.rangeRequests + ? fetch(this.path, { headers: { Range: `bytes=${offset}-${offset + length - 1}` } }) + : fetch(`${this.path}&bytes=${bytes}`); + const res = await fetchReq.then(async (res) => await res.arrayBuffer()); + return new Uint8Array(res, 0, res.byteLength); + } +} diff --git a/src/readers/fileReader.ts b/src/readers/file.ts similarity index 91% rename from src/readers/fileReader.ts rename to src/readers/file.ts index 5d92f47a..b37aea83 100644 --- a/src/readers/fileReader.ts +++ b/src/readers/file.ts @@ -1,7 +1,10 @@ -import { closeSync, openSync, readSync, statSync } from 'fs'; +import { promisify } from 'util'; +import { closeSync, openSync, read, readSync, statSync } from 'fs'; import type { Reader } from '.'; +const readAsync = promisify(read); + /** Reads data from a file */ export default class FileReader implements Reader { #fileHandle: number; @@ -9,9 +12,7 @@ export default class FileReader implements Reader { byteLength: number; textDecoder = new TextDecoder('utf-8'); - /** - * @param file - The path to the file - */ + /** @param file - The path to the file */ constructor(file: string) { const stats = statSync(file); this.byteLength = stats.size; @@ -161,8 +162,17 @@ export default class FileReader implements Reader { } /** - * Closes the file + * @param offset - the offset of the range + * @param length - the length of the range + * @returns - the ranged buffer */ + async getRange(offset: number, length: number): Promise { + const buffer = Buffer.alloc(length); + await readAsync(this.#fileHandle, buffer, 0, length, offset); + return new Uint8Array(buffer.buffer, 0, length); + } + + /** Closes the file */ close() { closeSync(this.#fileHandle); } diff --git a/src/readers/geotiff/compression/basedecoder.ts b/src/readers/geotiff/compression/basedecoder.ts new file mode 100644 index 00000000..7d7c04de --- /dev/null +++ b/src/readers/geotiff/compression/basedecoder.ts @@ -0,0 +1,31 @@ +import { applyPredictor } from '../predictor'; + +/** + * + */ +export default class BaseDecoder { + /** + * @param fileDirectory + * @param buffer + */ + async decode(fileDirectory, buffer) { + const decoded = await this.decodeBlock(buffer); + const predictor = fileDirectory.Predictor || 1; + if (predictor !== 1) { + const isTiled = !fileDirectory.StripOffsets; + const tileWidth = isTiled ? fileDirectory.TileWidth : fileDirectory.ImageWidth; + const tileHeight = isTiled + ? fileDirectory.TileLength + : fileDirectory.RowsPerStrip || fileDirectory.ImageLength; + return applyPredictor( + decoded, + predictor, + tileWidth, + tileHeight, + fileDirectory.BitsPerSample, + fileDirectory.PlanarConfiguration, + ); + } + return decoded; + } +} diff --git a/src/readers/geotiff/compression/deflate.ts b/src/readers/geotiff/compression/deflate.ts new file mode 100644 index 00000000..7245ffd3 --- /dev/null +++ b/src/readers/geotiff/compression/deflate.ts @@ -0,0 +1,14 @@ +import BaseDecoder from './basedecoder'; +import { inflate } from 'pako'; + +/** + * + */ +export default class DeflateDecoder extends BaseDecoder { + /** + * @param buffer + */ + decodeBlock(buffer: ArrayBuffer): ArrayBuffer { + return inflate(new Uint8Array(buffer)).buffer; + } +} diff --git a/src/readers/geotiff/compression/index.ts b/src/readers/geotiff/compression/index.ts new file mode 100644 index 00000000..cf20586f --- /dev/null +++ b/src/readers/geotiff/compression/index.ts @@ -0,0 +1,43 @@ +const registry = new Map(); + +/** + * @param cases + * @param importFn + */ +export function addDecoder(cases, importFn) { + if (!Array.isArray(cases)) { + cases = [cases]; + } + cases.forEach((c) => registry.set(c, importFn)); +} + +/** + * @param fileDirectory + */ +export async function getDecoder(fileDirectory) { + const importFn = registry.get(fileDirectory.Compression); + if (!importFn) { + throw new Error(`Unknown compression method identifier: ${fileDirectory.Compression}`); + } + const Decoder = await importFn(); + return new Decoder(fileDirectory); +} + +// Add default decoders to registry (end-user may override with other implementations) +addDecoder([undefined, 1], () => import('./raw.js').then((m) => m.default)); +addDecoder(5, () => import('../../../util/lzw.js').then((m) => m.default)); +addDecoder(6, () => { + throw new Error('old style JPEG compression is not supported.'); +}); +addDecoder(7, () => import('./jpeg.js').then((m) => m.default)); +addDecoder([8, 32946], () => import('./deflate.js').then((m) => m.default)); +addDecoder(32773, () => import('./packbits.js').then((m) => m.default)); +addDecoder(34887, () => + import('./lerc.js') + .then(async (m) => { + await m.zstd.init(); + return m; + }) + .then((m) => m.default), +); +addDecoder([7, 50001], () => import('./webimage.js').then((m) => m.default)); diff --git a/src/readers/geotiff/compression/packbits.ts b/src/readers/geotiff/compression/packbits.ts new file mode 100644 index 00000000..84f07ad6 --- /dev/null +++ b/src/readers/geotiff/compression/packbits.ts @@ -0,0 +1,32 @@ +import BaseDecoder from './basedecoder'; + +/** + * + */ +export default class PackbitsDecoder extends BaseDecoder { + /** + * @param buffer + */ + decodeBlock(buffer: ArrayBuffer): ArrayBuffer { + const dataView = new DataView(buffer); + const out = []; + + for (let i = 0; i < buffer.byteLength; ++i) { + let header = dataView.getInt8(i); + if (header < 0) { + const next = dataView.getUint8(i + 1); + header = -header; + for (let j = 0; j <= header; ++j) { + out.push(next); + } + i += 1; + } else { + for (let j = 0; j <= header; ++j) { + out.push(dataView.getUint8(i + j + 1)); + } + i += header + 1; + } + } + return new Uint8Array(out).buffer; + } +} diff --git a/src/readers/geotiff/compression/raw.ts b/src/readers/geotiff/compression/raw.ts new file mode 100644 index 00000000..aee9dab0 --- /dev/null +++ b/src/readers/geotiff/compression/raw.ts @@ -0,0 +1,13 @@ +import BaseDecoder from './basedecoder'; + +/** + * + */ +export default class RawDecoder extends BaseDecoder { + /** + * @param buffer + */ + decodeBlock(buffer) { + return buffer; + } +} diff --git a/src/readers/geotiff/dataslice.ts b/src/readers/geotiff/dataslice.ts new file mode 100644 index 00000000..bed120bb --- /dev/null +++ b/src/readers/geotiff/dataslice.ts @@ -0,0 +1,192 @@ +/** + * + */ +export default class DataSlice { + #dataView: DataView; + #sliceOffset: number; + #littleEndian: boolean; + #bigTiff: boolean; + + /** + * @param arrayBuffer + * @param sliceOffset + * @param littleEndian + * @param bigTiff + */ + constructor( + arrayBuffer: ArrayBufferLike, + sliceOffset: number, + littleEndian: boolean, + bigTiff: boolean, + ) { + this.#dataView = new DataView(arrayBuffer); + this.#sliceOffset = sliceOffset; + this.#littleEndian = littleEndian; + this.#bigTiff = bigTiff; + } + + /** + * + */ + get sliceOffset() { + return this.#sliceOffset; + } + + /** + * + */ + get sliceTop() { + return this.#sliceOffset + this.buffer.byteLength; + } + + /** + * + */ + get littleEndian() { + return this.#littleEndian; + } + + /** + * + */ + get bigTiff() { + return this.#bigTiff; + } + + /** + * + */ + get buffer() { + return this.#dataView.buffer; + } + + /** + * @param offset + * @param length + */ + covers(offset: number, length: number): boolean { + return this.sliceOffset <= offset && this.sliceTop >= offset + length; + } + + /** + * @param offset + */ + readUint8(offset: number): number { + return this.#dataView.getUint8(offset - this.#sliceOffset); + } + + /** + * @param offset + */ + readInt8(offset: number): number { + return this.#dataView.getInt8(offset - this.#sliceOffset); + } + + /** + * @param offset + */ + readUint16(offset: number) { + return this.#dataView.getUint16(offset - this.#sliceOffset, this.#littleEndian); + } + + /** + * @param offset + */ + readInt16(offset: number) { + return this.#dataView.getInt16(offset - this.#sliceOffset, this.#littleEndian); + } + + /** + * @param offset + */ + readUint32(offset: number) { + return this.#dataView.getUint32(offset - this.#sliceOffset, this.#littleEndian); + } + + /** + * @param offset + */ + readInt32(offset: number) { + return this.#dataView.getInt32(offset - this.#sliceOffset, this.#littleEndian); + } + + /** + * @param offset + */ + readFloat32(offset: number) { + return this.#dataView.getFloat32(offset - this.#sliceOffset, this.#littleEndian); + } + + /** + * @param offset + */ + readFloat64(offset: number) { + return this.#dataView.getFloat64(offset - this.#sliceOffset, this.#littleEndian); + } + + /** + * @param offset + */ + readUint64(offset: number) { + const left = this.readUint32(offset); + const right = this.readUint32(offset + 4); + let combined; + if (this.#littleEndian) { + combined = left + 2 ** 32 * right; + if (!Number.isSafeInteger(combined)) { + throw new Error( + `${combined} exceeds MAX_SAFE_INTEGER. ` + + 'Precision may be lost. Please report if you get this message to https://github.com/geotiffjs/geotiff.js/issues', + ); + } + return combined; + } + combined = 2 ** 32 * left + right; + if (!Number.isSafeInteger(combined)) { + throw new Error( + `${combined} exceeds MAX_SAFE_INTEGER. ` + + 'Precision may be lost. Please report if you get this message to https://github.com/geotiffjs/geotiff.js/issues', + ); + } + + return combined; + } + + /** + * adapted from https://stackoverflow.com/a/55338384/8060591 + * @param offset + */ + readInt64(offset: number) { + let value = 0; + const isNegative = (this.#dataView.getUint8(offset + (this.#littleEndian ? 7 : 0)) & 0x80) > 0; + let carrying = true; + for (let i = 0; i < 8; i++) { + let byte = this.#dataView.getUint8(offset + (this.#littleEndian ? i : 7 - i)); + if (isNegative) { + if (carrying) { + if (byte !== 0x00) { + byte = ~(byte - 1) & 0xff; + carrying = false; + } + } else { + byte = ~byte & 0xff; + } + } + value += byte * 256 ** i; + } + if (isNegative) { + value = -value; + } + return value; + } + + /** + * @param offset + */ + readOffset(offset: number) { + if (this.#bigTiff) { + return this.readUint64(offset); + } + return this.readUint32(offset); + } +} diff --git a/src/readers/geotiff/decode.ts b/src/readers/geotiff/decode.ts new file mode 100644 index 00000000..9265f9d0 --- /dev/null +++ b/src/readers/geotiff/decode.ts @@ -0,0 +1,76 @@ +import { applyPredictor } from './predictor'; + +/** + * @param fileDirectory + * @param buffer + */ +export async function decode(fileDirectory, buffer) { + const decoded = await this.decodeBlock(buffer); + const predictor = fileDirectory.Predictor || 1; + if (predictor !== 1) { + const isTiled = !fileDirectory.StripOffsets; + const tileWidth = isTiled ? fileDirectory.TileWidth : fileDirectory.ImageWidth; + const tileHeight = isTiled + ? fileDirectory.TileLength + : fileDirectory.RowsPerStrip || fileDirectory.ImageLength; + return applyPredictor( + decoded, + predictor, + tileWidth, + tileHeight, + fileDirectory.BitsPerSample, + fileDirectory.PlanarConfiguration, + ); + } + return decoded; +} + +/** Assumes inputs like 'image/png' */ +export interface BlobOptions { + type?: string; +} + +/** + * @param fileDirectory + * @param buffer + * @param options + */ +export async function decodeImage( + buffer: ArrayBufferLike, + options: BlobOptions = {}, +): ArrayBufferLike { + const blob = new Blob([buffer], options); // e.g. { type: 'image/png' } + const imageBitmap = await createImageBitmap(blob); + // Create OffscreenCanvas and draw + const canvas = new OffscreenCanvas(imageBitmap.width, imageBitmap.height); + const ctx = canvas.getContext('2d'); + ctx.drawImage(imageBitmap, 0, 0); + + return ctx.getImageData(0, 0, canvas.width, canvas.height); +} + +/** + * @param buffer + */ +export function decodePackedBitsBlock(buffer: ArrayBufferLike): ArrayBufferLike { + const dataView = new DataView(buffer); + const out = []; + + for (let i = 0; i < buffer.byteLength; ++i) { + let header = dataView.getInt8(i); + if (header < 0) { + const next = dataView.getUint8(i + 1); + header = -header; + for (let j = 0; j <= header; ++j) { + out.push(next); + } + i += 1; + } else { + for (let j = 0; j <= header; ++j) { + out.push(dataView.getUint8(i + j + 1)); + } + i += header + 1; + } + } + return new Uint8Array(out).buffer; +} diff --git a/src/readers/geotiff/geotiff.ts b/src/readers/geotiff/geotiff.ts new file mode 100644 index 00000000..47016603 --- /dev/null +++ b/src/readers/geotiff/geotiff.ts @@ -0,0 +1,825 @@ +import GeoTIFFImage from './geotiffimage'; +import DataView64 from './dataview64'; +import DataSlice from './dataslice'; +import Pool from './pool'; + +import { makeCustomSource, makeRemoteSource } from './source/remote'; +import { makeBufferSource } from './source/arraybuffer'; +import { makeFileReaderSource } from './source/filereader'; +import { makeFileSource } from './source/file'; +import { BaseClient, BaseResponse } from './source/client/base'; + +import { ARRAY_FIELDS, FIELD_TAG_NAMES, FIELD_TYPES, geoKeyNames } from './globals'; +import { writeGeotiff } from './geotiffwriter'; +import * as globals from './globals'; +import * as rgb from './rgb'; +import { addDecoder, getDecoder } from './compression/index'; +import { setLogger } from './logging'; + +export { globals }; +export { rgb }; +export { default as BaseDecoder } from './compression/basedecoder'; +export { getDecoder, addDecoder }; +export { setLogger }; + +/** + * + * TypedArray + */ + +/** + */ + +/** + * The autogenerated docs are a little confusing here. The effective type is: + * + * `TypedArray & { height: number; width: number}` + */ + +/** + * The autogenerated docs are a little confusing here. The effective type is: + * + * `TypedArray[] & { height: number; width: number}` + */ + +/** + * The autogenerated docs are a little confusing here. The effective type is: + * + * `(TypedArray | TypedArray[]) & { height: number; width: number}` + */ + +/** + * @param fieldType + */ +function getFieldTypeLength(fieldType) { + switch (fieldType) { + case FIELD_TYPES.BYTE: + case FIELD_TYPES.ASCII: + case FIELD_TYPES.SBYTE: + case FIELD_TYPES.UNDEFINED: + return 1; + case FIELD_TYPES.SHORT: + case FIELD_TYPES.SSHORT: + return 2; + case FIELD_TYPES.LONG: + case FIELD_TYPES.SLONG: + case FIELD_TYPES.FLOAT: + case FIELD_TYPES.IFD: + return 4; + case FIELD_TYPES.RATIONAL: + case FIELD_TYPES.SRATIONAL: + case FIELD_TYPES.DOUBLE: + case FIELD_TYPES.LONG8: + case FIELD_TYPES.SLONG8: + case FIELD_TYPES.IFD8: + return 8; + default: + throw new RangeError(`Invalid field type: ${fieldType}`); + } +} + +/** + * @param fileDirectory + */ +function parseGeoKeyDirectory(fileDirectory) { + const rawGeoKeyDirectory = fileDirectory.GeoKeyDirectory; + if (!rawGeoKeyDirectory) { + return null; + } + + const geoKeyDirectory = {}; + for (let i = 4; i <= rawGeoKeyDirectory[3] * 4; i += 4) { + const key = geoKeyNames[rawGeoKeyDirectory[i]]; + const location = rawGeoKeyDirectory[i + 1] ? FIELD_TAG_NAMES[rawGeoKeyDirectory[i + 1]] : null; + const count = rawGeoKeyDirectory[i + 2]; + const offset = rawGeoKeyDirectory[i + 3]; + + let value = null; + if (!location) { + value = offset; + } else { + value = fileDirectory[location]; + if (typeof value === 'undefined' || value === null) { + throw new Error(`Could not get value of geoKey '${key}'.`); + } else if (typeof value === 'string') { + value = value.substring(offset, offset + count - 1); + } else if (value.subarray) { + value = value.subarray(offset, offset + count); + if (count === 1) { + value = value[0]; + } + } + } + geoKeyDirectory[key] = value; + } + return geoKeyDirectory; +} + +/** + * @param dataSlice + * @param fieldType + * @param count + * @param offset + */ +function getValues(dataSlice, fieldType, count, offset) { + let values = null; + let readMethod = null; + const fieldTypeLength = getFieldTypeLength(fieldType); + + switch (fieldType) { + case FIELD_TYPES.BYTE: + case FIELD_TYPES.ASCII: + case FIELD_TYPES.UNDEFINED: + values = new Uint8Array(count); + readMethod = dataSlice.readUint8; + break; + case FIELD_TYPES.SBYTE: + values = new Int8Array(count); + readMethod = dataSlice.readInt8; + break; + case FIELD_TYPES.SHORT: + values = new Uint16Array(count); + readMethod = dataSlice.readUint16; + break; + case FIELD_TYPES.SSHORT: + values = new Int16Array(count); + readMethod = dataSlice.readInt16; + break; + case FIELD_TYPES.LONG: + case FIELD_TYPES.IFD: + values = new Uint32Array(count); + readMethod = dataSlice.readUint32; + break; + case FIELD_TYPES.SLONG: + values = new Int32Array(count); + readMethod = dataSlice.readInt32; + break; + case FIELD_TYPES.LONG8: + case FIELD_TYPES.IFD8: + values = new Array(count); + readMethod = dataSlice.readUint64; + break; + case FIELD_TYPES.SLONG8: + values = new Array(count); + readMethod = dataSlice.readInt64; + break; + case FIELD_TYPES.RATIONAL: + values = new Uint32Array(count * 2); + readMethod = dataSlice.readUint32; + break; + case FIELD_TYPES.SRATIONAL: + values = new Int32Array(count * 2); + readMethod = dataSlice.readInt32; + break; + case FIELD_TYPES.FLOAT: + values = new Float32Array(count); + readMethod = dataSlice.readFloat32; + break; + case FIELD_TYPES.DOUBLE: + values = new Float64Array(count); + readMethod = dataSlice.readFloat64; + break; + default: + throw new RangeError(`Invalid field type: ${fieldType}`); + } + + // normal fields + if (!(fieldType === FIELD_TYPES.RATIONAL || fieldType === FIELD_TYPES.SRATIONAL)) { + for (let i = 0; i < count; ++i) { + values[i] = readMethod.call(dataSlice, offset + i * fieldTypeLength); + } + } else { + // RATIONAL or SRATIONAL + for (let i = 0; i < count; i += 2) { + values[i] = readMethod.call(dataSlice, offset + i * fieldTypeLength); + values[i + 1] = readMethod.call(dataSlice, offset + (i * fieldTypeLength + 4)); + } + } + + if (fieldType === FIELD_TYPES.ASCII) { + return new TextDecoder('utf-8').decode(values); + } + return values; +} + +/** + * Data class to store the parsed file directory, geo key directory and + * offset to the next IFD + */ +class ImageFileDirectory { + /** + * @param fileDirectory + * @param geoKeyDirectory + * @param nextIFDByteOffset + */ + constructor(fileDirectory, geoKeyDirectory, nextIFDByteOffset) { + this.fileDirectory = fileDirectory; + this.geoKeyDirectory = geoKeyDirectory; + this.nextIFDByteOffset = nextIFDByteOffset; + } +} + +/** + * Error class for cases when an IFD index was requested, that does not exist + * in the file. + */ +class GeoTIFFImageIndexError extends Error { + /** + * @param index + */ + constructor(index) { + super(`No image at index ${index}`); + this.index = index; + } +} + +/** + * + */ +class GeoTIFFBase { + /** + * (experimental) Reads raster data from the best fitting image. This function uses + * the image with the lowest resolution that is still a higher resolution than the + * requested resolution. + * When specified, the `bbox` option is translated to the `window` option and the + * `resX` and `resY` to `width` and `height` respectively. + * Then, the [readRasters]{@link GeoTIFFImage#readRasters} method of the selected + * image is called and the result returned. + * @see GeoTIFFImage.readRasters + * @param [options] optional parameters + * @returns the decoded array(s), with `height` and `width`, as a promise + */ + async readRasters(options = {}) { + const { window: imageWindow, width, height } = options; + let { resX, resY, bbox } = options; + + const firstImage = await this.getImage(); + let usedImage = firstImage; + const imageCount = await this.getImageCount(); + const imgBBox = firstImage.getBoundingBox(); + + if (imageWindow && bbox) { + throw new Error('Both "bbox" and "window" passed.'); + } + + // if width/height is passed, transform it to resolution + if (width || height) { + // if we have an image window (pixel coordinates), transform it to a BBox + // using the origin/resolution of the first image. + if (imageWindow) { + const [oX, oY] = firstImage.getOrigin(); + const [rX, rY] = firstImage.getResolution(); + + bbox = [ + oX + imageWindow[0] * rX, + oY + imageWindow[1] * rY, + oX + imageWindow[2] * rX, + oY + imageWindow[3] * rY, + ]; + } + + // if we have a bbox (or calculated one) + + const usedBBox = bbox || imgBBox; + + if (width) { + if (resX) { + throw new Error('Both width and resX passed'); + } + resX = (usedBBox[2] - usedBBox[0]) / width; + } + if (height) { + if (resY) { + throw new Error('Both width and resY passed'); + } + resY = (usedBBox[3] - usedBBox[1]) / height; + } + } + + // if resolution is set or calculated, try to get the image with the worst acceptable resolution + if (resX || resY) { + const allImages = []; + for (let i = 0; i < imageCount; ++i) { + const image = await this.getImage(i); + const { SubfileType: subfileType, NewSubfileType: newSubfileType } = image.fileDirectory; + if (i === 0 || subfileType === 2 || newSubfileType & 1) { + allImages.push(image); + } + } + + allImages.sort((a, b) => a.getWidth() - b.getWidth()); + for (let i = 0; i < allImages.length; ++i) { + const image = allImages[i]; + const imgResX = (imgBBox[2] - imgBBox[0]) / image.getWidth(); + const imgResY = (imgBBox[3] - imgBBox[1]) / image.getHeight(); + + usedImage = image; + if ((resX && resX > imgResX) || (resY && resY > imgResY)) { + break; + } + } + } + + let wnd = imageWindow; + if (bbox) { + const [oX, oY] = firstImage.getOrigin(); + const [imageResX, imageResY] = usedImage.getResolution(firstImage); + + wnd = [ + Math.round((bbox[0] - oX) / imageResX), + Math.round((bbox[1] - oY) / imageResY), + Math.round((bbox[2] - oX) / imageResX), + Math.round((bbox[3] - oY) / imageResY), + ]; + wnd = [ + Math.min(wnd[0], wnd[2]), + Math.min(wnd[1], wnd[3]), + Math.max(wnd[0], wnd[2]), + Math.max(wnd[1], wnd[3]), + ]; + } + + return usedImage.readRasters({ ...options, window: wnd }); + } +} + +/** + * [cache=false] whether or not decoded tiles shall be cached. + */ + +/** + * The abstraction for a whole GeoTIFF file. + */ +class GeoTIFF extends GeoTIFFBase { + /** + * @param source The datasource to read from. + * @param littleEndian Whether the image uses little endian. + * @param bigTiff Whether the image uses bigTIFF conventions. + * @param firstIFDOffset The numeric byte-offset from the start of the image + * to the first IFD. + * @param [options] further options. + */ + constructor(source, littleEndian, bigTiff, firstIFDOffset, options = {}) { + super(); + this.source = source; + this.littleEndian = littleEndian; + this.bigTiff = bigTiff; + this.firstIFDOffset = firstIFDOffset; + this.cache = options.cache || false; + this.ifdRequests = []; + this.ghostValues = null; + } + + /** + * @param offset + * @param size + */ + async getSlice(offset, size) { + const fallbackSize = this.bigTiff ? 4048 : 1024; + return new DataSlice( + ( + await this.source.fetch([ + { + offset, + length: typeof size !== 'undefined' ? size : fallbackSize, + }, + ]) + )[0], + offset, + this.littleEndian, + this.bigTiff, + ); + } + + /** + * Instructs to parse an image file directory at the given file offset. + * As there is no way to ensure that a location is indeed the start of an IFD, + * this function must be called with caution (e.g only using the IFD offsets from + * the headers or other IFDs). + * @param offset the offset to parse the IFD at + * @returns the parsed IFD + */ + async parseFileDirectoryAt(offset) { + const entrySize = this.bigTiff ? 20 : 12; + const offsetSize = this.bigTiff ? 8 : 2; + + let dataSlice = await this.getSlice(offset); + const numDirEntries = this.bigTiff + ? dataSlice.readUint64(offset) + : dataSlice.readUint16(offset); + + // if the slice does not cover the whole IFD, request a bigger slice, where the + // whole IFD fits: num of entries + n x tag length + offset to next IFD + const byteSize = numDirEntries * entrySize + (this.bigTiff ? 16 : 6); + if (!dataSlice.covers(offset, byteSize)) { + dataSlice = await this.getSlice(offset, byteSize); + } + + const fileDirectory = {}; + + // loop over the IFD and create a file directory object + let i = offset + (this.bigTiff ? 8 : 2); + for (let entryCount = 0; entryCount < numDirEntries; i += entrySize, ++entryCount) { + const fieldTag = dataSlice.readUint16(i); + const fieldType = dataSlice.readUint16(i + 2); + const typeCount = this.bigTiff ? dataSlice.readUint64(i + 4) : dataSlice.readUint32(i + 4); + + let fieldValues; + let value; + const fieldTypeLength = getFieldTypeLength(fieldType); + const valueOffset = i + (this.bigTiff ? 12 : 8); + + // check whether the value is directly encoded in the tag or refers to a + // different external byte range + if (fieldTypeLength * typeCount <= (this.bigTiff ? 8 : 4)) { + fieldValues = getValues(dataSlice, fieldType, typeCount, valueOffset); + } else { + // resolve the reference to the actual byte range + const actualOffset = dataSlice.readOffset(valueOffset); + const length = getFieldTypeLength(fieldType) * typeCount; + + // check, whether we actually cover the referenced byte range; if not, + // request a new slice of bytes to read from it + if (dataSlice.covers(actualOffset, length)) { + fieldValues = getValues(dataSlice, fieldType, typeCount, actualOffset); + } else { + const fieldDataSlice = await this.getSlice(actualOffset, length); + fieldValues = getValues(fieldDataSlice, fieldType, typeCount, actualOffset); + } + } + + // unpack single values from the array + if ( + typeCount === 1 && + ARRAY_FIELDS.indexOf(fieldTag) === -1 && + !(fieldType === FIELD_TYPES.RATIONAL || fieldType === FIELD_TYPES.SRATIONAL) + ) { + value = fieldValues[0]; + } else { + value = fieldValues; + } + + // write the tags value to the file directly + fileDirectory[FIELD_TAG_NAMES[fieldTag]] = value; + } + const geoKeyDirectory = parseGeoKeyDirectory(fileDirectory); + const nextIFDByteOffset = dataSlice.readOffset(offset + offsetSize + entrySize * numDirEntries); + + return new ImageFileDirectory(fileDirectory, geoKeyDirectory, nextIFDByteOffset); + } + + /** + * @param index + */ + async requestIFD(index) { + // see if we already have that IFD index requested. + if (this.ifdRequests[index]) { + // attach to an already requested IFD + return this.ifdRequests[index]; + } else if (index === 0) { + // special case for index 0 + this.ifdRequests[index] = this.parseFileDirectoryAt(this.firstIFDOffset); + return this.ifdRequests[index]; + } else if (!this.ifdRequests[index - 1]) { + // if the previous IFD was not yet loaded, load that one first + // this is the recursive call. + try { + this.ifdRequests[index - 1] = this.requestIFD(index - 1); + } catch (e) { + // if the previous one already was an index error, rethrow + // with the current index + if (e instanceof GeoTIFFImageIndexError) { + throw new GeoTIFFImageIndexError(index); + } + // rethrow anything else + throw e; + } + } + // if the previous IFD was loaded, we can finally fetch the one we are interested in. + // we need to wrap this in an IIFE, otherwise this.ifdRequests[index] would be delayed + this.ifdRequests[index] = (async () => { + const previousIfd = await this.ifdRequests[index - 1]; + if (previousIfd.nextIFDByteOffset === 0) { + throw new GeoTIFFImageIndexError(index); + } + return this.parseFileDirectoryAt(previousIfd.nextIFDByteOffset); + })(); + return this.ifdRequests[index]; + } + + /** + * Get the n-th internal subfile of an image. By default, the first is returned. + * @param [index] the index of the image to return. + * @returns the image at the given index + */ + async getImage(index = 0) { + const ifd = await this.requestIFD(index); + return new GeoTIFFImage( + ifd.fileDirectory, + ifd.geoKeyDirectory, + this.dataView, + this.littleEndian, + this.cache, + this.source, + ); + } + + /** + * Returns the count of the internal subfiles. + * @returns the number of internal subfile images + */ + async getImageCount() { + let index = 0; + // loop until we run out of IFDs + let hasNext = true; + while (hasNext) { + try { + await this.requestIFD(index); + ++index; + } catch (e) { + if (e instanceof GeoTIFFImageIndexError) { + hasNext = false; + } else { + throw e; + } + } + } + return index; + } + + /** + * Get the values of the COG ghost area as a parsed map. + * See https://gdal.org/drivers/raster/cog.html#header-ghost-area for reference + * @returns the parsed ghost area or null, if no such area was found + */ + async getGhostValues() { + const offset = this.bigTiff ? 16 : 8; + if (this.ghostValues) { + return this.ghostValues; + } + const detectionString = 'GDAL_STRUCTURAL_METADATA_SIZE='; + const heuristicAreaSize = detectionString.length + 100; + let slice = await this.getSlice(offset, heuristicAreaSize); + if (detectionString === getValues(slice, FIELD_TYPES.ASCII, detectionString.length, offset)) { + const valuesString = getValues(slice, FIELD_TYPES.ASCII, heuristicAreaSize, offset); + const firstLine = valuesString.split('\n')[0]; + const metadataSize = Number(firstLine.split('=')[1].split(' ')[0]) + firstLine.length; + if (metadataSize > heuristicAreaSize) { + slice = await this.getSlice(offset, metadataSize); + } + const fullString = getValues(slice, FIELD_TYPES.ASCII, metadataSize, offset); + this.ghostValues = {}; + fullString + .split('\n') + .filter((line) => line.length > 0) + .map((line) => line.split('=')) + .forEach(([key, value]) => { + this.ghostValues[key] = value; + }); + } + return this.ghostValues; + } + + /** + * Parse a (Geo)TIFF file from the given source. + * @param source The source of data to parse from. + * @param [options] Additional options. + * @param [signal] An AbortSignal that may be signalled if the request is + * to be aborted + */ + static async fromSource(source, options, signal) { + const headerData = (await source.fetch([{ offset: 0, length: 1024 }], signal))[0]; + const dataView = new DataView64(headerData); + + const BOM = dataView.getUint16(0, 0); + let littleEndian; + if (BOM === 0x4949) { + littleEndian = true; + } else if (BOM === 0x4d4d) { + littleEndian = false; + } else { + throw new TypeError('Invalid byte order value.'); + } + + const magicNumber = dataView.getUint16(2, littleEndian); + let bigTiff; + if (magicNumber === 42) { + bigTiff = false; + } else if (magicNumber === 43) { + bigTiff = true; + const offsetByteSize = dataView.getUint16(4, littleEndian); + if (offsetByteSize !== 8) { + throw new Error('Unsupported offset byte-size.'); + } + } else { + throw new TypeError('Invalid magic number.'); + } + + const firstIFDOffset = bigTiff + ? dataView.getUint64(8, littleEndian) + : dataView.getUint32(4, littleEndian); + return new GeoTIFF(source, littleEndian, bigTiff, firstIFDOffset, options); + } + + /** + * Closes the underlying file buffer + * N.B. After the GeoTIFF has been completely processed it needs + * to be closed but only if it has been constructed from a file. + */ + close() { + if (typeof this.source.close === 'function') { + return this.source.close(); + } + return false; + } +} + +export { GeoTIFF }; +export default GeoTIFF; + +/** + * Wrapper for GeoTIFF files that have external overviews. + */ +class MultiGeoTIFF extends GeoTIFFBase { + /** + * Construct a new MultiGeoTIFF from a main and several overview files. + * @param mainFile The main GeoTIFF file. + * @param overviewFiles An array of overview files. + */ + constructor(mainFile, overviewFiles) { + super(); + this.mainFile = mainFile; + this.overviewFiles = overviewFiles; + this.imageFiles = [mainFile].concat(overviewFiles); + + this.fileDirectoriesPerFile = null; + this.fileDirectoriesPerFileParsing = null; + this.imageCount = null; + } + + /** + * + */ + async parseFileDirectoriesPerFile() { + const requests = [this.mainFile.parseFileDirectoryAt(this.mainFile.firstIFDOffset)].concat( + this.overviewFiles.map((file) => file.parseFileDirectoryAt(file.firstIFDOffset)), + ); + + this.fileDirectoriesPerFile = await Promise.all(requests); + return this.fileDirectoriesPerFile; + } + + /** + * Get the n-th internal subfile of an image. By default, the first is returned. + * @param [index] the index of the image to return. + * @returns the image at the given index + */ + async getImage(index = 0) { + await this.getImageCount(); + await this.parseFileDirectoriesPerFile(); + let visited = 0; + let relativeIndex = 0; + for (let i = 0; i < this.imageFiles.length; i++) { + const imageFile = this.imageFiles[i]; + for (let ii = 0; ii < this.imageCounts[i]; ii++) { + if (index === visited) { + const ifd = await imageFile.requestIFD(relativeIndex); + return new GeoTIFFImage( + ifd.fileDirectory, + ifd.geoKeyDirectory, + imageFile.dataView, + imageFile.littleEndian, + imageFile.cache, + imageFile.source, + ); + } + visited++; + relativeIndex++; + } + relativeIndex = 0; + } + + throw new RangeError('Invalid image index'); + } + + /** + * Returns the count of the internal subfiles. + * @returns the number of internal subfile images + */ + async getImageCount() { + if (this.imageCount !== null) { + return this.imageCount; + } + const requests = [this.mainFile.getImageCount()].concat( + this.overviewFiles.map((file) => file.getImageCount()), + ); + this.imageCounts = await Promise.all(requests); + this.imageCount = this.imageCounts.reduce((count, ifds) => count + ifds, 0); + return this.imageCount; + } +} + +export { MultiGeoTIFF }; + +/** + * Creates a new GeoTIFF from a remote URL. + * @param url The URL to access the image from + * @param [options] Additional options to pass to the source. + * See {@link makeRemoteSource} for details. + * @param [signal] An AbortSignal that may be signalled if the request is + * to be aborted + * @returns The resulting GeoTIFF file. + */ +export async function fromUrl(url, options = {}, signal) { + return GeoTIFF.fromSource(makeRemoteSource(url, options), signal); +} + +/** + * Creates a new GeoTIFF from a custom {@link BaseClient}. + * @param client The client. + * @param [options] Additional options to pass to the source. + * See {@link makeRemoteSource} for details. + * @param [signal] An AbortSignal that may be signalled if the request is + * to be aborted + * @returns The resulting GeoTIFF file. + */ +export async function fromCustomClient(client, options = {}, signal) { + return GeoTIFF.fromSource(makeCustomSource(client, options), signal); +} + +/** + * Construct a new GeoTIFF from an + * [ArrayBuffer]{@link https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/ArrayBuffer}. + * @param arrayBuffer The data to read the file from. + * @param [signal] An AbortSignal that may be signalled if the request is + * to be aborted + * @returns The resulting GeoTIFF file. + */ +export async function fromArrayBuffer(arrayBuffer, signal) { + return GeoTIFF.fromSource(makeBufferSource(arrayBuffer), signal); +} + +/** + * Construct a GeoTIFF from a local file path. This uses the node + * [filesystem API]{@link https://nodejs.org/api/fs.html} and is + * not available on browsers. + * + * N.B. After the GeoTIFF has been completely processed it needs + * to be closed but only if it has been constructed from a file. + * @param path The file path to read from. + * @param [signal] An AbortSignal that may be signalled if the request is + * to be aborted + * @returns The resulting GeoTIFF file. + */ +export async function fromFile(path, signal) { + return GeoTIFF.fromSource(makeFileSource(path), signal); +} + +/** + * Construct a GeoTIFF from an HTML + * [Blob]{@link https://developer.mozilla.org/en-US/docs/Web/API/Blob} or + * [File]{@link https://developer.mozilla.org/en-US/docs/Web/API/File} + * object. + * @param blob The Blob or File object to read from. + * @param [signal] An AbortSignal that may be signalled if the request is + * to be aborted + * @returns The resulting GeoTIFF file. + */ +export async function fromBlob(blob, signal) { + return GeoTIFF.fromSource(makeFileReaderSource(blob), signal); +} + +/** + * Construct a MultiGeoTIFF from the given URLs. + * @param mainUrl The URL for the main file. + * @param overviewUrls An array of URLs for the overview images. + * @param [options] Additional options to pass to the source. + * See [makeRemoteSource]{@link module:source.makeRemoteSource} + * for details. + * @param [signal] An AbortSignal that may be signalled if the request is + * to be aborted + * @returns The resulting MultiGeoTIFF file. + */ +export async function fromUrls(mainUrl, overviewUrls = [], options = {}, signal) { + const mainFile = await GeoTIFF.fromSource(makeRemoteSource(mainUrl, options), signal); + const overviewFiles = await Promise.all( + overviewUrls.map((url) => GeoTIFF.fromSource(makeRemoteSource(url, options))), + ); + + return new MultiGeoTIFF(mainFile, overviewFiles); +} + +/** + * Main creating function for GeoTIFF files. + * @param array of pixel values + * @param values + * @param metadata + * @returns metadata + */ +export function writeArrayBuffer(values, metadata) { + return writeGeotiff(values, metadata); +} + +export { Pool }; +export { GeoTIFFImage }; +export { BaseClient, BaseResponse }; diff --git a/src/readers/geotiff/geotiffimage.ts b/src/readers/geotiff/geotiffimage.ts new file mode 100644 index 00000000..4d55f793 --- /dev/null +++ b/src/readers/geotiff/geotiffimage.ts @@ -0,0 +1,1041 @@ +// import { getFloat16 } from '@petamoriken/float16'; +import { findTagsByName, getAttribute } from 's2-tools/readers/xml'; + +import { getDecoder } from './compression/index'; +import { ExtraSamplesValues, PHOTOMETRIC_INTERPRETATIONS } from './globals'; +import { + fromBlackIsZero, + fromCIELab, + fromCMYK, + fromPalette, + fromWhiteIsZero, + fromYCbCr, +} from './rgb'; +import { resample, resampleInterleaved } from './resample'; + +/** + * [window=whole window] the subset to read data from in pixels. + * [bbox=whole image] the subset to read data from in + * geographical coordinates. + * [samples=all samples] the selection of samples to read from. Default is all samples. + * [interleave=false] whether the data shall be read + * in one single array or separate + * arrays. + * [pool=null] The optional decoder pool to use. + * [width] The desired width of the output. When the width is not the + * same as the images, resampling will be performed. + * [height] The desired height of the output. When the width is not the + * same as the images, resampling will be performed. + * [resampleMethod='nearest'] The desired resampling method. + * [signal] An AbortSignal that may be signalled if the request is + * to be aborted + * [fillValue] The value to use for parts of the image + * outside of the images extent. When multiple + * samples are requested, an array of fill values + * can be passed. + */ + +/** + * @param array + * @param start + * @param end + */ +function sum(array, start, end) { + let s = 0; + for (let i = start; i < end; ++i) { + s += array[i]; + } + return s; +} + +/** + * @param format + * @param bitsPerSample + * @param size + */ +function arrayForType(format, bitsPerSample, size) { + switch (format) { + case 1: // unsigned integer data + if (bitsPerSample <= 8) { + return new Uint8Array(size); + } else if (bitsPerSample <= 16) { + return new Uint16Array(size); + } else if (bitsPerSample <= 32) { + return new Uint32Array(size); + } + break; + case 2: // twos complement signed integer data + if (bitsPerSample === 8) { + return new Int8Array(size); + } else if (bitsPerSample === 16) { + return new Int16Array(size); + } else if (bitsPerSample === 32) { + return new Int32Array(size); + } + break; + case 3: // floating point data + switch (bitsPerSample) { + case 16: + case 32: + return new Float32Array(size); + case 64: + return new Float64Array(size); + default: + break; + } + break; + default: + break; + } + throw Error('Unsupported data format/bitsPerSample'); +} + +/** + * @param format + * @param bitsPerSample + */ +function needsNormalization(format, bitsPerSample) { + if ((format === 1 || format === 2) && bitsPerSample <= 32 && bitsPerSample % 8 === 0) { + return false; + } else if ( + format === 3 && + (bitsPerSample === 16 || bitsPerSample === 32 || bitsPerSample === 64) + ) { + return false; + } + return true; +} + +/** + * @param inBuffer + * @param format + * @param planarConfiguration + * @param samplesPerPixel + * @param bitsPerSample + * @param tileWidth + * @param tileHeight + */ +function normalizeArray( + inBuffer, + format, + planarConfiguration, + samplesPerPixel, + bitsPerSample, + tileWidth, + tileHeight, +) { + // const inByteArray = new Uint8Array(inBuffer); + const view = new DataView(inBuffer); + const outSize = + planarConfiguration === 2 ? tileHeight * tileWidth : tileHeight * tileWidth * samplesPerPixel; + const samplesToTransfer = planarConfiguration === 2 ? 1 : samplesPerPixel; + const outArray = arrayForType(format, bitsPerSample, outSize); + // let pixel = 0; + + const bitMask = parseInt('1'.repeat(bitsPerSample), 2); + + if (format === 1) { + // unsigned integer + // translation of https://github.com/OSGeo/gdal/blob/master/gdal/frmts/gtiff/geotiff.cpp#L7337 + let pixelBitSkip; + // let sampleBitOffset = 0; + if (planarConfiguration === 1) { + pixelBitSkip = samplesPerPixel * bitsPerSample; + // sampleBitOffset = (samplesPerPixel - 1) * bitsPerSample; + } else { + pixelBitSkip = bitsPerSample; + } + + // Bits per line rounds up to next byte boundary. + let bitsPerLine = tileWidth * pixelBitSkip; + if ((bitsPerLine & 7) !== 0) { + bitsPerLine = (bitsPerLine + 7) & ~7; + } + + for (let y = 0; y < tileHeight; ++y) { + const lineBitOffset = y * bitsPerLine; + for (let x = 0; x < tileWidth; ++x) { + const pixelBitOffset = lineBitOffset + x * samplesToTransfer * bitsPerSample; + for (let i = 0; i < samplesToTransfer; ++i) { + const bitOffset = pixelBitOffset + i * bitsPerSample; + const outIndex = (y * tileWidth + x) * samplesToTransfer + i; + + const byteOffset = Math.floor(bitOffset / 8); + const innerBitOffset = bitOffset % 8; + if (innerBitOffset + bitsPerSample <= 8) { + outArray[outIndex] = + (view.getUint8(byteOffset) >> (8 - bitsPerSample - innerBitOffset)) & bitMask; + } else if (innerBitOffset + bitsPerSample <= 16) { + outArray[outIndex] = + (view.getUint16(byteOffset) >> (16 - bitsPerSample - innerBitOffset)) & bitMask; + } else if (innerBitOffset + bitsPerSample <= 24) { + const raw = (view.getUint16(byteOffset) << 8) | view.getUint8(byteOffset + 2); + outArray[outIndex] = (raw >> (24 - bitsPerSample - innerBitOffset)) & bitMask; + } else { + outArray[outIndex] = + (view.getUint32(byteOffset) >> (32 - bitsPerSample - innerBitOffset)) & bitMask; + } + + // let outWord = 0; + // for (let bit = 0; bit < bitsPerSample; ++bit) { + // if (inByteArray[bitOffset >> 3] + // & (0x80 >> (bitOffset & 7))) { + // outWord |= (1 << (bitsPerSample - 1 - bit)); + // } + // ++bitOffset; + // } + + // outArray[outIndex] = outWord; + // outArray[pixel] = outWord; + // pixel += 1; + } + // bitOffset = bitOffset + pixelBitSkip - bitsPerSample; + } + } + } else if (format === 3) { + // floating point + // Float16 is handled elsewhere + // normalize 16/24 bit floats to 32 bit floats in the array + // console.time(); + // if (bitsPerSample === 16) { + // for (let byte = 0, outIndex = 0; byte < inBuffer.byteLength; byte += 2, ++outIndex) { + // outArray[outIndex] = getFloat16(view, byte); + // } + // } + // console.timeEnd() + } + + return outArray.buffer; +} + +/** + * GeoTIFF sub-file image. + */ +export default class GeoTIFFImage { + /** + * @param fileDirectory The parsed file directory + * @param geoKeys The parsed geo-keys + * @param dataView The DataView for the underlying file. + * @param littleEndian Whether the file is encoded in little or big endian + * @param cache Whether or not decoded tiles shall be cached + * @param source The datasource to read from + */ + constructor(fileDirectory, geoKeys, dataView, littleEndian, cache, source) { + this.fileDirectory = fileDirectory; + this.geoKeys = geoKeys; + this.dataView = dataView; + this.littleEndian = littleEndian; + this.tiles = cache ? {} : null; + this.isTiled = !fileDirectory.StripOffsets; + const planarConfiguration = fileDirectory.PlanarConfiguration; + this.planarConfiguration = typeof planarConfiguration === 'undefined' ? 1 : planarConfiguration; + if (this.planarConfiguration !== 1 && this.planarConfiguration !== 2) { + throw new Error('Invalid planar configuration.'); + } + + this.source = source; + } + + /** + * Returns the associated parsed file directory. + * @returns the parsed file directory + */ + getFileDirectory() { + return this.fileDirectory; + } + + /** + * Returns the associated parsed geo keys. + * @returns the parsed geo keys + */ + getGeoKeys() { + return this.geoKeys; + } + + /** + * Returns the width of the image. + * @returns the width of the image + */ + getWidth() { + return this.fileDirectory.ImageWidth; + } + + /** + * Returns the height of the image. + * @returns the height of the image + */ + getHeight() { + return this.fileDirectory.ImageLength; + } + + /** + * Returns the number of samples per pixel. + * @returns the number of samples per pixel + */ + getSamplesPerPixel() { + return typeof this.fileDirectory.SamplesPerPixel !== 'undefined' + ? this.fileDirectory.SamplesPerPixel + : 1; + } + + /** + * Returns the width of each tile. + * @returns the width of each tile + */ + getTileWidth() { + return this.isTiled ? this.fileDirectory.TileWidth : this.getWidth(); + } + + /** + * Returns the height of each tile. + * @returns the height of each tile + */ + getTileHeight() { + if (this.isTiled) { + return this.fileDirectory.TileLength; + } + if (typeof this.fileDirectory.RowsPerStrip !== 'undefined') { + return Math.min(this.fileDirectory.RowsPerStrip, this.getHeight()); + } + return this.getHeight(); + } + + /** + * + */ + getBlockWidth() { + return this.getTileWidth(); + } + + /** + * @param y + */ + getBlockHeight(y) { + if (this.isTiled || (y + 1) * this.getTileHeight() <= this.getHeight()) { + return this.getTileHeight(); + } else { + return this.getHeight() - y * this.getTileHeight(); + } + } + + /** + * Calculates the number of bytes for each pixel across all samples. Only full + * bytes are supported, an exception is thrown when this is not the case. + * @returns the bytes per pixel + */ + getBytesPerPixel() { + let bytes = 0; + for (let i = 0; i < this.fileDirectory.BitsPerSample.length; ++i) { + bytes += this.getSampleByteSize(i); + } + return bytes; + } + + /** + * @param i + */ + getSampleByteSize(i) { + if (i >= this.fileDirectory.BitsPerSample.length) { + throw new RangeError(`Sample index ${i} is out of range.`); + } + return Math.ceil(this.fileDirectory.BitsPerSample[i] / 8); + } + + /** + * @param sampleIndex + */ + getReaderForSample(sampleIndex) { + const format = this.fileDirectory.SampleFormat + ? this.fileDirectory.SampleFormat[sampleIndex] + : 1; + const bitsPerSample = this.fileDirectory.BitsPerSample[sampleIndex]; + switch (format) { + case 1: // unsigned integer data + if (bitsPerSample <= 8) { + return DataView.prototype.getUint8; + } else if (bitsPerSample <= 16) { + return DataView.prototype.getUint16; + } else if (bitsPerSample <= 32) { + return DataView.prototype.getUint32; + } + break; + case 2: // twos complement signed integer data + if (bitsPerSample <= 8) { + return DataView.prototype.getInt8; + } else if (bitsPerSample <= 16) { + return DataView.prototype.getInt16; + } else if (bitsPerSample <= 32) { + return DataView.prototype.getInt32; + } + break; + case 3: + switch (bitsPerSample) { + case 16: + return function (offset, littleEndian) { + // TODO: Dataview has this function (or polyfilled) + return getFloat16(this, offset, littleEndian); + }; + case 32: + return DataView.prototype.getFloat32; + case 64: + return DataView.prototype.getFloat64; + default: + break; + } + break; + default: + break; + } + throw Error('Unsupported data format/bitsPerSample'); + } + + /** + * @param sampleIndex + */ + getSampleFormat(sampleIndex = 0) { + return this.fileDirectory.SampleFormat ? this.fileDirectory.SampleFormat[sampleIndex] : 1; + } + + /** + * @param sampleIndex + */ + getBitsPerSample(sampleIndex = 0) { + return this.fileDirectory.BitsPerSample[sampleIndex]; + } + + /** + * @param sampleIndex + * @param size + */ + getArrayForSample(sampleIndex, size) { + const format = this.getSampleFormat(sampleIndex); + const bitsPerSample = this.getBitsPerSample(sampleIndex); + return arrayForType(format, bitsPerSample, size); + } + + /** + * Returns the decoded strip or tile. + * @param x the strip or tile x-offset + * @param y the tile y-offset (0 for stripped images) + * @param sample the sample to get for separated samples + * @param poolOrDecoder the decoder or decoder pool + * @param [signal] An AbortSignal that may be signalled if the request is + * to be aborted + * @returns + */ + async getTileOrStrip(x, y, sample, poolOrDecoder, signal) { + const numTilesPerRow = Math.ceil(this.getWidth() / this.getTileWidth()); + const numTilesPerCol = Math.ceil(this.getHeight() / this.getTileHeight()); + let index; + const { tiles } = this; + if (this.planarConfiguration === 1) { + index = y * numTilesPerRow + x; + } else if (this.planarConfiguration === 2) { + index = sample * numTilesPerRow * numTilesPerCol + y * numTilesPerRow + x; + } + + let offset; + let byteCount; + if (this.isTiled) { + offset = this.fileDirectory.TileOffsets[index]; + byteCount = this.fileDirectory.TileByteCounts[index]; + } else { + offset = this.fileDirectory.StripOffsets[index]; + byteCount = this.fileDirectory.StripByteCounts[index]; + } + const slice = (await this.source.fetch([{ offset, length: byteCount }], signal))[0]; + + let request; + if (tiles === null || !tiles[index]) { + // resolve each request by potentially applying array normalization + request = (async () => { + let data = await poolOrDecoder.decode(this.fileDirectory, slice); + const sampleFormat = this.getSampleFormat(); + const bitsPerSample = this.getBitsPerSample(); + if (needsNormalization(sampleFormat, bitsPerSample)) { + data = normalizeArray( + data, + sampleFormat, + this.planarConfiguration, + this.getSamplesPerPixel(), + bitsPerSample, + this.getTileWidth(), + this.getBlockHeight(y), + ); + } + return data; + })(); + + // set the cache + if (tiles !== null) { + tiles[index] = request; + } + } else { + // get from the cache + request = tiles[index]; + } + + // cache the tile request + return { x, y, sample, data: await request }; + } + + /** + * Internal read function. + * @param imageWindow The image window in pixel coordinates + * @param samples The selected samples (0-based indices) + * @param valueArrays The array(s) to write into + * @param interleave Whether or not to write in an interleaved manner + * @param poolOrDecoder the decoder or decoder pool + * @param width the width of window to be read into + * @param height the height of window to be read into + * @param resampleMethod the resampling method to be used when interpolating + * @param [signal] An AbortSignal that may be signalled if the request is + * to be aborted + * @returns + */ + async _readRaster( + imageWindow, + samples, + valueArrays, + interleave, + poolOrDecoder, + width, + height, + resampleMethod, + signal, + ) { + const tileWidth = this.getTileWidth(); + const tileHeight = this.getTileHeight(); + const imageWidth = this.getWidth(); + const imageHeight = this.getHeight(); + + const minXTile = Math.max(Math.floor(imageWindow[0] / tileWidth), 0); + const maxXTile = Math.min( + Math.ceil(imageWindow[2] / tileWidth), + Math.ceil(imageWidth / tileWidth), + ); + const minYTile = Math.max(Math.floor(imageWindow[1] / tileHeight), 0); + const maxYTile = Math.min( + Math.ceil(imageWindow[3] / tileHeight), + Math.ceil(imageHeight / tileHeight), + ); + const windowWidth = imageWindow[2] - imageWindow[0]; + + let bytesPerPixel = this.getBytesPerPixel(); + + const srcSampleOffsets = []; + const sampleReaders = []; + for (let i = 0; i < samples.length; ++i) { + if (this.planarConfiguration === 1) { + srcSampleOffsets.push(sum(this.fileDirectory.BitsPerSample, 0, samples[i]) / 8); + } else { + srcSampleOffsets.push(0); + } + sampleReaders.push(this.getReaderForSample(samples[i])); + } + + const promises = []; + const { littleEndian } = this; + + for (let yTile = minYTile; yTile < maxYTile; ++yTile) { + for (let xTile = minXTile; xTile < maxXTile; ++xTile) { + let getPromise; + if (this.planarConfiguration === 1) { + getPromise = this.getTileOrStrip(xTile, yTile, 0, poolOrDecoder, signal); + } + for (let sampleIndex = 0; sampleIndex < samples.length; ++sampleIndex) { + const si = sampleIndex; + const sample = samples[sampleIndex]; + if (this.planarConfiguration === 2) { + bytesPerPixel = this.getSampleByteSize(sample); + getPromise = this.getTileOrStrip(xTile, yTile, sample, poolOrDecoder, signal); + } + const promise = getPromise.then((tile) => { + const buffer = tile.data; + const dataView = new DataView(buffer); + const blockHeight = this.getBlockHeight(tile.y); + const firstLine = tile.y * tileHeight; + const firstCol = tile.x * tileWidth; + const lastLine = firstLine + blockHeight; + const lastCol = (tile.x + 1) * tileWidth; + const reader = sampleReaders[si]; + + const ymax = Math.min( + blockHeight, + blockHeight - (lastLine - imageWindow[3]), + imageHeight - firstLine, + ); + const xmax = Math.min( + tileWidth, + tileWidth - (lastCol - imageWindow[2]), + imageWidth - firstCol, + ); + + for (let y = Math.max(0, imageWindow[1] - firstLine); y < ymax; ++y) { + for (let x = Math.max(0, imageWindow[0] - firstCol); x < xmax; ++x) { + const pixelOffset = (y * tileWidth + x) * bytesPerPixel; + const value = reader.call( + dataView, + pixelOffset + srcSampleOffsets[si], + littleEndian, + ); + let windowCoordinate; + if (interleave) { + windowCoordinate = + (y + firstLine - imageWindow[1]) * windowWidth * samples.length + + (x + firstCol - imageWindow[0]) * samples.length + + si; + valueArrays[windowCoordinate] = value; + } else { + windowCoordinate = + (y + firstLine - imageWindow[1]) * windowWidth + x + firstCol - imageWindow[0]; + valueArrays[si][windowCoordinate] = value; + } + } + } + }); + promises.push(promise); + } + } + } + await Promise.all(promises); + + if ( + (width && imageWindow[2] - imageWindow[0] !== width) || + (height && imageWindow[3] - imageWindow[1] !== height) + ) { + let resampled; + if (interleave) { + resampled = resampleInterleaved( + valueArrays, + imageWindow[2] - imageWindow[0], + imageWindow[3] - imageWindow[1], + width, + height, + samples.length, + resampleMethod, + ); + } else { + resampled = resample( + valueArrays, + imageWindow[2] - imageWindow[0], + imageWindow[3] - imageWindow[1], + width, + height, + resampleMethod, + ); + } + resampled.width = width; + resampled.height = height; + return resampled; + } + + valueArrays.width = width || imageWindow[2] - imageWindow[0]; + valueArrays.height = height || imageWindow[3] - imageWindow[1]; + + return valueArrays; + } + + /** + * Reads raster data from the image. This function reads all selected samples + * into separate arrays of the correct type for that sample or into a single + * combined array when `interleave` is set. When provided, only a subset + * of the raster is read for each sample. + * @param [options] optional parameters + * @param options.window + * @param options.samples + * @param options.interleave + * @param options.pool + * @param options.width + * @param options.height + * @param options.resampleMethod + * @param options.fillValue + * @param options.signal + * @returns the decoded arrays as a promise + */ + async readRasters({ + window: wnd, + samples = [], + interleave, + pool = null, + width, + height, + resampleMethod, + fillValue, + signal, + } = {}) { + const imageWindow = wnd || [0, 0, this.getWidth(), this.getHeight()]; + + // check parameters + if (imageWindow[0] > imageWindow[2] || imageWindow[1] > imageWindow[3]) { + throw new Error('Invalid subsets'); + } + + const imageWindowWidth = imageWindow[2] - imageWindow[0]; + const imageWindowHeight = imageWindow[3] - imageWindow[1]; + const numPixels = imageWindowWidth * imageWindowHeight; + const samplesPerPixel = this.getSamplesPerPixel(); + + if (!samples || !samples.length) { + for (let i = 0; i < samplesPerPixel; ++i) { + samples.push(i); + } + } else { + for (let i = 0; i < samples.length; ++i) { + if (samples[i] >= samplesPerPixel) { + return Promise.reject(new RangeError(`Invalid sample index '${samples[i]}'.`)); + } + } + } + let valueArrays; + if (interleave) { + const format = this.fileDirectory.SampleFormat + ? Math.max.apply(null, this.fileDirectory.SampleFormat) + : 1; + const bitsPerSample = Math.max.apply(null, this.fileDirectory.BitsPerSample); + valueArrays = arrayForType(format, bitsPerSample, numPixels * samples.length); + if (fillValue) { + valueArrays.fill(fillValue); + } + } else { + valueArrays = []; + for (let i = 0; i < samples.length; ++i) { + const valueArray = this.getArrayForSample(samples[i], numPixels); + if (Array.isArray(fillValue) && i < fillValue.length) { + valueArray.fill(fillValue[i]); + } else if (fillValue && !Array.isArray(fillValue)) { + valueArray.fill(fillValue); + } + valueArrays.push(valueArray); + } + } + + const poolOrDecoder = pool || (await getDecoder(this.fileDirectory)); + + const result = await this._readRaster( + imageWindow, + samples, + valueArrays, + interleave, + poolOrDecoder, + width, + height, + resampleMethod, + signal, + ); + return result; + } + + /** + * Reads raster data from the image as RGB. The result is always an + * interleaved typed array. + * Colorspaces other than RGB will be transformed to RGB, color maps expanded. + * When no other method is applicable, the first sample is used to produce a + * grayscale image. + * When provided, only a subset of the raster is read for each sample. + * @param [options] optional parameters + * @param [options.window] the subset to read data from in pixels. + * @param [options.interleave] whether the data shall be read + * in one single array or separate + * arrays. + * @param [options.pool] The optional decoder pool to use. + * @param [options.width] The desired width of the output. When the width is no the + * same as the images, resampling will be performed. + * @param [options.height] The desired height of the output. When the width is no the + * same as the images, resampling will be performed. + * @param [options.resampleMethod] The desired resampling method. + * @param [options.enableAlpha] Enable reading alpha channel if present. + * @param [options.signal] An AbortSignal that may be signalled if the request is + * to be aborted + * @returns the RGB array as a Promise + */ + async readRGB({ + window, + interleave = true, + pool = null, + width, + height, + resampleMethod, + enableAlpha = false, + signal, + } = {}) { + const imageWindow = window || [0, 0, this.getWidth(), this.getHeight()]; + + // check parameters + if (imageWindow[0] > imageWindow[2] || imageWindow[1] > imageWindow[3]) { + throw new Error('Invalid subsets'); + } + + const pi = this.fileDirectory.PhotometricInterpretation; + + if (pi === PHOTOMETRIC_INTERPRETATIONS.RGB) { + let s = [0, 1, 2]; + if (!(this.fileDirectory.ExtraSamples === ExtraSamplesValues.Unspecified) && enableAlpha) { + s = []; + for (let i = 0; i < this.fileDirectory.BitsPerSample.length; i += 1) { + s.push(i); + } + } + return this.readRasters({ + window, + interleave, + samples: s, + pool, + width, + height, + resampleMethod, + signal, + }); + } + + let samples; + switch (pi) { + case PHOTOMETRIC_INTERPRETATIONS.WhiteIsZero: + case PHOTOMETRIC_INTERPRETATIONS.BlackIsZero: + case PHOTOMETRIC_INTERPRETATIONS.Palette: + samples = [0]; + break; + case PHOTOMETRIC_INTERPRETATIONS.CMYK: + samples = [0, 1, 2, 3]; + break; + case PHOTOMETRIC_INTERPRETATIONS.YCbCr: + case PHOTOMETRIC_INTERPRETATIONS.CIELab: + samples = [0, 1, 2]; + break; + default: + throw new Error('Invalid or unsupported photometric interpretation.'); + } + + const subOptions = { + window: imageWindow, + interleave: true, + samples, + pool, + width, + height, + resampleMethod, + signal, + }; + const { fileDirectory } = this; + const raster = await this.readRasters(subOptions); + + const max = 2 ** this.fileDirectory.BitsPerSample[0]; + let data; + switch (pi) { + case PHOTOMETRIC_INTERPRETATIONS.WhiteIsZero: + data = fromWhiteIsZero(raster, max); + break; + case PHOTOMETRIC_INTERPRETATIONS.BlackIsZero: + data = fromBlackIsZero(raster, max); + break; + case PHOTOMETRIC_INTERPRETATIONS.Palette: + data = fromPalette(raster, fileDirectory.ColorMap); + break; + case PHOTOMETRIC_INTERPRETATIONS.CMYK: + data = fromCMYK(raster); + break; + case PHOTOMETRIC_INTERPRETATIONS.YCbCr: + data = fromYCbCr(raster); + break; + case PHOTOMETRIC_INTERPRETATIONS.CIELab: + data = fromCIELab(raster); + break; + default: + throw new Error('Unsupported photometric interpretation.'); + } + + // if non-interleaved data is requested, we must split the channels + // into their respective arrays + if (!interleave) { + const red = new Uint8Array(data.length / 3); + const green = new Uint8Array(data.length / 3); + const blue = new Uint8Array(data.length / 3); + for (let i = 0, j = 0; i < data.length; i += 3, ++j) { + red[j] = data[i]; + green[j] = data[i + 1]; + blue[j] = data[i + 2]; + } + data = [red, green, blue]; + } + + data.width = raster.width; + data.height = raster.height; + return data; + } + + /** + * Returns an array of tiepoints. + * @returns + */ + getTiePoints() { + if (!this.fileDirectory.ModelTiepoint) { + return []; + } + + const tiePoints = []; + for (let i = 0; i < this.fileDirectory.ModelTiepoint.length; i += 6) { + tiePoints.push({ + i: this.fileDirectory.ModelTiepoint[i], + j: this.fileDirectory.ModelTiepoint[i + 1], + k: this.fileDirectory.ModelTiepoint[i + 2], + x: this.fileDirectory.ModelTiepoint[i + 3], + y: this.fileDirectory.ModelTiepoint[i + 4], + z: this.fileDirectory.ModelTiepoint[i + 5], + }); + } + return tiePoints; + } + + /** + * Returns the parsed GDAL metadata items. + * + * If sample is passed to null, dataset-level metadata will be returned. + * Otherwise only metadata specific to the provided sample will be returned. + * @param [sample] The sample index. + * @returns + */ + getGDALMetadata(sample = null) { + const metadata = {}; + if (!this.fileDirectory.GDAL_METADATA) { + return null; + } + const string = this.fileDirectory.GDAL_METADATA; + + let items = findTagsByName(string, 'Item'); + + if (sample === null) { + items = items.filter((item) => getAttribute(item, 'sample') === undefined); + } else { + items = items.filter((item) => Number(getAttribute(item, 'sample')) === sample); + } + + for (let i = 0; i < items.length; ++i) { + const item = items[i]; + metadata[getAttribute(item, 'name')] = item.inner; + } + return metadata; + } + + /** + * Returns the GDAL nodata value + * @returns + */ + getGDALNoData() { + if (!this.fileDirectory.GDAL_NODATA) { + return null; + } + const string = this.fileDirectory.GDAL_NODATA; + return Number(string.substring(0, string.length - 1)); + } + + /** + * Returns the image origin as a XYZ-vector. When the image has no affine + * transformation, then an exception is thrown. + * @returns The origin as a vector + */ + getOrigin() { + const tiePoints = this.fileDirectory.ModelTiepoint; + const modelTransformation = this.fileDirectory.ModelTransformation; + if (tiePoints && tiePoints.length === 6) { + return [tiePoints[3], tiePoints[4], tiePoints[5]]; + } + if (modelTransformation) { + return [modelTransformation[3], modelTransformation[7], modelTransformation[11]]; + } + throw new Error('The image does not have an affine transformation.'); + } + + /** + * Returns the image resolution as a XYZ-vector. When the image has no affine + * transformation, then an exception is thrown. + * @param [referenceImage] A reference image to calculate the resolution from + * in cases when the current image does not have the + * required tags on its own. + * @returns The resolution as a vector + */ + getResolution(referenceImage = null) { + const modelPixelScale = this.fileDirectory.ModelPixelScale; + const modelTransformation = this.fileDirectory.ModelTransformation; + + if (modelPixelScale) { + return [modelPixelScale[0], -modelPixelScale[1], modelPixelScale[2]]; + } + if (modelTransformation) { + if (modelTransformation[1] === 0 && modelTransformation[4] === 0) { + return [modelTransformation[0], -modelTransformation[5], modelTransformation[10]]; + } + return [ + Math.sqrt( + modelTransformation[0] * modelTransformation[0] + + modelTransformation[4] * modelTransformation[4], + ), + -Math.sqrt( + modelTransformation[1] * modelTransformation[1] + + modelTransformation[5] * modelTransformation[5], + ), + modelTransformation[10], + ]; + } + + if (referenceImage) { + const [refResX, refResY, refResZ] = referenceImage.getResolution(); + return [ + (refResX * referenceImage.getWidth()) / this.getWidth(), + (refResY * referenceImage.getHeight()) / this.getHeight(), + (refResZ * referenceImage.getWidth()) / this.getWidth(), + ]; + } + + throw new Error('The image does not have an affine transformation.'); + } + + /** + * Returns whether or not the pixels of the image depict an area (or point). + * @returns Whether the pixels are a point + */ + pixelIsArea() { + return this.geoKeys.GTRasterTypeGeoKey === 1; + } + + /** + * Returns the image bounding box as an array of 4 values: min-x, min-y, + * max-x and max-y. When the image has no affine transformation, then an + * exception is thrown. + * @param [tilegrid] If true return extent for a tilegrid + * without adjustment for ModelTransformation. + * @returns The bounding box + */ + getBoundingBox(tilegrid = false) { + const height = this.getHeight(); + const width = this.getWidth(); + + if (this.fileDirectory.ModelTransformation && !tilegrid) { + const [a, b, c, d, e, f, g, h] = this.fileDirectory.ModelTransformation; + + const corners = [ + [0, 0], + [0, height], + [width, 0], + [width, height], + ]; + + const projected = corners.map(([I, J]) => [d + a * I + b * J, h + e * I + f * J]); + + const xs = projected.map((pt) => pt[0]); + const ys = projected.map((pt) => pt[1]); + + return [Math.min(...xs), Math.min(...ys), Math.max(...xs), Math.max(...ys)]; + } else { + const origin = this.getOrigin(); + const resolution = this.getResolution(); + + const x1 = origin[0]; + const y1 = origin[1]; + + const x2 = x1 + resolution[0] * width; + const y2 = y1 + resolution[1] * height; + + return [Math.min(x1, x2), Math.min(y1, y2), Math.max(x1, x2), Math.max(y1, y2)]; + } + } +} diff --git a/src/readers/geotiff/globals.ts b/src/readers/geotiff/globals.ts new file mode 100644 index 00000000..8ba86c3a --- /dev/null +++ b/src/readers/geotiff/globals.ts @@ -0,0 +1,306 @@ +export const FIELD_TAG_NAMES = { + // TIFF Baseline + 0x013b: 'Artist', + 0x0102: 'BitsPerSample', + 0x0109: 'CellLength', + 0x0108: 'CellWidth', + 0x0140: 'ColorMap', + 0x0103: 'Compression', + 0x8298: 'Copyright', + 0x0132: 'DateTime', + 0x0152: 'ExtraSamples', + 0x010a: 'FillOrder', + 0x0121: 'FreeByteCounts', + 0x0120: 'FreeOffsets', + 0x0123: 'GrayResponseCurve', + 0x0122: 'GrayResponseUnit', + 0x013c: 'HostComputer', + 0x010e: 'ImageDescription', + 0x0101: 'ImageLength', + 0x0100: 'ImageWidth', + 0x010f: 'Make', + 0x0119: 'MaxSampleValue', + 0x0118: 'MinSampleValue', + 0x0110: 'Model', + 0x00fe: 'NewSubfileType', + 0x0112: 'Orientation', + 0x0106: 'PhotometricInterpretation', + 0x011c: 'PlanarConfiguration', + 0x0128: 'ResolutionUnit', + 0x0116: 'RowsPerStrip', + 0x0115: 'SamplesPerPixel', + 0x0131: 'Software', + 0x0117: 'StripByteCounts', + 0x0111: 'StripOffsets', + 0x00ff: 'SubfileType', + 0x0107: 'Threshholding', + 0x011a: 'XResolution', + 0x011b: 'YResolution', + + // TIFF Extended + 0x0146: 'BadFaxLines', + 0x0147: 'CleanFaxData', + 0x0157: 'ClipPath', + 0x0148: 'ConsecutiveBadFaxLines', + 0x01b1: 'Decode', + 0x01b2: 'DefaultImageColor', + 0x010d: 'DocumentName', + 0x0150: 'DotRange', + 0x0141: 'HalftoneHints', + 0x015a: 'Indexed', + 0x015b: 'JPEGTables', + 0x011d: 'PageName', + 0x0129: 'PageNumber', + 0x013d: 'Predictor', + 0x013f: 'PrimaryChromaticities', + 0x0214: 'ReferenceBlackWhite', + 0x0153: 'SampleFormat', + 0x0154: 'SMinSampleValue', + 0x0155: 'SMaxSampleValue', + 0x022f: 'StripRowCounts', + 0x014a: 'SubIFDs', + 0x0124: 'T4Options', + 0x0125: 'T6Options', + 0x0145: 'TileByteCounts', + 0x0143: 'TileLength', + 0x0144: 'TileOffsets', + 0x0142: 'TileWidth', + 0x012d: 'TransferFunction', + 0x013e: 'WhitePoint', + 0x0158: 'XClipPathUnits', + 0x011e: 'XPosition', + 0x0211: 'YCbCrCoefficients', + 0x0213: 'YCbCrPositioning', + 0x0212: 'YCbCrSubSampling', + 0x0159: 'YClipPathUnits', + 0x011f: 'YPosition', + + // EXIF + 0x9202: 'ApertureValue', + 0xa001: 'ColorSpace', + 0x9004: 'DateTimeDigitized', + 0x9003: 'DateTimeOriginal', + 0x8769: 'Exif IFD', + 0x9000: 'ExifVersion', + 0x829a: 'ExposureTime', + 0xa300: 'FileSource', + 0x9209: 'Flash', + 0xa000: 'FlashpixVersion', + 0x829d: 'FNumber', + 0xa420: 'ImageUniqueID', + 0x9208: 'LightSource', + 0x927c: 'MakerNote', + 0x9201: 'ShutterSpeedValue', + 0x9286: 'UserComment', + + // IPTC + 0x83bb: 'IPTC', + + // ICC + 0x8773: 'ICC Profile', + + // XMP + 0x02bc: 'XMP', + + // GDAL + 0xa480: 'GDAL_METADATA', + 0xa481: 'GDAL_NODATA', + + // Photoshop + 0x8649: 'Photoshop', + + // GeoTiff + 0x830e: 'ModelPixelScale', + 0x8482: 'ModelTiepoint', + 0x85d8: 'ModelTransformation', + 0x87af: 'GeoKeyDirectory', + 0x87b0: 'GeoDoubleParams', + 0x87b1: 'GeoAsciiParams', + + // LERC + 0xc5f2: 'LercParameters', +}; + +export const fieldTagTypes = { + 256: 'SHORT', + 257: 'SHORT', + 258: 'SHORT', + 259: 'SHORT', + 262: 'SHORT', + 270: 'ASCII', + 271: 'ASCII', + 272: 'ASCII', + 273: 'LONG', + 274: 'SHORT', + 277: 'SHORT', + 278: 'LONG', + 279: 'LONG', + 282: 'RATIONAL', + 283: 'RATIONAL', + 284: 'SHORT', + 286: 'SHORT', + 287: 'RATIONAL', + 296: 'SHORT', + 297: 'SHORT', + 305: 'ASCII', + 306: 'ASCII', + 315: 'ASCII', + 338: 'SHORT', + 339: 'SHORT', + 513: 'LONG', + 514: 'LONG', + 1024: 'SHORT', + 1025: 'SHORT', + 2048: 'SHORT', + 2049: 'ASCII', + 3072: 'SHORT', + 3073: 'ASCII', + 33432: 'ASCII', + 33550: 'DOUBLE', + 33922: 'DOUBLE', + 34264: 'DOUBLE', + 34665: 'LONG', + 34735: 'SHORT', + 34736: 'DOUBLE', + 34737: 'ASCII', + 42113: 'ASCII', +}; + +export const ARRAY_FIELDS: number[] = [ + 0x0102, // BitsPerSample + 0x0152, // ExtraSamples + 0x0153, // SampleFormat + 0x0117, // StripByteCounts + 0x0111, // StripOffsets + 0x022f, // StripRowCounts + 0x0145, // TileByteCounts + 0x0144, // TileOffsets + 0x014a, // SubIFDs +]; + +export const FIELD_TYPE_NAMES = { + 0x0001: 'BYTE', + 0x0002: 'ASCII', + 0x0003: 'SHORT', + 0x0004: 'LONG', + 0x0005: 'RATIONAL', + 0x0006: 'SBYTE', + 0x0007: 'UNDEFINED', + 0x0008: 'SSHORT', + 0x0009: 'SLONG', + 0x000a: 'SRATIONAL', + 0x000b: 'FLOAT', + 0x000c: 'DOUBLE', + // IFD offset, suggested by https://owl.phy.queensu.ca/~phil/exiftool/standards.html + 0x000d: 'IFD', + // introduced by BigTIFF + 0x0010: 'LONG8', + 0x0011: 'SLONG8', + 0x0012: 'IFD8', +}; + +export const FIELD_TYPES = { + BYTE: 0x0001, + ASCII: 0x0002, + SHORT: 0x0003, + LONG: 0x0004, + RATIONAL: 0x0005, + SBYTE: 0x0006, + UNDEFINED: 0x0007, + SSHORT: 0x0008, + SLONG: 0x0009, + SRATIONAL: 0x000a, + FLOAT: 0x000b, + DOUBLE: 0x000c, + // IFD offset, suggested by https://owl.phy.queensu.ca/~phil/exiftool/standards.html + IFD: 0x000d, + // introduced by BigTIFF + LONG8: 0x0010, + SLONG8: 0x0011, + IFD8: 0x0012, +}; + +export const PHOTOMETRIC_INTERPRETATIONS = { + WhiteIsZero: 0, + BlackIsZero: 1, + RGB: 2, + Palette: 3, + TransparencyMask: 4, + CMYK: 5, + YCbCr: 6, + + CIELab: 8, + ICCLab: 9, +}; + +export const ExtraSamplesValues = { + Unspecified: 0, + Assocalpha: 1, + Unassalpha: 2, +}; + +export const LercParameters = { + Version: 0, + AddCompression: 1, +}; + +export const LercAddCompression = { + None: 0, + Deflate: 1, + Zstandard: 2, +}; + +export const geoKeyNames = { + 1024: 'GTModelTypeGeoKey', + 1025: 'GTRasterTypeGeoKey', + 1026: 'GTCitationGeoKey', + 2048: 'GeographicTypeGeoKey', + 2049: 'GeogCitationGeoKey', + 2050: 'GeogGeodeticDatumGeoKey', + 2051: 'GeogPrimeMeridianGeoKey', + 2052: 'GeogLinearUnitsGeoKey', + 2053: 'GeogLinearUnitSizeGeoKey', + 2054: 'GeogAngularUnitsGeoKey', + 2055: 'GeogAngularUnitSizeGeoKey', + 2056: 'GeogEllipsoidGeoKey', + 2057: 'GeogSemiMajorAxisGeoKey', + 2058: 'GeogSemiMinorAxisGeoKey', + 2059: 'GeogInvFlatteningGeoKey', + 2060: 'GeogAzimuthUnitsGeoKey', + 2061: 'GeogPrimeMeridianLongGeoKey', + 2062: 'GeogTOWGS84GeoKey', + 3072: 'ProjectedCSTypeGeoKey', + 3073: 'PCSCitationGeoKey', + 3074: 'ProjectionGeoKey', + 3075: 'ProjCoordTransGeoKey', + 3076: 'ProjLinearUnitsGeoKey', + 3077: 'ProjLinearUnitSizeGeoKey', + 3078: 'ProjStdParallel1GeoKey', + 3079: 'ProjStdParallel2GeoKey', + 3080: 'ProjNatOriginLongGeoKey', + 3081: 'ProjNatOriginLatGeoKey', + 3082: 'ProjFalseEastingGeoKey', + 3083: 'ProjFalseNorthingGeoKey', + 3084: 'ProjFalseOriginLongGeoKey', + 3085: 'ProjFalseOriginLatGeoKey', + 3086: 'ProjFalseOriginEastingGeoKey', + 3087: 'ProjFalseOriginNorthingGeoKey', + 3088: 'ProjCenterLongGeoKey', + 3089: 'ProjCenterLatGeoKey', + 3090: 'ProjCenterEastingGeoKey', + 3091: 'ProjCenterNorthingGeoKey', + 3092: 'ProjScaleAtNatOriginGeoKey', + 3093: 'ProjScaleAtCenterGeoKey', + 3094: 'ProjAzimuthAngleGeoKey', + 3095: 'ProjStraightVertPoleLongGeoKey', + 3096: 'ProjRectifiedGridAngleGeoKey', + 4096: 'VerticalCSTypeGeoKey', + 4097: 'VerticalCitationGeoKey', + 4098: 'VerticalDatumGeoKey', + 4099: 'VerticalUnitsGeoKey', +}; + +// export const geoKeys = {}; +// for (const key in geoKeyNames) { +// geoKeys[geoKeyNames[key]] = parseInt(key, 10); +// } diff --git a/src/readers/geotiff/predictor.ts b/src/readers/geotiff/predictor.ts new file mode 100644 index 00000000..0b15754a --- /dev/null +++ b/src/readers/geotiff/predictor.ts @@ -0,0 +1,121 @@ +/** + * @param row + * @param stride + */ +function decodeRowAcc(row, stride) { + let length = row.length - stride; + let offset = 0; + do { + for (let i = stride; i > 0; i--) { + row[offset + stride] += row[offset]; + offset++; + } + + length -= stride; + } while (length > 0); +} + +/** + * @param row + * @param stride + * @param bytesPerSample + */ +function decodeRowFloatingPoint(row, stride, bytesPerSample) { + let index = 0; + let count = row.length; + const wc = count / bytesPerSample; + + while (count > stride) { + for (let i = stride; i > 0; --i) { + row[index + stride] += row[index]; + ++index; + } + count -= stride; + } + + const copy = row.slice(); + for (let i = 0; i < wc; ++i) { + for (let b = 0; b < bytesPerSample; ++b) { + row[bytesPerSample * i + b] = copy[(bytesPerSample - b - 1) * wc + i]; + } + } +} + +/** + * @param block + * @param predictor + * @param width + * @param height + * @param bitsPerSample + * @param planarConfiguration + */ +export function applyPredictor( + block, + predictor, + width, + height, + bitsPerSample, + planarConfiguration, +) { + if (!predictor || predictor === 1) { + return block; + } + + for (let i = 0; i < bitsPerSample.length; ++i) { + if (bitsPerSample[i] % 8 !== 0) { + throw new Error('When decoding with predictor, only multiple of 8 bits are supported.'); + } + if (bitsPerSample[i] !== bitsPerSample[0]) { + throw new Error('When decoding with predictor, all samples must have the same size.'); + } + } + + const bytesPerSample = bitsPerSample[0] / 8; + const stride = planarConfiguration === 2 ? 1 : bitsPerSample.length; + + for (let i = 0; i < height; ++i) { + // Last strip will be truncated if height % stripHeight != 0 + if (i * stride * width * bytesPerSample >= block.byteLength) { + break; + } + let row; + if (predictor === 2) { + // horizontal prediction + switch (bitsPerSample[0]) { + case 8: + row = new Uint8Array( + block, + i * stride * width * bytesPerSample, + stride * width * bytesPerSample, + ); + break; + case 16: + row = new Uint16Array( + block, + i * stride * width * bytesPerSample, + (stride * width * bytesPerSample) / 2, + ); + break; + case 32: + row = new Uint32Array( + block, + i * stride * width * bytesPerSample, + (stride * width * bytesPerSample) / 4, + ); + break; + default: + throw new Error(`Predictor 2 not allowed with ${bitsPerSample[0]} bits per sample.`); + } + decodeRowAcc(row, stride, bytesPerSample); + } else if (predictor === 3) { + // horizontal floating point + row = new Uint8Array( + block, + i * stride * width * bytesPerSample, + stride * width * bytesPerSample, + ); + decodeRowFloatingPoint(row, stride, bytesPerSample); + } + } + return block; +} diff --git a/src/readers/geotiff/resample.ts b/src/readers/geotiff/resample.ts new file mode 100644 index 00000000..73a8a3fd --- /dev/null +++ b/src/readers/geotiff/resample.ts @@ -0,0 +1,271 @@ +/** + * @param array + * @param width + * @param height + * @param samplesPerPixel + */ +function copyNewSize( + array: number[], + width: number, + height: number, + samplesPerPixel = 1, +): number[] { + return new (Object.getPrototypeOf(array).constructor)(width * height * samplesPerPixel); +} + +/** + * Resample the input arrays using nearest neighbor value selection. + * @param valueArrays The input arrays to resample + * @param inWidth The width of the input rasters + * @param inHeight The height of the input rasters + * @param outWidth The desired width of the output rasters + * @param outHeight The desired height of the output rasters + * @returns The resampled rasters + */ +export function resampleNearest( + valueArrays: number[][], + inWidth: number, + inHeight: number, + outWidth: number, + outHeight: number, +): number[][] { + const relX = inWidth / outWidth; + const relY = inHeight / outHeight; + return valueArrays.map((array) => { + const newArray = copyNewSize(array, outWidth, outHeight); + for (let y = 0; y < outHeight; ++y) { + const cy = Math.min(Math.round(relY * y), inHeight - 1); + for (let x = 0; x < outWidth; ++x) { + const cx = Math.min(Math.round(relX * x), inWidth - 1); + const value = array[cy * inWidth + cx]; + newArray[y * outWidth + x] = value; + } + } + return newArray; + }); +} + +/** + * simple linear interpolation, code from: + * https://en.wikipedia.org/wiki/Linear_interpolation#Programming_language_support + * @param v0 + * @param v1 + * @param t + */ +function lerp(v0: number, v1: number, t: number): number { + return (1 - t) * v0 + t * v1; +} + +/** + * Resample the input arrays using bilinear interpolation. + * @param valueArrays The input arrays to resample + * @param inWidth The width of the input rasters + * @param inHeight The height of the input rasters + * @param outWidth The desired width of the output rasters + * @param outHeight The desired height of the output rasters + * @returns The resampled rasters + */ +export function resampleBilinear( + valueArrays: number[][], + inWidth: number, + inHeight: number, + outWidth: number, + outHeight: number, +): number[][] { + const relX = inWidth / outWidth; + const relY = inHeight / outHeight; + + return valueArrays.map((array) => { + const newArray = copyNewSize(array, outWidth, outHeight); + for (let y = 0; y < outHeight; ++y) { + const rawY = relY * y; + + const yl = Math.floor(rawY); + const yh = Math.min(Math.ceil(rawY), inHeight - 1); + + for (let x = 0; x < outWidth; ++x) { + const rawX = relX * x; + const tx = rawX % 1; + + const xl = Math.floor(rawX); + const xh = Math.min(Math.ceil(rawX), inWidth - 1); + + const ll = array[yl * inWidth + xl]; + const hl = array[yl * inWidth + xh]; + const lh = array[yh * inWidth + xl]; + const hh = array[yh * inWidth + xh]; + + const value = lerp(lerp(ll, hl, tx), lerp(lh, hh, tx), rawY % 1); + newArray[y * outWidth + x] = value; + } + } + return newArray; + }); +} + +/** + * + */ +export type Method = 'nearest' | 'bilinear' | 'linear'; + +/** + * Resample the input arrays using the selected resampling method. + * @param valueArrays The input arrays to resample + * @param inWidth The width of the input rasters + * @param inHeight The height of the input rasters + * @param outWidth The desired width of the output rasters + * @param outHeight The desired height of the output rasters + * @param [method] The desired resampling method + * @returns The resampled rasters + */ +export function resample( + valueArrays: number[][], + inWidth: number, + inHeight: number, + outWidth: number, + outHeight: number, + method: Method = 'nearest', +): number[][] { + switch (method) { + case 'nearest': + return resampleNearest(valueArrays, inWidth, inHeight, outWidth, outHeight); + case 'bilinear': + case 'linear': + return resampleBilinear(valueArrays, inWidth, inHeight, outWidth, outHeight); + default: + throw new Error(`Unsupported resampling method: '${method}'`); + } +} + +/** + * Resample the pixel interleaved input array using nearest neighbor value selection. + * @param valueArrays The input arrays to resample + * @param valueArray + * @param inWidth The width of the input rasters + * @param inHeight The height of the input rasters + * @param outWidth The desired width of the output rasters + * @param outHeight The desired height of the output rasters + * @param samples The number of samples per pixel for pixel + * interleaved data + * @returns The resampled raster + */ +export function resampleNearestInterleaved( + valueArray: number[], + inWidth: number, + inHeight: number, + outWidth: number, + outHeight: number, + samples: number, +): number[] { + const relX = inWidth / outWidth; + const relY = inHeight / outHeight; + + const newArray = copyNewSize(valueArray, outWidth, outHeight, samples); + for (let y = 0; y < outHeight; ++y) { + const cy = Math.min(Math.round(relY * y), inHeight - 1); + for (let x = 0; x < outWidth; ++x) { + const cx = Math.min(Math.round(relX * x), inWidth - 1); + for (let i = 0; i < samples; ++i) { + const value = valueArray[cy * inWidth * samples + cx * samples + i]; + newArray[y * outWidth * samples + x * samples + i] = value; + } + } + } + return newArray; +} + +/** + * Resample the pixel interleaved input array using bilinear interpolation. + * @param valueArrays The input arrays to resample + * @param valueArray + * @param inWidth The width of the input rasters + * @param inHeight The height of the input rasters + * @param outWidth The desired width of the output rasters + * @param outHeight The desired height of the output rasters + * @param samples The number of samples per pixel for pixel + * interleaved data + * @returns The resampled raster + */ +export function resampleBilinearInterleaved( + valueArray: number[], + inWidth: number, + inHeight: number, + outWidth: number, + outHeight: number, + samples: number, +): number[] { + const relX = inWidth / outWidth; + const relY = inHeight / outHeight; + const newArray = copyNewSize(valueArray, outWidth, outHeight, samples); + for (let y = 0; y < outHeight; ++y) { + const rawY = relY * y; + + const yl = Math.floor(rawY); + const yh = Math.min(Math.ceil(rawY), inHeight - 1); + + for (let x = 0; x < outWidth; ++x) { + const rawX = relX * x; + const tx = rawX % 1; + + const xl = Math.floor(rawX); + const xh = Math.min(Math.ceil(rawX), inWidth - 1); + + for (let i = 0; i < samples; ++i) { + const ll = valueArray[yl * inWidth * samples + xl * samples + i]; + const hl = valueArray[yl * inWidth * samples + xh * samples + i]; + const lh = valueArray[yh * inWidth * samples + xl * samples + i]; + const hh = valueArray[yh * inWidth * samples + xh * samples + i]; + + const value = lerp(lerp(ll, hl, tx), lerp(lh, hh, tx), rawY % 1); + newArray[y * outWidth * samples + x * samples + i] = value; + } + } + } + return newArray; +} + +/** + * Resample the pixel interleaved input array using the selected resampling method. + * @param valueArray The input array to resample + * @param inWidth The width of the input rasters + * @param inHeight The height of the input rasters + * @param outWidth The desired width of the output rasters + * @param outHeight The desired height of the output rasters + * @param samples The number of samples per pixel for pixel + * interleaved data + * @param [method] The desired resampling method + * @returns The resampled rasters + */ +export function resampleInterleaved( + valueArray: number[], + inWidth: number, + inHeight: number, + outWidth: number, + outHeight: number, + samples: number, + method: Method = 'nearest', +): number[] { + switch (method) { + case 'nearest': + return resampleNearestInterleaved( + valueArray, + inWidth, + inHeight, + outWidth, + outHeight, + samples, + ); + case 'bilinear': + case 'linear': + return resampleBilinearInterleaved( + valueArray, + inWidth, + inHeight, + outWidth, + outHeight, + samples, + ); + default: + throw new Error(`Unsupported resampling method: '${method}'`); + } +} diff --git a/src/readers/geotiff/rgb.ts b/src/readers/geotiff/rgb.ts new file mode 100644 index 00000000..2d19a714 --- /dev/null +++ b/src/readers/geotiff/rgb.ts @@ -0,0 +1,132 @@ +/** + * @param raster + * @param max + */ +export function fromWhiteIsZero(raster: Image, max: number): Uint8Array { + const { width, height } = raster; + const rgbRaster = new Uint8Array(width * height * 3); + let value; + for (let i = 0, j = 0; i < raster.length; ++i, j += 3) { + value = 256 - (raster[i] / max) * 256; + rgbRaster[j] = value; + rgbRaster[j + 1] = value; + rgbRaster[j + 2] = value; + } + return rgbRaster; +} + +/** + * @param raster + * @param max + */ +export function fromBlackIsZero(raster, max) { + const { width, height } = raster; + const rgbRaster = new Uint8Array(width * height * 3); + let value; + for (let i = 0, j = 0; i < raster.length; ++i, j += 3) { + value = (raster[i] / max) * 256; + rgbRaster[j] = value; + rgbRaster[j + 1] = value; + rgbRaster[j + 2] = value; + } + return rgbRaster; +} + +/** + * @param raster + * @param colorMap + */ +export function fromPalette(raster, colorMap) { + const { width, height } = raster; + const rgbRaster = new Uint8Array(width * height * 3); + const greenOffset = colorMap.length / 3; + const blueOffset = (colorMap.length / 3) * 2; + for (let i = 0, j = 0; i < raster.length; ++i, j += 3) { + const mapIndex = raster[i]; + rgbRaster[j] = (colorMap[mapIndex] / 65536) * 256; + rgbRaster[j + 1] = (colorMap[mapIndex + greenOffset] / 65536) * 256; + rgbRaster[j + 2] = (colorMap[mapIndex + blueOffset] / 65536) * 256; + } + return rgbRaster; +} + +/** + * @param cmykRaster + */ +export function fromCMYK(cmykRaster) { + const { width, height } = cmykRaster; + const rgbRaster = new Uint8Array(width * height * 3); + for (let i = 0, j = 0; i < cmykRaster.length; i += 4, j += 3) { + const c = cmykRaster[i]; + const m = cmykRaster[i + 1]; + const y = cmykRaster[i + 2]; + const k = cmykRaster[i + 3]; + + rgbRaster[j] = 255 * ((255 - c) / 256) * ((255 - k) / 256); + rgbRaster[j + 1] = 255 * ((255 - m) / 256) * ((255 - k) / 256); + rgbRaster[j + 2] = 255 * ((255 - y) / 256) * ((255 - k) / 256); + } + return rgbRaster; +} + +/** + * @param yCbCrRaster + */ +export function fromYCbCr(yCbCrRaster) { + const { width, height } = yCbCrRaster; + const rgbRaster = new Uint8ClampedArray(width * height * 3); + for (let i = 0, j = 0; i < yCbCrRaster.length; i += 3, j += 3) { + const y = yCbCrRaster[i]; + const cb = yCbCrRaster[i + 1]; + const cr = yCbCrRaster[i + 2]; + + rgbRaster[j] = y + 1.402 * (cr - 0x80); + rgbRaster[j + 1] = y - 0.34414 * (cb - 0x80) - 0.71414 * (cr - 0x80); + rgbRaster[j + 2] = y + 1.772 * (cb - 0x80); + } + return rgbRaster; +} + +const Xn = 0.95047; +const Yn = 1.0; +const Zn = 1.08883; + +// from https://github.com/antimatter15/rgb-lab/blob/master/color.js + +/** + * @param cieLabRaster + */ +export function fromCIELab(cieLabRaster) { + const { width, height } = cieLabRaster; + const rgbRaster = new Uint8Array(width * height * 3); + + for (let i = 0, j = 0; i < cieLabRaster.length; i += 3, j += 3) { + const L = cieLabRaster[i + 0]; + const a_ = (cieLabRaster[i + 1] << 24) >> 24; // conversion from uint8 to int8 + const b_ = (cieLabRaster[i + 2] << 24) >> 24; // same + + let y = (L + 16) / 116; + let x = a_ / 500 + y; + let z = y - b_ / 200; + let r; + let g; + let b; + + x = Xn * (x * x * x > 0.008856 ? x * x * x : (x - 16 / 116) / 7.787); + y = Yn * (y * y * y > 0.008856 ? y * y * y : (y - 16 / 116) / 7.787); + z = Zn * (z * z * z > 0.008856 ? z * z * z : (z - 16 / 116) / 7.787); + + r = x * 3.2406 + y * -1.5372 + z * -0.4986; + g = x * -0.9689 + y * 1.8758 + z * 0.0415; + b = x * 0.0557 + y * -0.204 + z * 1.057; + + r = r > 0.0031308 ? 1.055 * r ** (1 / 2.4) - 0.055 : 12.92 * r; + g = g > 0.0031308 ? 1.055 * g ** (1 / 2.4) - 0.055 : 12.92 * g; + b = b > 0.0031308 ? 1.055 * b ** (1 / 2.4) - 0.055 : 12.92 * b; + + rgbRaster[j] = Math.max(0, Math.min(1, r)) * 255; + rgbRaster[j + 1] = Math.max(0, Math.min(1, g)) * 255; + rgbRaster[j + 2] = Math.max(0, Math.min(1, b)) * 255; + } + return rgbRaster; +} diff --git a/src/readers/geotiff/source/blockedsource.ts b/src/readers/geotiff/source/blockedsource.ts new file mode 100644 index 00000000..81949eb3 --- /dev/null +++ b/src/readers/geotiff/source/blockedsource.ts @@ -0,0 +1,300 @@ +// import QuickLRU from 'quick-lru'; // TODO: REplace with cache from dataStore +import Cache from 's2-tools/dataStructures/cache'; +import { BaseSource } from './basesource'; +import { wait, zip } from '../utils'; + +/** A Block is a storage unit explaining a block of data and may contain the data itself */ +class Block { + /** + * @param offset - byte offset + * @param length - byte length + * @param [data] - the actual data + */ + constructor( + public offset: number, + public length: number, + public data?: ArrayBufferLike, + ) {} + + /** @returns the top byte border */ + get top() { + return this.offset + this.length; + } +} + +/** + * + */ +class BlockGroup { + /** + * @param offset + * @param length + * @param blockIds + */ + constructor(offset, length, blockIds) { + this.offset = offset; + this.length = length; + this.blockIds = blockIds; + } +} + +/** + * + */ +export class BlockedSource extends BaseSource { + /** + * @param source The underlying source that shall be blocked and cached + * @param options + * @param [options.blockSize] + * @param [options.cacheSize] + */ + constructor(source, { blockSize = 65536, cacheSize = 100 } = {}) { + super(); + this.source = source; + this.blockSize = blockSize; + + // this.blockCache = new Cache({ + // maxSize: cacheSize, + // /** + // * @param blockId + // * @param block + // */ + // onEviction: (blockId, block) => { + // this.evictedBlocks.set(blockId, block); + // }, + // }); + this.blockCache = new Cache(cacheSize, (blockId, block) => { + this.evictedBlocks.set(blockId, block); + }); + + this.evictedBlocks = new Map(); + + // mapping blockId -> Block instance + this.blockRequests = new Map(); + + // set of blockIds missing for the current requests + this.blockIdsToFetch = new Set(); + + this.abortedBlockIds = new Set(); + } + + /** + * + */ + get fileSize() { + return this.source.fileSize; + } + + /** + * @param slices + * @param signal + */ + async fetch(slices, signal) { + const blockRequests = []; + const missingBlockIds = []; + const allBlockIds = []; + this.evictedBlocks.clear(); + + for (const { offset, length } of slices) { + let top = offset + length; + + const { fileSize } = this; + if (fileSize !== null) { + top = Math.min(top, fileSize); + } + + const firstBlockOffset = Math.floor(offset / this.blockSize) * this.blockSize; + + for (let current = firstBlockOffset; current < top; current += this.blockSize) { + const blockId = Math.floor(current / this.blockSize); + if (!this.blockCache.has(blockId) && !this.blockRequests.has(blockId)) { + this.blockIdsToFetch.add(blockId); + missingBlockIds.push(blockId); + } + if (this.blockRequests.has(blockId)) { + blockRequests.push(this.blockRequests.get(blockId)); + } + allBlockIds.push(blockId); + } + } + + // allow additional block requests to accumulate + await wait(); + this.fetchBlocks(signal); + + // Gather all of the new requests that this fetch call is contributing to `fetch`. + const missingRequests = []; + for (const blockId of missingBlockIds) { + // The requested missing block could already be in the cache + // instead of having its request still be outstanding. + if (this.blockRequests.has(blockId)) { + missingRequests.push(this.blockRequests.get(blockId)); + } + } + + // Actually await all pending requests that are needed for this `fetch`. + await Promise.allSettled(blockRequests); + await Promise.allSettled(missingRequests); + + // Perform retries if a block was interrupted by a previous signal + const abortedBlockRequests = []; + const abortedBlockIds = allBlockIds.filter( + (id) => this.abortedBlockIds.has(id) || !this.blockCache.has(id), + ); + abortedBlockIds.forEach((id) => this.blockIdsToFetch.add(id)); + // start the retry of some blocks if required + if (abortedBlockIds.length > 0 && signal && !signal.aborted) { + this.fetchBlocks(null); + for (const blockId of abortedBlockIds) { + const block = this.blockRequests.get(blockId); + if (!block) { + throw new Error(`Block ${blockId} is not in the block requests`); + } + abortedBlockRequests.push(block); + } + await Promise.allSettled(abortedBlockRequests); + } + + // throw an abort error + if (signal && signal.aborted) { + throw new AbortError('Request was aborted'); + } + + const blocks = allBlockIds.map((id) => this.blockCache.get(id) || this.evictedBlocks.get(id)); + const failedBlocks = blocks.filter((i) => !i); + if (failedBlocks.length) { + throw new AggregateError(failedBlocks, 'Request failed'); + } + + // create a final Map, with all required blocks for this request to satisfy + const requiredBlocks = new Map(zip(allBlockIds, blocks)); + + // TODO: satisfy each slice + return this.readSliceData(slices, requiredBlocks); + } + + /** + * @param signal + */ + fetchBlocks(signal) { + // check if we still need to + if (this.blockIdsToFetch.size > 0) { + const groups = this.groupBlocks(this.blockIdsToFetch); + + // start requesting slices of data + const groupRequests = this.source.fetch(groups, signal); + + for (let groupIndex = 0; groupIndex < groups.length; ++groupIndex) { + const group = groups[groupIndex]; + + for (const blockId of group.blockIds) { + // make an async IIFE for each block + this.blockRequests.set( + blockId, + (async () => { + try { + const response = (await groupRequests)[groupIndex]; + const blockOffset = blockId * this.blockSize; + const o = blockOffset - response.offset; + const t = Math.min(o + this.blockSize, response.data.byteLength); + const data = response.data.slice(o, t); + const block = new Block(blockOffset, data.byteLength, data, blockId); + this.blockCache.set(blockId, block); + this.abortedBlockIds.delete(blockId); + } catch (err) { + if (err.name === 'AbortError') { + // store the signal here, we need it to determine later if an + // error was caused by this signal + err.signal = signal; + this.blockCache.delete(blockId); + this.abortedBlockIds.add(blockId); + } else { + throw err; + } + } finally { + this.blockRequests.delete(blockId); + } + })(), + ); + } + } + this.blockIdsToFetch.clear(); + } + } + + /** + * @param blockIds + * @returns + */ + groupBlocks(blockIds) { + const sortedBlockIds = Array.from(blockIds).sort((a, b) => a - b); + if (sortedBlockIds.length === 0) { + return []; + } + let current = []; + let lastBlockId = null; + const groups = []; + + for (const blockId of sortedBlockIds) { + if (lastBlockId === null || lastBlockId + 1 === blockId) { + current.push(blockId); + lastBlockId = blockId; + } else { + groups.push( + new BlockGroup(current[0] * this.blockSize, current.length * this.blockSize, current), + ); + current = [blockId]; + lastBlockId = blockId; + } + } + + groups.push( + new BlockGroup(current[0] * this.blockSize, current.length * this.blockSize, current), + ); + + return groups; + } + + /** + * @param slices + * @param blocks + */ + readSliceData(slices, blocks) { + return slices.map((slice) => { + let top = slice.offset + slice.length; + if (this.fileSize !== null) { + top = Math.min(this.fileSize, top); + } + const blockIdLow = Math.floor(slice.offset / this.blockSize); + const blockIdHigh = Math.floor(top / this.blockSize); + const sliceData = new ArrayBuffer(slice.length); + const sliceView = new Uint8Array(sliceData); + + for (let blockId = blockIdLow; blockId <= blockIdHigh; ++blockId) { + const block = blocks.get(blockId); + const delta = block.offset - slice.offset; + const topDelta = block.top - top; + let blockInnerOffset = 0; + let rangeInnerOffset = 0; + let usedBlockLength; + + if (delta < 0) { + blockInnerOffset = -delta; + } else if (delta > 0) { + rangeInnerOffset = delta; + } + + if (topDelta < 0) { + usedBlockLength = block.length - blockInnerOffset; + } else { + usedBlockLength = top - block.offset - blockInnerOffset; + } + + const blockView = new Uint8Array(block.data, blockInnerOffset, usedBlockLength); + sliceView.set(blockView, rangeInnerOffset); + } + + return sliceData; + }); + } +} diff --git a/src/readers/geotiff/source/fetch.ts b/src/readers/geotiff/source/fetch.ts new file mode 100644 index 00000000..dc0fa2bc --- /dev/null +++ b/src/readers/geotiff/source/fetch.ts @@ -0,0 +1,68 @@ +import { BaseClient, BaseResponse } from './client/base'; + +/** + * + */ +class FetchResponse extends BaseResponse { + /** + * BaseResponse facade for fetch API Response + * @param response + */ + constructor(response) { + super(); + this.response = response; + } + + /** + * + */ + get status() { + return this.response.status; + } + + /** + * @param name + */ + getHeader(name) { + return this.response.headers.get(name); + } + + /** + * + */ + async getData() { + const data = this.response.arrayBuffer + ? await this.response.arrayBuffer() + : (await this.response.buffer()).buffer; + return data; + } +} + +/** + * + */ +export class FetchClient extends BaseClient { + /** + * @param url + * @param credentials + */ + constructor(url, credentials) { + super(url); + this.credentials = credentials; + } + + /** + * @param [options] + * @param options.headers + * @param options.signal + * @returns + */ + async request({ headers, signal } = {}) { + const response = await fetch(this.url, { + headers, + credentials: this.credentials, + signal, + }); + return new FetchResponse(response); + } +} diff --git a/src/readers/geotiff/source/httputils.ts b/src/readers/geotiff/source/httputils.ts new file mode 100644 index 00000000..56275b2e --- /dev/null +++ b/src/readers/geotiff/source/httputils.ts @@ -0,0 +1,145 @@ +const CRLFCRLF = '\r\n\r\n'; + +/* + * Shim for 'Object.fromEntries' + */ +function itemsToObject(items) { + if (typeof Object.fromEntries !== 'undefined') { + return Object.fromEntries(items); + } + const obj = {}; + for (const [key, value] of items) { + obj[key.toLowerCase()] = value; + } + return obj; +} + +/** + * Parse HTTP headers from a given string. + * @param {String} text the text to parse the headers from + * @returns {Object} the parsed headers with lowercase keys + */ +function parseHeaders(text) { + const items = text + .split('\r\n') + .map((line) => { + const kv = line.split(':').map((str) => str.trim()); + kv[0] = kv[0].toLowerCase(); + return kv; + }); + + return itemsToObject(items); +} + +/** + * Parse a 'Content-Type' header value to the content-type and parameters + * @param {String} rawContentType the raw string to parse from + * @returns {Object} the parsed content type with the fields: type and params + */ +export function parseContentType(rawContentType) { + const [type, ...rawParams] = rawContentType.split(';').map((s) => s.trim()); + const paramsItems = rawParams.map((param) => param.split('=')); + return { type, params: itemsToObject(paramsItems) }; +} + +/** + * Parse a 'Content-Range' header value to its start, end, and total parts + * @param {String} rawContentRange the raw string to parse from + * @returns {Object} the parsed parts + */ +export function parseContentRange(rawContentRange) { + let start; + let end; + let total; + + if (rawContentRange) { + [, start, end, total] = rawContentRange.match(/bytes (\d+)-(\d+)\/(\d+)/); + start = parseInt(start, 10); + end = parseInt(end, 10); + total = parseInt(total, 10); + } + + return { start, end, total }; +} + +/** + * Parses a list of byteranges from the given 'multipart/byteranges' HTTP response. + * Each item in the list has the following properties: + * - headers: the HTTP headers + * - data: the sliced ArrayBuffer for that specific part + * - offset: the offset of the byterange within its originating file + * - length: the length of the byterange + * @param {ArrayBuffer} responseArrayBuffer the response to be parsed and split + * @param {String} boundary the boundary string used to split the sections + * @returns {Object[]} the parsed byteranges + */ +export function parseByteRanges(responseArrayBuffer, boundary) { + let offset = null; + const decoder = new TextDecoder('ascii'); + const out = []; + + const startBoundary = `--${boundary}`; + const endBoundary = `${startBoundary}--`; + + // search for the initial boundary, may be offset by some bytes + // TODO: more efficient to check for `--` in bytes directly + for (let i = 0; i < 10; ++i) { + const text = decoder.decode( + new Uint8Array(responseArrayBuffer, i, startBoundary.length), + ); + if (text === startBoundary) { + offset = i; + } + } + + if (offset === null) { + throw new Error('Could not find initial boundary'); + } + + while (offset < responseArrayBuffer.byteLength) { + const text = decoder.decode( + new Uint8Array(responseArrayBuffer, offset, + Math.min(startBoundary.length + 1024, responseArrayBuffer.byteLength - offset), + ), + ); + + // break if we arrived at the end + if (text.length === 0 || text.startsWith(endBoundary)) { + break; + } + + // assert that we are actually dealing with a byterange and are at the correct offset + if (!text.startsWith(startBoundary)) { + throw new Error('Part does not start with boundary'); + } + + // get a substring from where we read the headers + const innerText = text.substr(startBoundary.length + 2); + + if (innerText.length === 0) { + break; + } + + // find the double linebreak that denotes the end of the headers + const endOfHeaders = innerText.indexOf(CRLFCRLF); + + // parse the headers to get the content range size + const headers = parseHeaders(innerText.substr(0, endOfHeaders)); + const { start, end, total } = parseContentRange(headers['content-range']); + + // calculate the length of the slice and the next offset + const startOfData = offset + startBoundary.length + endOfHeaders + CRLFCRLF.length; + const length = parseInt(end, 10) + 1 - parseInt(start, 10); + out.push({ + headers, + data: responseArrayBuffer.slice(startOfData, startOfData + length), + offset: start, + length, + fileSize: total, + }); + + offset = startOfData + length + 4; + } + + return out; +} diff --git a/src/readers/geotiff/source/remote.ts b/src/readers/geotiff/source/remote.ts new file mode 100644 index 00000000..af0822e7 --- /dev/null +++ b/src/readers/geotiff/source/remote.ts @@ -0,0 +1,219 @@ +import { parseByteRanges, parseContentRange, parseContentType } from './httputils'; +import { BaseSource } from './basesource'; +import { BlockedSource } from './blockedsource'; + +import { FetchClient } from './fetch'; + +/** + * + */ +class RemoteSource extends BaseSource { + /** + * @param client + * @param headers + * @param maxRanges + * @param allowFullFile + */ + constructor(client, headers, maxRanges, allowFullFile) { + super(); + this.client = client; + this.headers = headers; + this.maxRanges = maxRanges; + this.allowFullFile = allowFullFile; + this._fileSize = null; + } + + /** + * @param slices + * @param signal + */ + async fetch(slices, signal) { + // if we allow multi-ranges, split the incoming request into that many sub-requests + // and join them afterwards + if (this.maxRanges >= slices.length) { + return this.fetchSlices(slices, signal); + } else if (this.maxRanges > 0 && slices.length > 1) { + // TODO: split into multiple multi-range requests + // const subSlicesRequests = []; + // for (let i = 0; i < slices.length; i += this.maxRanges) { + // subSlicesRequests.push( + // this.fetchSlices(slices.slice(i, i + this.maxRanges), signal), + // ); + // } + // return (await Promise.all(subSlicesRequests)).flat(); + } + + // otherwise make a single request for each slice + return Promise.all(slices.map((slice) => this.fetchSlice(slice, signal))); + } + + /** + * @param slices + * @param signal + */ + async fetchSlices(slices, signal) { + const response = await this.client.request({ + headers: { + ...this.headers, + Range: `bytes=${slices + .map(({ offset, length }) => `${offset}-${offset + length}`) + .join(',')}`, + }, + signal, + }); + + if (!response.ok) { + throw new Error('Error fetching data.'); + } else if (response.status === 206) { + const { type, params } = parseContentType(response.getHeader('content-type')); + if (type === 'multipart/byteranges') { + const byteRanges = parseByteRanges(await response.getData(), params.boundary); + this._fileSize = byteRanges[0].fileSize || null; + return byteRanges; + } + + const data = await response.getData(); + + const { start, end, total } = parseContentRange(response.getHeader('content-range')); + this._fileSize = total || null; + const first = [ + { + data, + offset: start, + length: end - start, + }, + ]; + + if (slices.length > 1) { + // we requested more than one slice, but got only the first + // unfortunately, some HTTP Servers don't support multi-ranges + // and return only the first + + // get the rest of the slices and fetch them iteratively + const others = await Promise.all( + slices.slice(1).map((slice) => this.fetchSlice(slice, signal)), + ); + return first.concat(others); + } + return first; + } else { + if (!this.allowFullFile) { + throw new Error('Server responded with full file'); + } + const data = await response.getData(); + this._fileSize = data.byteLength; + return [ + { + data, + offset: 0, + length: data.byteLength, + }, + ]; + } + } + + /** + * @param slice + * @param signal + */ + async fetchSlice(slice, signal) { + const { offset, length } = slice; + const response = await this.client.request({ + headers: { + ...this.headers, + Range: `bytes=${offset}-${offset + length}`, + }, + signal, + }); + + // check the response was okay and if the server actually understands range requests + if (!response.ok) { + throw new Error('Error fetching data.'); + } else if (response.status === 206) { + const data = await response.getData(); + + const { total } = parseContentRange(response.getHeader('content-range')); + this._fileSize = total || null; + return { + data, + offset, + length, + }; + } else { + if (!this.allowFullFile) { + throw new Error('Server responded with full file'); + } + + const data = await response.getData(); + + this._fileSize = data.byteLength; + return { + data, + offset: 0, + length: data.byteLength, + }; + } + } + + /** + * + */ + get fileSize() { + return this._fileSize; + } +} + +/** + * @param source + * @param root0 + * @param root0.blockSize + * @param root0.cacheSize + */ +function maybeWrapInBlockedSource(source, { blockSize, cacheSize }) { + if (blockSize === null) { + return source; + } + return new BlockedSource(source, { blockSize, cacheSize }); +} + +/** + * @param url + * @param root0 + * @param root0.headers + * @param root0.credentials + * @param root0.maxRanges + * @param root0.allowFullFile + */ +export function makeFetchSource( + url, + { headers = {}, credentials, maxRanges = 0, allowFullFile = false, ...blockOptions } = {}, +) { + const client = new FetchClient(url, credentials); + const source = new RemoteSource(client, headers, maxRanges, allowFullFile); + return maybeWrapInBlockedSource(source, blockOptions); +} + +/** + * @param client + * @param root0 + * @param root0.headers + * @param root0.maxRanges + * @param root0.allowFullFile + */ +export function makeCustomSource( + client, + { headers = {}, maxRanges = 0, allowFullFile = false, ...blockOptions } = {}, +) { + const source = new RemoteSource(client, headers, maxRanges, allowFullFile); + return maybeWrapInBlockedSource(source, blockOptions); +} + +/** + * @param url + * @param options + * @param options.forceXHR + * @param clientOptions + */ +export function makeRemoteSource(url, clientOptions = {}) { + return makeFetchSource(url, clientOptions); +} diff --git a/src/readers/geotiff/utils.ts b/src/readers/geotiff/utils.ts new file mode 100644 index 00000000..d74d81c0 --- /dev/null +++ b/src/readers/geotiff/utils.ts @@ -0,0 +1,177 @@ +/** + * @param target + * @param source + */ +export function assign(target, source) { + for (const key in source) { + if (source.hasOwnProperty(key)) { + target[key] = source[key]; + } + } +} + +/** + * @param iterable + * @param length + */ +export function chunk(iterable, length) { + const results = []; + const lengthOfIterable = iterable.length; + for (let i = 0; i < lengthOfIterable; i += length) { + const chunked = []; + for (let ci = i; ci < i + length; ci++) { + chunked.push(iterable[ci]); + } + results.push(chunked); + } + return results; +} + +/** + * @param string + * @param expectedEnding + */ +export function endsWith(string, expectedEnding) { + if (string.length < expectedEnding.length) { + return false; + } + const actualEnding = string.substr(string.length - expectedEnding.length); + return actualEnding === expectedEnding; +} + +/** + * @param iterable + * @param func + */ +export function forEach(iterable, func) { + const { length } = iterable; + for (let i = 0; i < length; i++) { + func(iterable[i], i); + } +} + +/** + * @param oldObj + */ +export function invert(oldObj) { + const newObj = {}; + for (const key in oldObj) { + if (oldObj.hasOwnProperty(key)) { + const value = oldObj[key]; + newObj[value] = key; + } + } + return newObj; +} + +/** + * @param n + */ +export function range(n: number): number[] { + const results = []; + for (let i = 0; i < n; i++) { + results.push(i); + } + return results; +} + +/** + * @param numTimes + * @param func + */ +export function times(numTimes: number, func: (i: number) => number): number[] { + const results = []; + for (let i = 0; i < numTimes; i++) { + results.push(func(i)); + } + return results; +} + +/** + * @param iterable + */ +export function toArray(iterable) { + const results = []; + const { length } = iterable; + for (let i = 0; i < length; i++) { + results.push(iterable[i]); + } + return results; +} + +/** + * @param input + */ +export function toArrayRecursively(input) { + if (input.length) { + return toArray(input).map(toArrayRecursively); + } + return input; +} + +// copied from https://github.com/academia-de-codigo/parse-content-range-header/blob/master/index.js +/** + * @param headerValue + */ +export function parseContentRange(headerValue) { + if (!headerValue) { + return null; + } + + if (typeof headerValue !== 'string') { + throw new Error('invalid argument'); + } + + /** + * @param number + */ + const parseInt = (number) => Number.parseInt(number, 10); + + // Check for presence of unit + let matches = headerValue.match(/^(\w*) /); + const unit = matches && matches[1]; + + // check for start-end/size header format + matches = headerValue.match(/(\d+)-(\d+)\/(\d+|\*)/); + if (matches) { + return { + unit, + first: parseInt(matches[1]), + last: parseInt(matches[2]), + length: matches[3] === '*' ? null : parseInt(matches[3]), + }; + } + + // check for size header format + matches = headerValue.match(/(\d+|\*)/); + if (matches) { + return { + unit, + first: null, + last: null, + length: matches[1] === '*' ? null : parseInt(matches[1]), + }; + } + + return null; +} + +/* + * Promisified wrapper around 'setTimeout' to allow 'await' + */ +/** + * @param milliseconds + */ +export async function wait(milliseconds) { + return new Promise((resolve) => setTimeout(resolve, milliseconds)); +} + +/** + * @param a + * @param b + */ +export function zip(a: Iterable | T[], b: Iterable | T[]): T[][] { + const A = Array.isArray(a) ? a : Array.from(a); + const B = Array.isArray(b) ? b : Array.from(b); + return A.map((k, i) => [k, B[i]]); +} diff --git a/src/readers/index.ts b/src/readers/index.ts index 9b75a687..bb42aa80 100644 --- a/src/readers/index.ts +++ b/src/readers/index.ts @@ -1,6 +1,15 @@ -export * from './bufferReader'; +import type { Features } from '../geometry'; -/** Reader interface. Used to read data from either a buffer or a filesystem */ +export * from './csv'; +export * from './json'; +export * from './osm'; +export * from './pmtiles'; +export * from './shapefile'; +export * from './wkt'; +export * from './fetch'; +export * from './nadgrid'; + +/** Reader interface. Implemented to read data from either a buffer or a filesystem */ export interface Reader { // Properties byteLength: number; @@ -20,4 +29,67 @@ export interface Reader { slice: (begin: number, end: number) => DataView; setStringEncoding: (encoding: string) => void; parseString: (byteOffset: number, byteLength: number) => string; + getRange: (offset: number, length: number) => Promise; +} + +/** Feature iteration interface. Implemented by readers to iterate over features */ +export interface FeatureIterator> { + [Symbol.asyncIterator]: () => AsyncGenerator>; +} + +/** A buffer reader is an extension of a DataView with some extra methods */ +export class BufferReader extends DataView implements Reader { + textDecoder = new TextDecoder('utf-8'); + + /** + * @param buffer - the input buffer + * @param byteOffset - offset in the buffer + * @param byteLength - length of the buffer + */ + constructor( + buffer: ArrayBufferLike & { + BYTES_PER_ELEMENT?: never; + }, + byteOffset?: number, + byteLength?: number, + ) { + super(buffer, byteOffset, byteLength); + } + + /** + * @param begin - beginning of the slice + * @param end - end of the slice. If not provided, the end of the data is used + * @returns - a DataView of the slice + */ + slice(begin: number, end: number): DataView { + return new DataView( + this.buffer.slice(this.byteOffset + begin, this.byteOffset + (end ?? this.byteLength)), + ); + } + + /** @param encoding - update the text decoder's encoding */ + setStringEncoding(encoding: string) { + this.textDecoder = new TextDecoder(encoding); + } + + /** + * @param byteOffset - Start of the string + * @param byteLength - Length of the string + * @returns - The string + */ + parseString(byteOffset: number, byteLength: number): string { + const { textDecoder } = this; + const data = this.slice(byteOffset, byteOffset + byteLength).buffer; + const out = textDecoder.decode(data, { stream: true }) + textDecoder.decode(); + return out.replace(/\0/g, '').trim(); + } + + /** + * @param offset - the offset of the range + * @param length - the length of the range + * @returns - the ranged buffer + */ + async getRange(offset: number, length: number): Promise { + return new Uint8Array(this.buffer).slice(offset, offset + length); + } } diff --git a/src/readers/json/index.ts b/src/readers/json/index.ts index b9b1d8c2..0f2124b5 100644 --- a/src/readers/json/index.ts +++ b/src/readers/json/index.ts @@ -1,8 +1,8 @@ -import type { Reader } from '..'; +import type { FeatureIterator, Reader } from '..'; import type { Features, JSONCollection } from 's2-tools/geometry'; /** Standard Buffer Reader for (Geo|S2)JSON */ -export class BufferJSONReader { +export class BufferJSONReader implements FeatureIterator { data: JSONCollection; /** @param data - the JSON data to parase */ @@ -18,7 +18,7 @@ export class BufferJSONReader { * Generator to iterate over each (Geo|S2)JSON object in the file * @yields {Features} */ - *iterate(): Generator { + async *[Symbol.asyncIterator](): AsyncGenerator { const { type } = this.data; if (type === 'FeatureCollection') { @@ -40,7 +40,7 @@ export class BufferJSONReader { } /** Parse (Geo|S2)JSON from a file that is in a newline-delimited format */ -export class NewLineDelimitedJSONReader { +export class NewLineDelimitedJSONReader implements FeatureIterator { /** @param reader - the reader to parse from */ constructor(public reader: Reader) {} @@ -48,7 +48,7 @@ export class NewLineDelimitedJSONReader { * Generator to iterate over each (Geo|S2)JSON object in the file * @yields {Features} */ - *iterate(): Generator { + async *[Symbol.asyncIterator](): AsyncGenerator { const { reader } = this; let cursor = 0; let offset = 0; @@ -108,10 +108,10 @@ export class JSONReader { * Generator to iterate over each (Geo|S2)JSON object in the reader. * @yields {Features} */ - *iterate(): Generator { + async *[Symbol.asyncIterator](): AsyncGenerator { if (this.#length <= this.#chunkSize) { const reader = new BufferJSONReader(this.reader.parseString(0, this.#length)); - for (const feature of reader.iterate()) yield feature; + for await (const feature of reader) yield feature; return; } // buffer the first chunk diff --git a/src/readers/mmapReader.ts b/src/readers/mmap.ts similarity index 95% rename from src/readers/mmapReader.ts rename to src/readers/mmap.ts index a7b8d8bc..3f0eca86 100644 --- a/src/readers/mmapReader.ts +++ b/src/readers/mmap.ts @@ -158,4 +158,13 @@ export default class MMapReader implements Reader { const out = textDecoder.decode(data, { stream: true }) + textDecoder.decode(); return out.replace(/\0/g, '').trim(); } + + /** + * @param offset - the offset of the range + * @param length - the length of the range + * @returns - the ranged buffer + */ + async getRange(offset: number, length: number): Promise { + return this.#buffer.slice(offset, offset + length); + } } diff --git a/src/readers/nadgrid.ts b/src/readers/nadgrid.ts index f01f7f50..9e5ca27f 100644 --- a/src/readers/nadgrid.ts +++ b/src/readers/nadgrid.ts @@ -165,17 +165,22 @@ export class NadGrid { geometry: { type: 'MultiPoint', is3D: false, + // CVS => lonLat coords coordinates: nodes.map(({ longitudeShift, latitudeShift }) => { return { x: secondsToDegrees(longitudeShift), y: secondsToDegrees(latitudeShift) }; }), }, metadata: { + // ll => lowerLonLat lowerLonLat: { x: secondsToDegrees(subHeader.lowerLongitude), y: secondsToDegrees(subHeader.lowerLatitude), }, + // del => lonLatInterval lonLatInterval: { x: subHeader.longitudeInterval, y: subHeader.latitudeInterval }, + // lim => lonLatColumnCount lonLatColumnCount: { x: lonColumnCount, y: latColumnCount }, + // count => count count: subHeader.gridNodeCount, }, }; diff --git a/src/readers/osm/blob.ts b/src/readers/osm/blob.ts index cc989466..0fed19f9 100644 --- a/src/readers/osm/blob.ts +++ b/src/readers/osm/blob.ts @@ -31,9 +31,9 @@ export class BlobHeader { * @param pbf */ #readLayer(tag: number, blobHeader: BlobHeader, pbf: Protobuf): void { - if (tag == 1) blobHeader.type = pbf.readString(); - else if (tag == 2) blobHeader.indexdata = pbf.readBytes(); - else if (tag == 3) blobHeader.datasize = pbf.readVarint(); + if (tag === 1) blobHeader.type = pbf.readString(); + else if (tag === 2) blobHeader.indexdata = pbf.readBytes(); + else if (tag === 3) blobHeader.datasize = pbf.readVarint(); else throw new Error('unknown tag ' + tag); } } @@ -60,18 +60,18 @@ export class Blob { */ #readLayer(tag: number, blob: Blob, pbf: Protobuf): void { // no compression - if (tag == 1) blob.data = pbf.readBytes(); - else if (tag == 2) blob.raw_size = pbf.readVarint(); + if (tag === 1) blob.data = pbf.readBytes(); + else if (tag === 2) blob.raw_size = pbf.readVarint(); // ZLIB: - else if (tag == 3) blob.data = decompressStream(pbf.readBytes()); + else if (tag === 3) blob.data = decompressStream(pbf.readBytes()); // For LZMA compressed data (optional) - else if (tag == 4) blob.data = pbf.readBytes(); + else if (tag === 4) blob.data = pbf.readBytes(); // Formerly used for bzip2 compressed data. Deprecated in 2010. - else if (tag == 5) throw new Error('bzip2 not supported'); + else if (tag === 5) throw new Error('bzip2 not supported'); // For LZ4 compressed data (optional) - else if (tag == 6) blob.data = pbf.readBytes(); + else if (tag === 6) blob.data = pbf.readBytes(); // For ZSTD compressed data (optional) - else if (tag == 7) blob.data = pbf.readBytes(); + else if (tag === 7) blob.data = pbf.readBytes(); else throw new Error('unknown tag ' + tag); } } diff --git a/src/readers/osm/headerBlock.ts b/src/readers/osm/headerBlock.ts index ae15f51a..99ca1393 100644 --- a/src/readers/osm/headerBlock.ts +++ b/src/readers/osm/headerBlock.ts @@ -80,14 +80,14 @@ export class HeaderBlock { * @param pbf */ readLayer(tag: number, header: HeaderBlock, pbf: Protobuf): void { - if (tag == 1) header.bbox = pbf.readMessage(header.bbox.readLayer, header.bbox); - else if (tag == 4) header.required_features.push(pbf.readString()); - else if (tag == 5) header.optional_features.push(pbf.readString()); - else if (tag == 16) header.writingprogram = pbf.readString(); - else if (tag == 17) header.source = pbf.readString(); - else if (tag == 32) header.osmosis_replication_timestamp = pbf.readVarint(); - else if (tag == 33) header.osmosis_replication_sequence_number = pbf.readVarint(); - else if (tag == 34) header.osmosis_replication_base_url = pbf.readString(); + if (tag === 1) header.bbox = pbf.readMessage(header.bbox.readLayer, header.bbox); + else if (tag === 4) header.required_features.push(pbf.readString()); + else if (tag === 5) header.optional_features.push(pbf.readString()); + else if (tag === 16) header.writingprogram = pbf.readString(); + else if (tag === 17) header.source = pbf.readString(); + else if (tag === 32) header.osmosis_replication_timestamp = pbf.readVarint(); + else if (tag === 33) header.osmosis_replication_sequence_number = pbf.readVarint(); + else if (tag === 34) header.osmosis_replication_base_url = pbf.readString(); else throw new Error('unknown tag ' + tag); } } @@ -109,10 +109,10 @@ export class HeaderBBox { * @param pbf */ readLayer(tag: number, bbox: HeaderBBox, pbf: Protobuf): void { - if (tag == 1) bbox.left = pbf.readVarint(); - else if (tag == 2) bbox.right = pbf.readVarint(); - else if (tag == 3) bbox.top = pbf.readVarint(); - else if (tag == 4) bbox.bottom = pbf.readVarint(); + if (tag === 1) bbox.left = pbf.readVarint(); + else if (tag === 2) bbox.right = pbf.readVarint(); + else if (tag === 3) bbox.top = pbf.readVarint(); + else if (tag === 4) bbox.bottom = pbf.readVarint(); else throw new Error('unknown tag ' + tag); } diff --git a/src/readers/osm/index.ts b/src/readers/osm/index.ts index b4d98ab3..933c6445 100644 --- a/src/readers/osm/index.ts +++ b/src/readers/osm/index.ts @@ -6,7 +6,7 @@ import { Blob, BlobHeader } from './blob'; import type { InfoBlock } from './info'; import type { KVStore } from 's2-tools/dataStore'; import type { OSMHeader } from './headerBlock'; -import type { Reader } from '../index'; +import type { FeatureIterator, Reader } from '..'; import type { VectorFeature, VectorLineString, VectorPoint } from 's2-tools/geometry'; export type * from './blob'; @@ -101,7 +101,7 @@ export interface OsmReaderOptions { /** * */ -export class OSMReader { +export class OSMReader implements FeatureIterator { /** if true, remove nodes that have no tags [Default = true] */ removeEmptyNodes: boolean; /** If provided, filters of the */ @@ -142,7 +142,8 @@ export class OSMReader { /** * */ - async *iterate(): AsyncGenerator> { + // async *iterate(): AsyncGenerator> { + async *[Symbol.asyncIterator](): AsyncGenerator> { this.#offset = 0; // skip the header await this.#next(); diff --git a/src/readers/osm/node.ts b/src/readers/osm/node.ts index 032f23b6..81cce5d5 100644 --- a/src/readers/osm/node.ts +++ b/src/readers/osm/node.ts @@ -177,7 +177,7 @@ export class DenseNodes { const keys: number[] = []; const vals: number[] = []; if (this.keysVals.length > 0) { - while (this.keysVals[j] != 0) { + while (this.keysVals[j] !== 0) { keys.push(this.keysVals[j]); vals.push(this.keysVals[j + 1]); j += 2; @@ -199,11 +199,11 @@ export class DenseNodes { #readLayer(tag: number, denseNodes: DenseNodes, pbf: Protobuf): void { const { primitiveBlock: pb } = denseNodes; - if (tag == 1) denseNodes.ids = pbf.readPackedSVarint(); - else if (tag == 5) denseNodes.denseinfo = new DenseInfo(pb, pbf); - else if (tag == 8) denseNodes.lats = pbf.readPackedSVarint(); - else if (tag == 9) denseNodes.lons = pbf.readPackedSVarint(); - else if (tag == 10) denseNodes.keysVals = pbf.readPackedVarint(); + if (tag === 1) denseNodes.ids = pbf.readPackedSVarint(); + else if (tag === 5) denseNodes.denseinfo = new DenseInfo(pb, pbf); + else if (tag === 8) denseNodes.lats = pbf.readPackedSVarint(); + else if (tag === 9) denseNodes.lons = pbf.readPackedSVarint(); + else if (tag === 10) denseNodes.keysVals = pbf.readPackedVarint(); else throw new Error('unknown tag ' + tag); } } diff --git a/src/readers/osm/primitive.ts b/src/readers/osm/primitive.ts index abbf453f..288ca2fa 100644 --- a/src/readers/osm/primitive.ts +++ b/src/readers/osm/primitive.ts @@ -157,7 +157,7 @@ export class StringTable { * @param pbf */ #readLayer(tag: number, st: StringTable, pbf: Protobuf): void { - if (tag == 1) st.strings.push(pbf.readString()); + if (tag === 1) st.strings.push(pbf.readString()); else throw new Error(`unknown tag ${tag}`); } } @@ -179,7 +179,7 @@ export class ChangeSet { * @param pbf */ #readLayer(tag: number, cs: ChangeSet, pbf: Protobuf): void { - if (tag == 1) cs.id = pbf.readVarint(); + if (tag === 1) cs.id = pbf.readVarint(); else throw new Error(`unknown tag ${tag}`); } } diff --git a/src/readers/osm/relation.ts b/src/readers/osm/relation.ts index 95f4d2ac..9fac1e06 100644 --- a/src/readers/osm/relation.ts +++ b/src/readers/osm/relation.ts @@ -290,7 +290,7 @@ function buildGeometry(members: WayMember[], nodes: NodeMember[]): undefined | R * @returns true if the points are equal */ function equalPoints(a: VectorPoint, b: VectorPoint): boolean { - return a.x === b.x && a.y == b.y; + return a.x === b.x && a.y === b.y; } /** @@ -305,7 +305,7 @@ function sortMembers(members: WayMember[]): void { const curFirstPoint = curWay[0]; const curLastPoint = curWay[curWay.length - 1]; // if current way is already self closing break - if (curFirstPoint == curLastPoint) break; + if (curFirstPoint === curLastPoint) break; for (let j = i + 1; j < members.length; j++) { const nextWay = members[j].way; const nextFirstPoint = nextWay[0]; @@ -327,7 +327,7 @@ function sortMembers(members: WayMember[]): void { nextWay.reverse(); } // we want to move the found member to be next to the current member - if (i + 1 != j) { + if (i + 1 !== j) { const temp = members[i + 1]; members[i + 1] = members[j]; members[j] = temp; diff --git a/src/readers/osm/way.ts b/src/readers/osm/way.ts index 093a1920..81c8dc3c 100644 --- a/src/readers/osm/way.ts +++ b/src/readers/osm/way.ts @@ -95,7 +95,7 @@ export class Way { if ( (upgradeWaysToAreas && this.#refs.length >= 4 && - this.#refs[0] == this.#refs[this.#refs.length - 1]) || + this.#refs[0] === this.#refs[this.#refs.length - 1]) || this.hasKeyValue('area', 'yes') ) { return true; diff --git a/src/readers/pmtiles/index.ts b/src/readers/pmtiles/index.ts new file mode 100644 index 00000000..8ee63301 --- /dev/null +++ b/src/readers/pmtiles/index.ts @@ -0,0 +1,4 @@ +export * from './reader'; +export * from './pmtiles'; +export * from './s2pmtiles'; +export * from './varint'; diff --git a/src/readers/pmtiles/pmtiles.ts b/src/readers/pmtiles/pmtiles.ts new file mode 100644 index 00000000..9818a502 --- /dev/null +++ b/src/readers/pmtiles/pmtiles.ts @@ -0,0 +1,296 @@ +import { readVarint } from './varint'; + +import type { Point } from 's2-tools/geometry'; + +/** A tile, in the format of ZXY. */ +export type FlatTile = [zoom: number, x: number, y: number]; + +/** PMTiles v3 directory entry. */ +export interface Entry { + tileID: number; + offset: number; + length: number; + runLength: number; +} + +/** + * Enum representing a compression algorithm used. + * 0 = unknown compression, for if you must use a different or unspecified algorithm. + * 1 = no compression. + * 2 = gzip + * 3 = brotli + * 4 = zstd + */ +export enum Compression { + /** unknown compression, for if you must use a different or unspecified algorithm. */ + Unknown = 0, + /** no compression. */ + None = 1, + /** gzip. */ + Gzip = 2, + /** brotli. */ + Brotli = 3, + /** zstd. */ + Zstd = 4, +} + +/** + * Provide a decompression implementation that acts on `buf` and returns decompressed data. + * + * Should use the native DecompressionStream on browsers, zlib on node. + * Should throw if the compression algorithm is not supported. + */ +export type DecompressFunc = (buf: Uint8Array, compression: Compression) => Promise; + +/** + * Describe the type of tiles stored in the archive. + * 0 is unknown/other, 1 is "MVT" vector tiles. + */ +export enum TileType { + /** unknown/other. */ + Unknown = 0, + /** Vector tiles. */ + Pbf = 1, + /** Image tiles. */ + Png = 2, + /** Image tiles. */ + Jpeg = 3, + /** Image tiles. */ + Webp = 4, + /** Image tiles. */ + Avif = 5, +} + +/** + * PMTiles v3 header storing basic archive-level information. + */ +export interface Header { + specVersion: number; + rootDirectoryOffset: number; + rootDirectoryLength: number; + jsonMetadataOffset: number; + jsonMetadataLength: number; + leafDirectoryOffset: number; + leafDirectoryLength?: number; + tileDataOffset: number; + tileDataLength?: number; + numAddressedTiles: number; + numTileEntries: number; + numTileContents: number; + clustered: boolean; + internalCompression: Compression; + tileCompression: Compression; + tileType: TileType; + minZoom: number; + maxZoom: number; + etag?: string; +} + +export const HEADER_SIZE_BYTES = 127; + +export const ROOT_SIZE = 16_384; + +/** + * @param n - the rotation size + * @param xy - the point + * @param rx - the x rotation + * @param ry - the y rotation + */ +function rotate(n: number, xy: Point, rx: number, ry: number): void { + if (ry === 0) { + if (rx === 1) { + xy[0] = n - 1 - xy[0]; + xy[1] = n - 1 - xy[1]; + } + const t = xy[0]; + xy[0] = xy[1]; + xy[1] = t; + } +} + +/** + * @param zoom - the zoom level + * @param pos - the tile position + * @returns - the tile + */ +function idOnLevel(zoom: number, pos: number): FlatTile { + const n = 2 ** zoom; + let rx = pos; + let ry = pos; + let t = pos; + const xy: Point = [0, 0]; + let s = 1; + while (s < n) { + rx = 1 & (t / 2); + ry = 1 & (t ^ rx); + rotate(s, xy, rx, ry); + xy[0] += s * rx; + xy[1] += s * ry; + t = t / 4; + s *= 2; + } + return [zoom, xy[0], xy[1]]; +} + +const tzValues: number[] = [ + 0, 1, 5, 21, 85, 341, 1365, 5461, 21845, 87381, 349525, 1398101, 5592405, 22369621, 89478485, + 357913941, 1431655765, 5726623061, 22906492245, 91625968981, 366503875925, 1466015503701, + 5864062014805, 23456248059221, 93824992236885, 375299968947541, 1501199875790165, +]; + +/** + * Convert Z,X,Y to a Hilbert TileID. + * @param zoom - the zoom level + * @param x - the x coordinate + * @param y - the y coordinate + * @returns - the Hilbert encoded TileID + */ +export function zxyToTileID(zoom: number, x: number, y: number): number { + if (zoom > 26) { + throw Error('Tile zoom level exceeds max safe number limit (26)'); + } + if (x > 2 ** zoom - 1 || y > 2 ** zoom - 1) { + throw Error('tile x/y outside zoom level bounds'); + } + + const acc = tzValues[zoom]; + const n = 2 ** zoom; + let rx = 0; + let ry = 0; + let d = 0; + const xy: [x: number, y: number] = [x, y]; + let s = n / 2; + while (true) { + rx = (xy[0] & s) > 0 ? 1 : 0; + ry = (xy[1] & s) > 0 ? 1 : 0; + d += s * s * ((3 * rx) ^ ry); + rotate(s, xy, rx, ry); + if (s <= 1) break; + s = s / 2; + } + return acc + d; +} + +/** + * Convert a Hilbert TileID to Z,X,Y. + * @param i - the encoded tile ID + * @returns - the decoded Z,X,Y + */ +export function tileIDToZxy(i: number): FlatTile { + let acc = 0; + + for (let z = 0; z < 27; z++) { + const numTiles = (0x1 << z) * (0x1 << z); + if (acc + numTiles > i) { + return idOnLevel(z, i - acc); + } + acc += numTiles; + } + + throw Error('Tile zoom level exceeds max safe number limit (26)'); +} + +/** + * Low-level function for looking up a TileID or leaf directory inside a directory. + * @param entries - the directory entries + * @param tileID - the tile ID + * @returns the entry associated with the tile, or null if not found + */ +export function findTile(entries: Entry[], tileID: number): Entry | null { + let m = 0; + let n = entries.length - 1; + while (m <= n) { + const k = (n + m) >> 1; + const cmp = tileID - entries[k].tileID; + if (cmp > 0) { + m = k + 1; + } else if (cmp < 0) { + n = k - 1; + } else { + return entries[k]; + } + } + + // at this point, m > n + if (n >= 0) { + if (entries[n].runLength === 0) return entries[n]; + if (tileID - entries[n].tileID < entries[n].runLength) return entries[n]; + } + return null; +} + +/** + * Parse raw header bytes into a Header object. + * @param bytes - the raw header bytes + * @returns the parsed header + */ +export function bytesToHeader(bytes: Uint8Array): Header { + const dv = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength); + // if (dv.getUint16(0, true) !== 0x4d50) { + // throw new Error('Wrong magic number for PMTiles archive'); + // } + + return { + specVersion: dv.getUint8(7), + rootDirectoryOffset: getUint64(dv, 8), + rootDirectoryLength: getUint64(dv, 16), + jsonMetadataOffset: getUint64(dv, 24), + jsonMetadataLength: getUint64(dv, 32), + leafDirectoryOffset: getUint64(dv, 40), + leafDirectoryLength: getUint64(dv, 48), + tileDataOffset: getUint64(dv, 56), + tileDataLength: getUint64(dv, 64), + numAddressedTiles: getUint64(dv, 72), + numTileEntries: getUint64(dv, 80), + numTileContents: getUint64(dv, 88), + clustered: dv.getUint8(96) === 1, + internalCompression: dv.getUint8(97), + tileCompression: dv.getUint8(98), + tileType: dv.getUint8(99), + minZoom: dv.getUint8(100), + maxZoom: dv.getUint8(101), + }; +} + +/** + * @param buffer - the buffer to deserialize + * @returns - the deserialized entries + */ +export function deserializeDir(buffer: Uint8Array): Entry[] { + const p = { buf: new Uint8Array(buffer), pos: 0 }; + const numEntries = readVarint(p); + + const entries: Entry[] = []; + + let lastID = 0; + for (let i = 0; i < numEntries; i++) { + const v = readVarint(p); + entries.push({ tileID: lastID + v, offset: 0, length: 0, runLength: 1 }); + lastID += v; + } + + // run lengths, lengths, and offsets + for (let i = 0; i < numEntries; i++) entries[i].runLength = readVarint(p); + for (let i = 0; i < numEntries; i++) entries[i].length = readVarint(p); + for (let i = 0; i < numEntries; i++) { + const v = readVarint(p); + if (v === 0 && i > 0) { + entries[i].offset = entries[i - 1].offset + entries[i - 1].length; + } else { + entries[i].offset = v - 1; + } + } + + return entries; +} + +/** + * @param dv - a DataView + * @param offset - the offset in the DataView + * @returns - the decoded 64-bit number + */ +export function getUint64(dv: DataView, offset: number): number { + const wh = dv.getUint32(offset + 4, true); + const wl = dv.getUint32(offset, true); + return wh * 2 ** 32 + wl; +} diff --git a/src/readers/pmtiles/reader.ts b/src/readers/pmtiles/reader.ts new file mode 100644 index 00000000..a1fa1974 --- /dev/null +++ b/src/readers/pmtiles/reader.ts @@ -0,0 +1,239 @@ +import DirCache from '../../dataStructures/cache'; +import { FetchReader } from '..'; +import { concatUint8Arrays } from '../../util'; +import { Compression, bytesToHeader, deserializeDir, findTile, zxyToTileID } from './pmtiles'; +import { S2_HEADER_SIZE_BYTES, S2_ROOT_SIZE, s2BytesToHeader } from './s2pmtiles'; + +import type { Reader } from '..'; +import type { Entry, Header } from './pmtiles'; +import type { Face, Metadata } from 's2-tilejson'; +import type { S2Entries, S2Header } from './s2pmtiles'; + +/** The File reader is to be used by bun/node/deno on the local filesystem. */ +export class S2PMTilesReader { + #header: Header | S2Header | undefined; + #reader: Reader; + // root directory will exist if header does + #rootDir: Entry[] = []; + #rootDirS2: S2Entries = { 0: [], 1: [], 2: [], 3: [], 4: [], 5: [] }; + #metadata!: Metadata; + #dirCache: DirCache; + #decoder = new TextDecoder('utf-8'); + + /** + * Given an input path, read in the header and root directory + * @param path - the location of the PMTiles data + * @param rangeRequests - FetchReader specific; enable range requests or use urlParam "bytes" + * @param maxSize - the max size of the cache before dumping old data. Defaults to 20. + */ + constructor( + readonly path: string | Reader, + rangeRequests: boolean = false, + maxSize = 20, + ) { + if (typeof path === 'string') { + this.#reader = new FetchReader(path, rangeRequests); + } else { + this.#reader = path; + } + this.#dirCache = new DirCache(maxSize); + } + + /** + * @returns - the header of the archive along with the root directory, + * including information such as tile type, min/max zoom, bounds, and summary statistics. + */ + async #getMetadata(): Promise
{ + if (this.#header !== undefined) return this.#header; + const data = await this.#reader.getRange(0, S2_ROOT_SIZE); + const headerData = data.slice(0, S2_HEADER_SIZE_BYTES); + // check if s2 + const isS2 = headerData[0] === 83 && headerData[1] === 50; + // header + const headerFunction = isS2 ? s2BytesToHeader : bytesToHeader; + const header = (this.#header = headerFunction(headerData)); + + // json metadata + const jsonMetadata = data.slice( + header.jsonMetadataOffset, + header.jsonMetadataOffset + header.jsonMetadataLength, + ); + this.#metadata = JSON.parse( + this.#arrayBufferToString(await decompress(jsonMetadata, header.internalCompression)), + ); + + // root directory data + const rootDirData = data.slice( + header.rootDirectoryOffset, + header.rootDirectoryOffset + header.rootDirectoryLength, + ); + this.#rootDir = deserializeDir(await decompress(rootDirData, header.internalCompression)); + + if (isS2) await this.#getS2Metadata(data, header as S2Header); + + return header; + } + + /** + * If S2 Projection, pull in the rest of the data + * @param data - the root data + * @param header - the S2 header with pointers to the rest of the data + */ + async #getS2Metadata(data: Uint8Array, header: S2Header): Promise { + // move the root directory to the s2 root + this.#rootDirS2[0] = this.#rootDir; + // add the 4 other faces + for (const face of [1, 2, 3, 4, 5]) { + const rootOffset = `rootDirectoryOffset${face}` as keyof S2Header; + const rootLenght = `rootDirectoryLength${face}` as keyof S2Header; + const faceDirData = data.slice( + header[rootOffset] as number, + (header[rootOffset] as number) + (header[rootLenght] as number), + ); + this.#rootDirS2[face as keyof S2Entries] = deserializeDir( + await decompress(faceDirData, header.internalCompression), + ); + } + } + + /** @returns - the header of the archive */ + async getHeader(): Promise
{ + return await this.#getMetadata(); + } + + /** @returns - the metadata of the archive */ + async getMetadata(): Promise { + await this.#getMetadata(); // ensure loaded first + return this.#metadata; + } + + /** + * @param face - the Open S2 projection face + * @param zoom - the zoom level of the tile + * @param x - the x coordinate of the tile + * @param y - the y coordinate of the tile + * @returns - the bytes of the tile at the given (face, zoom, x, y) coordinates, or undefined if the tile does not exist in the archive. + */ + async getTileS2(face: Face, zoom: number, x: number, y: number): Promise { + return await this.#getTile(face, zoom, x, y); + } + + /** + * @param zoom - the zoom level of the tile + * @param x - the x coordinate of the tile + * @param y - the y coordinate of the tile + * @returns - the bytes of the tile at the given (z, x, y) coordinates, or undefined if the tile does not exist in the archive. + */ + async getTile(zoom: number, x: number, y: number): Promise { + return await this.#getTile(-1, zoom, x, y); + } + + /** + * @param face - the Open S2 projection face + * @param zoom - the zoom level of the tile + * @param x - the x coordinate of the tile + * @param y - the y coordinate of the tile + * @returns - the bytes of the tile at the given (z, x, y) coordinates, or undefined if the tile does not exist in the archive. + */ + async #getTile( + face: number, + zoom: number, + x: number, + y: number, + ): Promise { + const header = await this.#getMetadata(); + const tileID = zxyToTileID(zoom, x, y); + const { minZoom, maxZoom, rootDirectoryOffset, rootDirectoryLength, tileDataOffset } = header; + if (zoom < minZoom || zoom > maxZoom) return undefined; + + let dO = rootDirectoryOffset; + let dL = rootDirectoryLength; + + for (let depth = 0; depth <= 3; depth++) { + const directory = await this.#getDirectory(dO, dL, face); + if (directory === undefined) return undefined; + const entry = findTile(directory, tileID); + if (entry !== null) { + if (entry.runLength > 0) { + const entryData = await this.#reader.getRange( + tileDataOffset + entry.offset, + entry.length, + ); + return await decompress(entryData, header.tileCompression); + } + dO = header.leafDirectoryOffset + entry.offset; + dL = entry.length; + } else return undefined; + } + throw Error('Maximum directory depth exceeded'); + } + + /** + * @param offset - the offset of the directory + * @param length - the length of the directory + * @param face - -1 for WM root, 0-5 for S2 + * @returns - the entries in the directory if it exists + */ + async #getDirectory(offset: number, length: number, face: number): Promise { + const dir = face === -1 ? this.#rootDir : this.#rootDirS2[face as Face]; + const header = await this.#getMetadata(); + const { internalCompression, rootDirectoryOffset } = header; + // if rootDirectoryOffset, return roon + if (offset === rootDirectoryOffset) return dir; + // check cache + const cache = this.#dirCache.get(offset); + if (cache !== undefined) return cache; + // get from archive + const resp = await this.#reader.getRange(offset, length); + const data = await decompress(resp, internalCompression); + const directory = deserializeDir(data); + if (directory.length === 0) throw new Error('Empty directory is invalid'); + // save in cache + this.#dirCache.set(offset, directory); + + return directory; + } + + /** + * @param buffer - the buffer to convert + * @returns - the string result + */ + #arrayBufferToString(buffer: Uint8Array): string { + return this.#decoder.decode(buffer); + } +} + +/** + * @param data - the data to decompress + * @param compression - the compression type + * @returns - the decompressed data + */ +async function decompress(data: Uint8Array, compression: Compression): Promise { + switch (compression) { + case Compression.Gzip: + return decompressGzip(data); + case Compression.Brotli: + throw new Error('Brotli decompression not implemented'); + case Compression.Zstd: + throw new Error('Zstd decompression not implemented'); + case Compression.None: + default: + return data; + } +} + +/** + * @param compressedBytes - the data to decompress + * @returns - the decompressed data + */ +async function decompressGzip(compressedBytes: Uint8Array): Promise { + // Convert the bytes to a stream. + const stream = new Blob([compressedBytes]).stream(); + // Create a decompressed stream. + const decompressedStream = stream.pipeThrough(new DecompressionStream('gzip')); + // Read all the bytes from this stream. + const chunks = []; + for await (const chunk of decompressedStream) chunks.push(chunk); + + return await concatUint8Arrays(chunks); +} diff --git a/src/readers/pmtiles/s2pmtiles.ts b/src/readers/pmtiles/s2pmtiles.ts new file mode 100644 index 00000000..f32caddf --- /dev/null +++ b/src/readers/pmtiles/s2pmtiles.ts @@ -0,0 +1,75 @@ +import { bytesToHeader, getUint64 } from './pmtiles'; + +import type { Entry, Header } from './pmtiles'; + +/** Store entries for each Face */ +export interface S2Entries { + 0: Entry[]; + 1: Entry[]; + 2: Entry[]; + 3: Entry[]; + 4: Entry[]; + 5: Entry[]; +} + +/** S2PMTiles v3 header storing basic archive-level information. */ +export interface S2Header extends Header { + rootDirectoryOffset1: number; + rootDirectoryLength1: number; + rootDirectoryOffset2: number; + rootDirectoryLength2: number; + rootDirectoryOffset3: number; + rootDirectoryLength3: number; + rootDirectoryOffset4: number; + rootDirectoryLength4: number; + rootDirectoryOffset5: number; + rootDirectoryLength5: number; + leafDirectoryOffset1: number; + leafDirectoryLength1: number; + leafDirectoryOffset2: number; + leafDirectoryLength2: number; + leafDirectoryOffset3: number; + leafDirectoryLength3: number; + leafDirectoryOffset4: number; + leafDirectoryLength4: number; + leafDirectoryOffset5: number; + leafDirectoryLength5: number; +} + +export const S2_HEADER_SIZE_BYTES = 262; + +export const S2_ROOT_SIZE = 98_304; + +/** + * Parse raw header bytes into a Header object. + * @param bytes - the raw header bytes + * @returns the parsed header + */ +export function s2BytesToHeader(bytes: Uint8Array): S2Header { + const baseHeader = bytesToHeader(bytes); + const dv = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength); + + return { + ...baseHeader, + rootDirectoryOffset1: getUint64(dv, 102), + rootDirectoryLength1: getUint64(dv, 110), + rootDirectoryOffset2: getUint64(dv, 118), + rootDirectoryLength2: getUint64(dv, 126), + rootDirectoryOffset3: getUint64(dv, 134), + rootDirectoryLength3: getUint64(dv, 142), + rootDirectoryOffset4: getUint64(dv, 150), + rootDirectoryLength4: getUint64(dv, 158), + rootDirectoryOffset5: getUint64(dv, 166), + rootDirectoryLength5: getUint64(dv, 174), + leafDirectoryOffset1: getUint64(dv, 182), + leafDirectoryLength1: getUint64(dv, 190), + leafDirectoryOffset2: getUint64(dv, 198), + leafDirectoryLength2: getUint64(dv, 206), + leafDirectoryOffset3: getUint64(dv, 214), + leafDirectoryLength3: getUint64(dv, 222), + leafDirectoryOffset4: getUint64(dv, 230), + leafDirectoryLength4: getUint64(dv, 238), + leafDirectoryOffset5: getUint64(dv, 246), + leafDirectoryLength5: getUint64(dv, 254), + }; +} diff --git a/src/readers/pmtiles/varint.ts b/src/readers/pmtiles/varint.ts new file mode 100644 index 00000000..e5431bbf --- /dev/null +++ b/src/readers/pmtiles/varint.ts @@ -0,0 +1,66 @@ +/** A buffer with the position to read from */ +export interface VarintBufPos { + buf: Uint8Array; + pos: number; +} + +/** + * @param low - the low 32 bits of the number + * @param high - the high 32 bits of the number + * @returns - the decoded number + */ +function toNum(low: number, high: number): number { + return (high >>> 0) * 0x100000000 + (low >>> 0); +} + +/** + * @param bufPos - the buffer with it's position + * @returns - the decoded number + */ +export function readVarint(bufPos: VarintBufPos): number { + const buf = bufPos.buf; + let b = buf[bufPos.pos++]; + let val = b & 0x7f; + if (b < 0x80) return val; + b = buf[bufPos.pos++]; + val |= (b & 0x7f) << 7; + if (b < 0x80) return val; + b = buf[bufPos.pos++]; + val |= (b & 0x7f) << 14; + if (b < 0x80) return val; + b = buf[bufPos.pos++]; + val |= (b & 0x7f) << 21; + if (b < 0x80) return val; + b = buf[bufPos.pos]; + val |= (b & 0x0f) << 28; + + return readVarintRemainder(val, bufPos); +} + +/** + * @param low - the low 32 bits of the number + * @param bufPos - the buffer with it's position + * @returns - the decoded remainder + */ +export function readVarintRemainder(low: number, bufPos: VarintBufPos): number { + const buf = bufPos.buf; + let b = buf[bufPos.pos++]; + let h = (b & 0x70) >> 4; + if (b < 0x80) return toNum(low, h); + b = buf[bufPos.pos++]; + h |= (b & 0x7f) << 3; + if (b < 0x80) return toNum(low, h); + b = buf[bufPos.pos++]; + h |= (b & 0x7f) << 10; + if (b < 0x80) return toNum(low, h); + b = buf[bufPos.pos++]; + h |= (b & 0x7f) << 17; + if (b < 0x80) return toNum(low, h); + b = buf[bufPos.pos++]; + h |= (b & 0x7f) << 24; + if (b < 0x80) return toNum(low, h); + b = buf[bufPos.pos++]; + h |= (b & 0x01) << 31; + if (b < 0x80) return toNum(low, h); + throw new Error('Expected varint not more than 10 bytes'); +} diff --git a/src/readers/shapefile/file.ts b/src/readers/shapefile/file.ts index 91aac5dc..02271eca 100644 --- a/src/readers/shapefile/file.ts +++ b/src/readers/shapefile/file.ts @@ -1,10 +1,12 @@ import DataBaseFile from './dbf'; -import FileReader from '../fileReader'; +import FileReader from '../file'; import Shapefile from './shp'; import { Transformer } from 's2-tools/proj4'; import { fromGzip } from '.'; import { exists, readFile } from 'fs/promises'; +import type { ProjectionTransformDefinition } from 's2-tools/proj4'; + export * from './dbf'; export * from './shp'; @@ -30,9 +32,13 @@ export interface Definition { * Assumes the input is pointing to a shapefile or name without the extension. * The algorithm will find the rest of the paths if they exist. * @param input - the path to the .shp file or name without the extension + * @param defs - optional array of ProjectionTransformDefinitions to insert * @returns - a Shapefile */ -export async function fromPath(input: string): Promise { +export async function fromPath( + input: string, + defs?: ProjectionTransformDefinition[], +): Promise { if (input.endsWith('.zip')) { const gzipData = await readFile(input); return fromGzip(gzipData.buffer); @@ -49,20 +55,28 @@ export async function fromPath(input: string): Promise { prj: (await exists(prj)) ? prj : undefined, cpg: (await exists(cpg)) ? cpg : undefined, }; - return fromDefinition(definition); + return fromDefinition(definition, defs); } /** * Build a Shapefile from a Definition * @param def - a description of the data to parse + * @param defs - optional array of ProjectionTransformDefinitions to insert * @returns - a Shapefile */ -export async function fromDefinition(def: Definition): Promise { +export async function fromDefinition( + def: Definition, + defs?: ProjectionTransformDefinition[], +): Promise { const { shp, dbf, prj, cpg } = def; const encoding = cpg ? await readFile(cpg, { encoding: 'utf8' }) : 'utf8'; const transform = prj ? new Transformer(await readFile(prj, { encoding: 'utf8' })) : undefined; const dbfReader = dbf !== undefined ? new FileReader(dbf) : undefined; const databaseFile = dbfReader !== undefined ? new DataBaseFile(dbfReader, encoding) : undefined; + if (transform && defs) { + for (const def of defs) transform.insertDefinition(def); + } + return new Shapefile(new FileReader(shp), databaseFile, transform); } diff --git a/src/readers/shapefile/index.ts b/src/readers/shapefile/index.ts index 0398678d..6ce657bd 100644 --- a/src/readers/shapefile/index.ts +++ b/src/readers/shapefile/index.ts @@ -4,6 +4,8 @@ import ShapeFile from './shp'; import { Transformer } from 's2-tools/proj4'; import { iterItems } from 's2-tools/util'; +import type { ProjectionTransformDefinition } from 's2-tools/proj4'; + export { default as DataBaseFile } from './dbf'; export { default as ShapeFile } from './shp'; @@ -13,12 +15,15 @@ export type * from './shp'; /** * Assumes the input is pointing to shapefile data. * @param input - raw buffer of gzipped data (folder of shp, dbf, prj, and/or cpg) + * @param defs - optional array of ProjectionTransformDefinitions to insert * @returns - a Shapefile */ -export async function fromGzip(input: ArrayBufferLike): Promise { - // TODO: BUILD TRANSFORM!!!!!!! +export async function fromGzip( + input: ArrayBufferLike, + defs?: ProjectionTransformDefinition[], +): Promise { let encoding = 'utf8'; - const transform: Transformer | undefined = undefined; + let transform: Transformer | undefined = undefined; let dbfReader: DataBaseFile | undefined = undefined; let shpData: Uint8Array | undefined = undefined; for (const item of iterItems(new Uint8Array(input))) { @@ -29,6 +34,12 @@ export async function fromGzip(input: ArrayBufferLike): Promise { dbfReader = new DataBaseFile(new BufferReader(data.buffer), encoding); } else if (item.filename.endsWith('shp')) { shpData = await item.read(); + } else if (item.filename.endsWith('prj')) { + const data = await item.read(); + transform = new Transformer(new TextDecoder('utf8').decode(data)); + if (defs) { + for (const def of defs) transform.insertDefinition(def); + } } } if (shpData === undefined) throw new Error('Shapefile not found'); diff --git a/src/readers/shapefile/mmap.ts b/src/readers/shapefile/mmap.ts index 7e7be642..f64d3396 100644 --- a/src/readers/shapefile/mmap.ts +++ b/src/readers/shapefile/mmap.ts @@ -1,5 +1,5 @@ import DataBaseFile from './dbf'; -import MMapReader from '../mmapReader'; +import MMapReader from '../mmap'; import Shapefile from './shp'; import { Transformer } from 's2-tools/proj4'; import { exists, readFile } from 'fs/promises'; diff --git a/src/readers/shapefile/shp.ts b/src/readers/shapefile/shp.ts index 59f46bd1..64b3c818 100644 --- a/src/readers/shapefile/shp.ts +++ b/src/readers/shapefile/shp.ts @@ -2,7 +2,6 @@ import { extendBBox } from 's2-tools/geometry'; import type DataBaseFile from './dbf'; -import type { Reader } from '..'; import type { Transformer } from 's2-tools/proj4'; import type { BBOX, @@ -20,6 +19,7 @@ import type { VectorPointGeometry, VectorPolygonGeometry, } from 's2-tools/geometry'; +import type { FeatureIterator, Reader } from '..'; /** A Shapefile Header describing the internal data */ export interface SHPHeader { @@ -38,7 +38,7 @@ export interface SHPRow { } /** The Shapefile Reader */ -export default class Shapefile { +export default class Shapefile implements FeatureIterator { #header!: SHPHeader; rows: number[] = []; /** @@ -67,14 +67,14 @@ export default class Shapefile { * Return all the features in the shapefile * @returns - a collection of VectorFeatures */ - getFeatureCollection(): FeatureCollection { + async getFeatureCollection(): Promise { const featureCollection: FeatureCollection = { type: 'FeatureCollection', features: [], bbox: this.#header.bbox, }; - for (const feature of this.iterate()) { + for await (const feature of this) { featureCollection.features.push(feature); } @@ -85,7 +85,7 @@ export default class Shapefile { * Iterate over all features in the shapefile * @yields {VectorFeature} */ - *iterate(): IterableIterator { + async *[Symbol.asyncIterator](): AsyncGenerator { for (let i = 0; i < this.rows.length; i++) { const feature = this.#parseRow(this.rows[i], i); if (feature !== undefined) yield feature; diff --git a/src/readers/wkt/index.ts b/src/readers/wkt/index.ts index 3223abbd..c52a624f 100644 --- a/src/readers/wkt/index.ts +++ b/src/readers/wkt/index.ts @@ -5,7 +5,8 @@ export * from './projection'; export type * from './projection'; /** - * @param str + * @param str - string to clean + * @returns - cleaned string */ export function cleanString(str: string): string { return str diff --git a/src/readers/wkt/projection.ts b/src/readers/wkt/projection.ts index 17d20956..c4b265d5 100644 --- a/src/readers/wkt/projection.ts +++ b/src/readers/wkt/projection.ts @@ -1,6 +1,7 @@ import { degToRad } from '../../geometry'; import { parseWKTObject } from '.'; +import type { ProjectionParams } from 's2-tools/proj4'; import type { WKTObject, WKTValue } from '.'; /** @@ -69,7 +70,7 @@ export type DatumParams = [number, number, number, number, number, number, numbe /** * */ -export interface WKTCRS { +export interface WKTCRS extends ProjectionParams { type?: string; name?: string; local?: boolean; @@ -80,6 +81,7 @@ export interface WKTCRS { PROJCS?: Omit; VERT_CS?: VertCS; PROJECTION?: string; + rectified_grid_angle?: number; standard_parallel_1?: number; standard_parallel_2?: number; latitude_of_origin?: number; @@ -90,11 +92,11 @@ export interface WKTCRS { false_northing?: number; AUTHORITY?: Authority; AXIS?: [string, string][]; - projName?: string; units?: string; to_meter?: number; datumCode?: string; ellps?: string; + from_greenwich?: number; a?: number; b?: number; rf?: number; @@ -102,6 +104,7 @@ export interface WKTCRS { y0?: number; k0?: number; lat_ts?: number; + latTS?: number; longc?: number; long0?: number; lat0?: number; @@ -421,6 +424,8 @@ function updateProj(wkt: WKTCRS): void { remap(wkt, 'standard_parallel_1', 'Latitude of 1st standard parallel' as keyof WKTCRS); remap(wkt, 'standard_parallel_2', 'Standard_Parallel_2' as keyof WKTCRS); remap(wkt, 'standard_parallel_2', 'Latitude of 2nd standard parallel' as keyof WKTCRS); + remap(wkt, 'rectified_grid_angle', 'Rectified_Grid_Angle' as keyof WKTCRS); + remap(wkt, 'rectifiedGridAngle', 'rectified_grid_angle' as keyof WKTCRS); remap(wkt, 'false_easting', 'False_Easting' as keyof WKTCRS); remap(wkt, 'false_easting', 'easting' as keyof WKTCRS); remap(wkt, 'false_easting', 'Easting at false origin' as keyof WKTCRS); @@ -435,7 +440,6 @@ function updateProj(wkt: WKTCRS): void { remap(wkt, 'latitude_of_origin', 'Latitude of natural origin' as keyof WKTCRS); remap(wkt, 'latitude_of_origin', 'Latitude of false origin' as keyof WKTCRS); remap(wkt, 'scale_factor', 'Scale_Factor' as keyof WKTCRS); - remap(wkt, 'k0', 'scale_factor'); remap(wkt, 'latitude_of_center', 'Latitude_Of_Center' as keyof WKTCRS); remap(wkt, 'latitude_of_center', 'Latitude_of_center' as keyof WKTCRS); remap(wkt, 'lat0', 'latitude_of_center', degToRad); @@ -451,6 +455,14 @@ function updateProj(wkt: WKTCRS): void { remap(wkt, 'lat2', 'standard_parallel_2', degToRad); remap(wkt, 'azimuth', 'Azimuth' as keyof WKTCRS); remap(wkt, 'alpha', 'azimuth', degToRad); + // uppercase all + remap(wkt, 'toMeter', 'to_meter'); + remap(wkt, 'fromGreenwich', 'from_greenwich'); + // latTS, datumParams, and scaleFactor + remap(wkt, 'latTs', 'lat_ts', degToRad); + remap(wkt, 'datumParams', 'datum_params'); + remap(wkt, 'scaleFactor', 'scale_factor'); + remap(wkt, 'k0', 'scaleFactor'); // update long0 if applicable if ( wkt.long0 === undefined && @@ -466,9 +478,9 @@ function updateProj(wkt: WKTCRS): void { ['Stereographic_South_Pole', 'Polar Stereographic (variant B)'].includes(wkt.projName ?? '') ) { wkt.lat0 = degToRad(wkt.lat1 > 0 ? 90 : -90); - wkt.lat_ts = wkt.lat1; + wkt.lat_ts = wkt.latTs = wkt.lat1; } else if (!wkt.lat_ts && wkt.lat0 && wkt.projName === 'Polar_Stereographic') { - wkt.lat_ts = wkt.lat0; + wkt.lat_ts = wkt.latTs = wkt.lat0; wkt.lat0 = degToRad(wkt.lat0 > 0 ? 90 : -90); } } diff --git a/src/readers/xml/index.ts b/src/readers/xml/index.ts new file mode 100644 index 00000000..65587db3 --- /dev/null +++ b/src/readers/xml/index.ts @@ -0,0 +1 @@ +export * from './parsing'; diff --git a/src/readers/xml/parsing.ts b/src/readers/xml/parsing.ts new file mode 100644 index 00000000..390d52e8 --- /dev/null +++ b/src/readers/xml/parsing.ts @@ -0,0 +1,269 @@ +/** Options for xml parsing */ +export interface Options { + debug?: boolean; + startIndex?: number; + nested?: boolean; + returnOnFirst?: boolean; +} + +/** A Tag is a pair of an inner and an outer strings with their indexes */ +export type Tag = { inner: null | string; outer: string; start: number; end: number }; +/** A Step is a name and an index */ +export type Step = { name: string; index?: number | undefined | null }; +/** A Path is an array of Steps or Strings */ +export type Path = Array | ReadonlyArray; + +/** + * @param string - the string + * @param substring - the substring + * @returns the number of times the substring appears in the string + */ +export function countSubstring(string: string, substring: string): number { + const pattern = new RegExp(substring, 'g'); + const match = string.match(pattern); + return match ? match.length : 0; +} + +/** + * @param xml - the xml string + * @param tagName - the tag name + * @param options - user defined options + * @returns the first tag with the given name + */ +export function findTagByName(xml: string, tagName: string, options?: Options): Tag | undefined { + const debug = (options && options.debug) ?? false; + const nested = !(options && options.nested === false); + + const startIndex = (options && options.startIndex) ?? 0; + + if (debug) console.info('[xml-utils] starting findTagByName with', tagName, ' and ', options); + + const start = indexOfMatch(xml, `<${tagName}[ \n>/]`, startIndex); + if (debug) console.info('[xml-utils] start:', start); + if (start === -1) return undefined; + + const afterStart = xml.slice(start + tagName.length); + + let relativeEnd = indexOfMatchEnd(afterStart, '^[^<]*[ /]>', 0); + + const selfClosing = relativeEnd !== -1 && afterStart[relativeEnd - 1] === '/'; + if (debug) console.info('[xml-utils] selfClosing:', selfClosing); + + if (selfClosing === false) { + // check if tag has subtags with the same name + if (nested) { + let startIndex = 0; + let openings = 1; + let closings = 0; + while ( + (relativeEnd = indexOfMatchEnd(afterStart, '[ /]' + tagName + '>', startIndex)) !== -1 + ) { + const clip = afterStart.substring(startIndex, relativeEnd + 1); + openings += countSubstring(clip, '<' + tagName + '[ \n\t>]'); + closings += countSubstring(clip, ''); + // we can't have more openings than closings + if (closings >= openings) break; + startIndex = relativeEnd; + } + } else { + relativeEnd = indexOfMatchEnd(afterStart, '[ /]' + tagName + '>', 0); + } + } + + const end = start + tagName.length + relativeEnd + 1; + if (debug) console.info('[xml-utils] end:', end); + if (end === -1) return undefined; + + const outer = xml.slice(start, end); + // tag is like urn:ogc:def:crs:EPSG::32617 + + let inner; + if (selfClosing) { + inner = null; + } else { + inner = outer.slice(outer.indexOf('>') + 1, outer.lastIndexOf('<')); + } + + return { inner, outer, start, end }; +} + +/** + * @param xml - the xml string + * @param path - the path + * @param options - user defined options + * @returns the first tag with the given path + */ +export function findTagByPath(xml: string, path: Path, options?: Options): Tag | undefined { + const debug = (options && options.debug) ?? false; + const found = findTagsByPath(xml, path, { debug, returnOnFirst: true }); + if (Array.isArray(found) && found.length === 1) return found[0]; + else return undefined; +} + +/** + * @param xml - the xml string + * @param tagName - the tag name + * @param options - user defined options + * @returns all tags with the given name + */ +export function findTagsByName(xml: string, tagName: string, options?: Options): Tag[] { + const tags = []; + const debug = (options && options.debug) ?? false; + const nested = options && typeof options.nested === 'boolean' ? options.nested : true; + let startIndex = (options && options.startIndex) ?? 0; + let tag; + while ((tag = findTagByName(xml, tagName, { debug, startIndex }))) { + if (nested) { + startIndex = tag.start + 1 + tagName.length; + } else { + startIndex = tag.end; + } + tags.push(tag); + } + if (debug) console.info('findTagsByName found', tags.length, 'tags'); + return tags; +} + +/** + * @param xml - the xml string + * @param path - the path + * @param options - user defined options + * @returns all tags with the given path + */ +export function findTagsByPath(xml: string, path: Path, options?: Options): Tag[] { + const debug = (options && options.debug) ?? false; + if (debug) console.info('[xml-utils] starting findTagsByPath with: ', xml.substring(0, 500)); + const returnOnFirst = (options && options.returnOnFirst) ?? false; + + if (Array.isArray(path) === false) throw new Error('[xml-utils] path should be an array'); + + const path0 = typeof path[0] === 'string' ? { name: path[0] } : path[0]; + let tags = findTagsByName(xml, path0.name, { debug, nested: false }); + if (typeof tags !== 'undefined' && typeof path0.index === 'number') { + if (typeof tags[path0.index] === 'undefined') { + tags = []; + } else { + tags = [tags[path0.index]]; + } + } + if (debug) console.info('first tags are:', tags); + + path = path.slice(1); + + for (let pathIndex = 0; pathIndex < path.length; pathIndex++) { + const part: { name: string } | Step = + typeof path[pathIndex] === 'string' + ? { name: path[pathIndex] as string } + : (path[pathIndex] as Step); + if (debug) console.info('part.name:', part.name); + let allSubTags: Tag[] = []; + for (let tagIndex = 0; tagIndex < tags.length; tagIndex++) { + const tag = tags[tagIndex]; + const subTags = findTagsByName(tag.outer, part.name, { + debug, + startIndex: 1, + }); + + if (debug) console.info('subTags.length:', subTags.length); + if (subTags.length > 0) { + subTags.forEach((subTag) => { + subTag.start += tag.start; + subTag.end += tag.start; + }); + if (returnOnFirst && pathIndex === path.length - 1) return [subTags[0]]; + allSubTags = allSubTags.concat(subTags); + } + } + tags = allSubTags; + if ('index' in part && typeof part.index === 'number') { + if (typeof tags[part.index] === 'undefined') { + tags = []; + } else { + tags = [tags[part.index]]; + } + } + } + return tags; +} + +/** + * @param tag - the tag + * @param attributeName - the attribute name + * @param options - user defined options + * @returns the attribute value + */ +export function getAttribute( + tag: string | Tag, + attributeName: string, + options?: Options, +): string | undefined { + const debug = (options && options.debug) ?? false; + if (debug) console.info('[xml-utils] getting ' + attributeName + ' in ' + tag); + + const xml = typeof tag === 'object' ? tag.outer : tag; + + // only search for attributes in the opening tag + const opening = xml.slice(0, xml.indexOf('>') + 1); + + const quotechars = ['"', "'"]; + for (let i = 0; i < quotechars.length; i++) { + const char = quotechars[i]; + const pattern = attributeName + '\\=' + char + '([^' + char + ']*)' + char; + if (debug) console.info('[xml-utils] pattern:', pattern); + + const re = new RegExp(pattern); + const match = re.exec(opening); + if (debug) console.info('[xml-utils] match:', match); + if (match) return match[1]; + } +} + +/** + * @param xml - the xml string + * @param pattern - the pattern + * @param startIndex - the start index + * @returns the index of the last match + */ +export function indexOfMatchEnd(xml: string, pattern: string, startIndex: number): number { + const re = new RegExp(pattern); + const match = re.exec(xml.slice(startIndex)); + if (match) return startIndex + match.index + match[0].length - 1; + else return -1; +} + +/** + * @param xml - the xml string + * @param pattern - the pattern + * @param startIndex - the start index + * @returns the index of the first match + */ +export function indexOfMatch(xml: string, pattern: string, startIndex: number): number { + const re = new RegExp(pattern); + const match = re.exec(xml.slice(startIndex)); + if (match) return startIndex + match.index; + else return -1; +} + +/** + * @param xml - the xml string + * @returns the xml without comments + */ +export function removeComments(xml: string): string { + return xml.replace(//g, ''); +} + +/** + * @param xml - the xml string + * @param tagName - the tag name + * @param options - user defined options + * @returns the xml without the given tag + */ +export function removeTagsByName(xml: string, tagName: string, options?: Options) { + const debug = (options && options.debug) ?? false; + let tag; + while ((tag = findTagByName(xml, tagName, { debug }))) { + xml = xml.substring(0, tag.start) + xml.substring(tag.end); + if (debug) console.info('[xml-utils] removed:', tag); + } + return xml; +} diff --git a/src/space/gpu/index.ts b/src/space/gpu/index.ts new file mode 100644 index 00000000..3c6b2b5d --- /dev/null +++ b/src/space/gpu/index.ts @@ -0,0 +1,266 @@ +import computeShader from './wgsl/index.ts'; +import { earthRadius, j2, j3oj2, pi, twoPi, vkmpersec, x2o3, xke } from '../util/constants.ts'; + +import type { Satellite } from '../sat.ts'; + +// After creating the class you must call `await bfGPU.init()` +/** + * + */ +export default class SGP4GPU { + #device: GPUDevice; + #sgp4Pipeline!: GPUComputePipeline; + #layout!: GPUBindGroupLayout; + #bindGroup0!: GPUBindGroup; + #bindGroup1!: GPUBindGroup; + #size = 0; + /** + * @param device + */ + constructor(device: GPUDevice) { + this.#device = device; + } + + /** + * + */ + async init(): Promise { + const layout = (this.#layout = this.#device.createBindGroupLayout({ + entries: [ + { + // constants + binding: 0, + visibility: GPUShaderStage.COMPUTE, + buffer: { + type: 'uniform', + }, + }, + { + // tsince + binding: 1, + visibility: GPUShaderStage.COMPUTE, + buffer: { + type: 'uniform', + }, + }, + { + // size + binding: 2, + visibility: GPUShaderStage.COMPUTE, + buffer: { + type: 'uniform', + }, + }, + { + // array + binding: 3, + visibility: GPUShaderStage.COMPUTE, + buffer: { + type: 'read-only-storage', + }, + }, + { + // array + binding: 4, + visibility: GPUShaderStage.COMPUTE, + buffer: { + type: 'storage', + }, + }, + ], + })); + + // ** BUILD PIPELINES ** + + // ? SGP4 + // Compute shader code + const computeShaderModule = this.#device.createShaderModule({ code: computeShader }); + // Pipeline setup + this.#sgp4Pipeline = await this.#device.createComputePipelineAsync({ + label: 'SGP4', + layout: this.#device.createPipelineLayout({ + bindGroupLayouts: [layout], + }), + compute: { + module: computeShaderModule, + entryPoint: 'sgp4', + }, + }); + } + + // returns the distance GPU Buffer incase you want to use it + /** + * @param sats + */ + prepareData(sats: Satellite[]): GPUBuffer { + const size = (this.#size = sats.length); + // ? PREP CONSTANTS + const constants = new Float32Array([ + pi, // pi: f32 + twoPi, // twoPi: f32 + earthRadius, // earthRadius: f32 + xke, // xke: f32 + vkmpersec, // vkmpersec: f32 + j2, // j2: f32 + j3oj2, // j3oj2: f32 + x2o3, // x2o3: f32 + ]); + const gpuBufferConstants = this.#device.createBuffer({ + mappedAtCreation: true, + size: constants.byteLength, + usage: GPUBufferUsage.UNIFORM, + }); + const arrayBufferConstants = gpuBufferConstants.getMappedRange(); + new Float32Array(arrayBufferConstants).set(constants); + gpuBufferConstants.unmap(); + + // ? PREP SIZE + const sizeArr = new Float32Array([size]); + const gpuBufferSize = this.#device.createBuffer({ + mappedAtCreation: true, + size: sizeArr.byteLength, + usage: GPUBufferUsage.UNIFORM, + }); + const arrayBufferSize = gpuBufferSize.getMappedRange(); + new Float32Array(arrayBufferSize).set(sizeArr); + gpuBufferSize.unmap(); + + // ? Satellites + const satsArray = new Float32Array(sats.flatMap((s) => s.gpu())); + const gpuBufferSats = this.#device.createBuffer({ + mappedAtCreation: true, + size: satsArray.byteLength, + usage: GPUBufferUsage.STORAGE, + }); + const arrayBufferSats = gpuBufferSats.getMappedRange(); + new Float32Array(arrayBufferSats).set(satsArray); + gpuBufferSats.unmap(); + + // ? OUT (build result distance array) + const out = Array(size * 7).fill(0); + const outArray = new Uint32Array(out); + const gpuBufferOut = this.#device.createBuffer({ + mappedAtCreation: true, + size: size * 7 * 4, + usage: GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_SRC, + }); + const arrayBufferDist = gpuBufferOut.getMappedRange(); + new Uint32Array(arrayBufferDist).set(outArray); + gpuBufferOut.unmap(); + + // ? prep bind group + const entries = [ + { + binding: 0, + resource: { + buffer: gpuBufferConstants, + }, + }, + { + binding: 1, + resource: { + buffer: gpuBufferSize, + }, + }, + { + binding: 2, + resource: { + buffer: gpuBufferSats, + }, + }, + { + binding: 3, + resource: { + buffer: gpuBufferOut, + }, + }, + ]; + + // Bind group layout and bind group + this.#bindGroup0 = this.#device.createBindGroup({ layout: this.#layout, entries }); + + return gpuBufferOut; + } + + // returns the distance GPU Buffer incase you want to use it + /** + * @param tsince + */ + setTime(tsince: number): void { + // ? Prep buffer + const tsinceArr = new Float32Array([tsince]); + const gpuBufferTsince = this.#device.createBuffer({ + mappedAtCreation: true, + size: tsinceArr.byteLength, + usage: GPUBufferUsage.UNIFORM, + }); + const arrayBufferSize = gpuBufferTsince.getMappedRange(); + new Float32Array(arrayBufferSize).set(tsinceArr); + gpuBufferTsince.unmap(); + + // ? prep bind group + const entries = [ + { + binding: 0, + resource: { + buffer: gpuBufferTsince, + }, + }, + ]; + + // Bind group layout and bind group + this.#bindGroup1 = this.#device.createBindGroup({ layout: this.#layout, entries }); + } + + // const commandEncoder = device.createCommandEncoder() + // const passEncoder = commandEncoder.beginComputePass() + // do stuff... + // this.run(passEncoder) + // do stuff... + // passEncoder.end() + /** + * @param passEncoder + * @param blockSize + */ + run(passEncoder: GPUComputePassEncoder, blockSize = 256): void { + const numBlocks = Math.ceil((this.#size + blockSize - 1) / blockSize); + // Commands submission + passEncoder.setBindGroup(0, this.#bindGroup0); + passEncoder.setBindGroup(1, this.#bindGroup1); + passEncoder.setPipeline(this.#sgp4Pipeline); + passEncoder.dispatchWorkgroups(numBlocks); + } + + // run this command after sending all instructions to passEncoder but BEFORE submitting to the queue + // const gpuBufferOut = this.prepareData({ ... }) + // ... + // this.run(passEncoder) + // passEncoder.end() + // this.createReadDistBuffer(gpuBufferOut) + // device.queue.submit([commandEncoder.finish()]) + // await gpuReadBufferDist.mapAsync(GPUMapMode.READ) + // const resultDist = gpuReadBufferDist.getMappedRange() + // const resultAB = new Float32Array(resultDist) + /** + * @param commandEncoder + * @param gpuBufferOut + */ + createReadDistBuffer(commandEncoder: GPUCommandEncoder, gpuBufferOut: GPUBuffer): GPUBuffer { + const size = this.#size * 7 * 4; + const gpuReadBufferDist = this.#device.createBuffer({ + size, + usage: GPUBufferUsage.COPY_DST | GPUBufferUsage.MAP_READ, + }); + + // Encode commands for copying buffer to buffer. + commandEncoder.copyBufferToBuffer( + gpuBufferOut /* source buffer */, + 0 /* source offset */, + gpuReadBufferDist /* destination buffer */, + 0 /* destination offset */, + size /* size */, + ); + + return gpuReadBufferDist; + } +} diff --git a/src/space/gpu/wgsl/index.ts b/src/space/gpu/wgsl/index.ts new file mode 100644 index 00000000..e69de29b diff --git a/src/space/gpu/wgsl/sgp4.wgsl b/src/space/gpu/wgsl/sgp4.wgsl new file mode 100644 index 00000000..7810214a --- /dev/null +++ b/src/space/gpu/wgsl/sgp4.wgsl @@ -0,0 +1,946 @@ +struct Satellite { + anomaly: f32, + motion: f32, + eccentricity: f32, + inclination: f32, + method: f32, // 0 -> 'd', 1 -> 'n' + opsmode: f32, // 0 -> 'a'; 1 -> 'i' + drag: f32, + mdot: f32, + perigee: f32, + argpdot: f32, + ascension: f32, + nodedot: f32, + nodecf: f32, + cc1: f32, + cc4: f32, + cc5: f32, + t2cof: f32, + isimp: f32, + omgcof: f32, + eta: f32, + xmcof: f32, + delmo: f32, + d2: f32, + d3: f32, + d4: f32, + sinmao: f32, + t3cof: f32, + t4cof: f32, + t5cof: f32, + irez: f32, + d2201: f32, + d2211: f32, + d3210: f32, + d3222: f32, + d4410: f32, + d4422: f32, + d5220: f32, + d5232: f32, + d5421: f32, + d5433: f32, + dedt: f32, + del1: f32, + del2: f32, + del3: f32, + didt: f32, + dmdt: f32, + dnodt: f32, + domdt: f32, + gsto: f32, + xfact: f32, + xlamo: f32, + atime: f32, + xli: f32, + xni: f32, + aycof: f32, + xlcof: f32, + con41: f32, + x1mth2: f32, + x7thm1: f32, + zmos: f32, + zmol: f32, + se2: f32, + se3: f32, + si2: f32, + si3: f32, + sl2: f32, + sl3: f32, + sl4: f32, + sgh2: f32, + sgh3: f32, + sgh4: f32, + sh2: f32, + sh3: f32, + ee2: f32, + e3: f32, + xi2: f32, + xi3: f32, + xl2: f32, + xl3: f32, + xl4: f32, + xgh2: f32, + xgh3: f32, + xgh4: f32, + xh2: f32, + xh3: f32, + peo: f32, + pinco: f32, + plo: f32, + pgho: f32, + pho: f32 +}; + +struct SatOutput { + error: f32, + position: vec3, + velocity: vec3, +}; + +struct Constants { + pi: f32, + twoPi: f32, + earthRadius: f32, + xke: f32, + vkmpersec: f32, + j2: f32, + j3oj2: f32, + x2o3: f32 +}; + +struct DpperOptions { + init: bool, + ep: f32, + inclp: f32, + nodep: f32, + argpp: f32, + mp: f32 +}; + +struct DpperOutput { + ep: f32 + inclp: f32 + nodep: f32 + argpp: f32 + mp: f32 +}; + +struct DspaceOptions { + irez: f32 + d2201: f32 + d2211: f32 + d3210: f32 + d3222: f32 + d4410: f32 + d4422: f32 + d5220: f32 + d5232: f32 + d5421: f32 + d5433: f32 + dedt: f32 + del1: f32 + del2: f32 + del3: f32 + didt: f32 + dmdt: f32 + dnodt: f32 + domdt: f32 + argpo: f32 + argpdot: f32 + tc: f32 + gsto: f32 + xfact: f32 + xlamo: f32 + no: f32 + atime: f32 + em: f32 + argpm: f32 + inclm: f32 + xli: f32 + mm: f32 + xni: f32 + nodem: f32 + nm: f32 +}; + +struct DspaceOutput { + atime: f32 + em: f32 // eccentricity + argpm: f32 // argument of perigee + inclm: f32 // inclination + xli: f32 + mm: f32 // mean anomaly + xni: f32 + nodem: f32 // right ascension of ascending node + dndt: f32 + nm: f32 // mean motion +} + +@binding(0) @group(0) var constants : Constants; // time since epoch (minutes) +@binding(0) @group(1) var size : u32; // time since epoch (minutes) +@binding(0) @group(2) var sat : array; +@binding(0) @group(3) var out : array; +@binding(1) @group(0) var tsince : f32; // time since epoch (minutes) + +@compute @workgroup_size(256) +fn sgp4( + @builtin(global_invocation_id) global_id : vec3 +) { + // Guard against out-of-bounds work group sizes + if (global_id.x >= size) { return; } + + let sat: ptr = sat[global_id.x]; + let out: ptr = &out[global_id.x]; + + var aycof: f32 = sat.aycof; + var xlcof: f32 = sat.xlcof; + var con41: f32 = sat.con41; + var x1mth2: f32 = sat.x1mth2; + var x7thm1: f32 = sat.x7thm1; + + var coseo1: f32 = 0.0; + var sineo1: f32 = 0.0; + var cosip: f32 = 0.0; + var sinip: f32 = 0.0; + var cosisq: f32 = 0.0; + var delm: f32 = 0.0; + var delomg: f32 = 0.0; + var eo1: f32 = 0.0; + var argpm: f32 = 0.0; + var argpp: f32 = 0.0; + var su: f32 = 0.0; + var t3: f32 = 0.0; + var t4: f32 = 0.0; + var tc: f32 = 0.0; + var tem5: f32 = 0.0; + var temp: f32 = 0.0; + var tempa: f32 = 0.0; + var tempe: f32 = 0.0; + var templ: f32 = 0.0; + var inclm: f32 = 0.0; + var mm: f32 = 0.0; + var nm: f32 = 0.0; + var nodem: f32 = 0.0; + var xincp: f32 = 0.0; + var xlm: f32 = 0.0; + var mp: f32 = 0.0; + var nodep: f32 = 0.0; + + // ------------------ set mathematical constants --------------- */ + // sgp4fix divisor for divide by zero check on inclination + // the old check used 1.0 + cos(pi-1.0e-9), but then compared it to + // 1.5 e-12, so the threshold was changed to 1.5e-12 for consistency + + let temp4: f32 = 1.5e-12; + + // ------- update for secular gravity and atmospheric drag ----- + let xmdf: f32 = sat.anomaly + (sat.mdot * tsince); + let argpdf: f32 = sat.perigee + (sat.argpdot * tsince); + let nodedf: f32 = sat.ascension + (sat.nodedot * tsince); + argpm = argpdf; + mm = xmdf; + let t2: f32 = tsince * tsince; + nodem = nodedf + (sat.nodecf * t2); + tempa = 1.0 - (sat.cc1 * tsince); + tempe = sat.drag * sat.cc4 * tsince; + templ = sat.t2cof * t2; + + if (sat.isimp != 1.0) { + delomg = sat.omgcof * tsince; + // sgp4fix use mutliply for speed instead of pow + let delmtemp: f32 = 1.0 + (sat.eta * cos(xmdf)); + delm = sat.xmcof * ((delmtemp * delmtemp * delmtemp) - sat.delmo); + temp = delomg + delm; + mm = xmdf + temp; + argpm = argpdf - temp; + t3 = t2 * tsince; + t4 = t3 * tsince; + tempa = tempa - (sat.d2 * t2) - (sat.d3 * t3) - (sat.d4 * t4); + tempe += sat.drag * sat.cc5 * (sin(mm) - sat.sinmao); + templ = templ + (sat.t3cof * t3) + (t4 * (sat.t4cof + (tsince * sat.t5cof))); + } + nm = sat.motion; + var em: f32 = sat.eccentricity; + inclm = sat.inclination; + if (sat.method == 0.0) { + tc = tsince; + + let dspaceOptions: DspaceOptions = DspaceOptions( + sat.irez, + sat.d2201, + sat.d2211, + sat.d3210, + sat.d3222, + sat.d4410, + sat.d4422, + sat.d5220, + sat.d5232, + sat.d5421, + sat.d5433, + sat.dedt, + sat.del1, + sat.del2, + sat.del3, + sat.didt, + sat.dmdt, + sat.dnodt, + sat.domdt, + sat.perigee, // argpo + sat.argpdot, + tc, + sat.gsto, + sat.xfact, + sat.xlamo, + sat.motion, // no + sat.atime, + em, + argpm, + inclm, + sat.xli, + mm, + sat.xni, + nodem, + nm + ); + + let dspaceResult: DspaceOutput = dspace(dspaceOptions); + em = dspaceResult.em; + argpm = dspaceResult.argpm; + inclm = dspaceResult.inclm; + // xli = dspaceResult.xli; + mm = dspaceResult.mm; + // xni = dspaceResult.xni; + nodem = dspaceResult.nodem; + nm = dspaceResult.nm; + } + + if (nm <= 0.0) { + // sgp4fix add return + (*out).error = 2.0; + return; + } + + let am: f32 = pow((constants.xke / nm), constants.x2o3) * tempa * tempa; + nm = constants.xke / pow(am, 1.5); + em -= tempe; + + // fix tolerance for error recognition + // sgp4fix am is fixed from the previous nm check + if (em >= 1.0 || em < -0.001) { // || (am < 0.95) + // sgp4fix to return if there is an error in eccentricity + (*out).error = 1.0; + return; + } + + // sgp4fix fix tolerance to avoid a divide by zero + if (em < 1.0e-6) { em = 1.0e-6; } + mm += sat.motion * templ; + xlm = mm + argpm + nodem; + + nodem %= constants.twoPi; + argpm %= constants.twoPi; + xlm %= constants.twoPi; + mm = (xlm - argpm - nodem) % constants.twoPi; + + // ----------------- compute extra mean quantities ------------- + let sinim: f32 = sin(inclm); + let cosim: f32 = cos(inclm); + + // -------------------- add lunar-solar periodics -------------- + var ep: f32 = em; + xincp = inclm; + argpp = argpm; + nodep = nodem; + mp = mm; + sinip = sinim; + cosip = cosim; + if (sat.method == 0.0) { + let dpperParameters: DpperOptions = DpperOptions( + 0.0, // init + ep, + xincp, // inclp + nodep, + argpp, + mp, + sat.opsmode + ); + let dpperResult: DpperOutput = dpper(sat, dpperParameters); + ep = dpperResult.ep; + nodep = dpperResult.nodep; + argpp = dpperResult.argpp; + mp = dpperResult.mp; + + xincp = dpperResult.inclp; + + if (xincp < 0.0) { + xincp = -xincp; + nodep += constants.pi; + argpp -= constants.pi; + } + if (ep < 0.0 || ep > 1.0) { + (*out).error = 3.0; + } + } + + // -------------------- long period periodics ------------------ + if (sat.method == 0.0) { + sinip = sin(xincp); + cosip = cos(xincp); + aycof = -0.5 * constants.j3oj2 * sinip; + + // sgp4fix for divide by zero for xincp = 180 deg + if (abs(cosip + 1.0) > 1.5e-12) { + xlcof = (-0.25 * constants.j3oj2 * sinip * (3.0 + (5.0 * cosip))) / (1.0 + cosip); + } else { + xlcof = (-0.25 * constants.j3oj2 * sinip * (3.0 + (5.0 * cosip))) / temp4; + } + } + + let axnl: f32 = ep * cos(argpp); + temp = 1.0 / (am * (1.0 - (ep * ep))); + let aynl: f32 = (ep * sin(argpp)) + (temp * aycof); + let xl: f32 = mp + argpp + nodep + (temp * xlcof * axnl); + + // --------------------- solve kepler's equation --------------- + let u: f32 = (xl - nodep) % constants.twoPi; + eo1 = u; + tem5 = 9999.9; + var ktr: i32 = 1; + + // sgp4fix for kepler iteration + // the following iteration needs better limits on corrections + while (abs(tem5) >= 1.0e-12 && ktr <= 10) { + sineo1 = sin(eo1); + coseo1 = cos(eo1); + tem5 = 1.0 - (coseo1 * axnl) - (sineo1 * aynl); + tem5 = (((u - (aynl * coseo1)) + (axnl * sineo1)) - eo1) / tem5; + if (abs(tem5) >= 0.95) { + if (tem5 > 0.0) { + tem5 = 0.95; + } else { + tem5 = -0.95; + } + } + eo1 += tem5; + ktr += 1; + } + + // ------------- short period preliminary quantities ----------- + let ecose: f32 = (axnl * coseo1) + (aynl * sineo1); + let esine: f32 = (axnl * sineo1) - (aynl * coseo1); + let el2: f32 = (axnl * axnl) + (aynl * aynl); + let pl: f32 = am * (1.0 - el2); + if (pl < 0.0) { + (*out).error = 4.0; + return; + } + + let rl: f32 = am * (1.0 - ecose); + let rdotl: f32 = (sqrt(am) * esine) / rl; + let rvdotl: f32 = sqrt(pl) / rl; + let betal: f32 = sqrt(1.0 - el2); + temp = esine / (1.0 + betal); + let sinu: f32 = (am / rl) * (sineo1 - aynl - (axnl * temp)); + let cosu: f32 = (am / rl) * ((coseo1 - axnl) + (aynl * temp)); + su = atan2(sinu, cosu); + let sin2u: f32 = (cosu + cosu) * sinu; + let cos2u: f32 = 1.0 - (2.0 * sinu * sinu); + temp = 1.0 / pl; + let temp1: f32 = 0.5 * constants.j2 * temp; + let temp2: f32 = temp1 * temp; + + // -------------- update for short period periodics ------------ + if (sat.method == 0.0) { + cosisq = cosip * cosip; + con41 = (3.0 * cosisq) - 1.0; + x1mth2 = 1.0 - cosisq; + x7thm1 = (7.0 * cosisq) - 1.0; + } + + let mrt: f32 = (rl * (1.0 - (1.5 * temp2 * betal * sat.con41))) + + (0.5 * temp1 * sat.x1mth2 * cos2u); + + // sgp4fix for decaying satellites + if (mrt < 1.0) { + (*out).error = 6.0; + return; + } + + su -= 0.25 * temp2 * x7thm1 * sin2u; + let xnode: f32 = nodep + (1.5 * temp2 * cosip * sin2u); + let xinc: f32 = xincp + (1.5 * temp2 * cosip * sinip * cos2u); + let mvt: f32 = rdotl - ((nm * temp1 * x1mth2 * sin2u) / constants.xke); + let rvdot: f32 = rvdotl + ((nm * temp1 * ((x1mth2 * cos2u) + (1.5 * con41))) / constants.xke); + + // --------------------- orientation vectors ------------------- + let sinsu: f32 = sin(su); + let cossu: f32 = cos(su); + let snod: f32 = sin(xnode); + let cnod: f32 = cos(xnode); + let sini: f32 = sin(xinc); + let cosi: f32 = cos(xinc); + let xmx: f32 = -snod * cosi; + let xmy: f32 = cnod * cosi; + let ux: f32 = (xmx * sinsu) + (cnod * cossu); + let uy: f32 = (xmy * sinsu) + (snod * cossu); + let uz: f32 = sini * sinsu; + let vx: f32 = (xmx * cossu) - (cnod * sinsu); + let vy: f32 = (xmy * cossu) - (snod * sinsu); + let vz: f32 = sini * cossu; + + // --------- position and velocity (in km and km/sec) ---------- + (*out).position = vec3( + (mrt * ux) * constants.earthRadius, + (mrt * uy) * constants.earthRadius, + (mrt * uz) * constants.earthRadius + ); + (*out).velocity = vec3( + ((mvt * ux) + (rvdot * vx)) * constants.vkmpersec, + ((mvt * uy) + (rvdot * vy)) * constants.vkmpersec, + ((mvt * uz) + (rvdot * vz)) * constants.vkmpersec + ); +} + +// +// +// procedure dpper +// +// this procedure provides deep space long period periodic contributions +// to the mean elements. by design, these periodics are zero at epoch. +// this used to be dscom which included initialization, but it's really a +// recurring function. +// +// author : david vallado 719-573-2600 28 jun 2005 +// +// inputs : +// e3 - +// ee2 - +// peo - +// pgho - +// pho - +// pinco - +// plo - +// se2 , se3 , sgh2, sgh3, sgh4, sh2, sh3, si2, si3, sl2, sl3, sl4 - +// t - +// xh2, xh3, xi2, xi3, xl2, xl3, xl4 - +// zmol - +// zmos - +// ep - eccentricity 0.0 - 1.0 +// inclo - inclination - needed for lyddane modification +// nodep - right ascension of ascending node +// argpp - argument of perigee +// mp - mean anomaly +// +// outputs : +// ep - eccentricity 0.0 - 1.0 +// inclp - inclination +// nodep - right ascension of ascending node +// argpp - argument of perigee +// mp - mean anomaly +// +// locals : +// alfdp - +// betdp - +// cosip , sinip , cosop , sinop , +// dalf - +// dbet - +// dls - +// f2, f3 - +// pe - +// pgh - +// ph - +// pinc - +// pl - +// sel , ses , sghl , sghs , shl , shs , sil , sinzf , sis , +// sll , sls +// xls - +// xnoh - +// zf - +// zm - +// +// coupling : +// none. +// +// references : +// hoots, roehrich, norad spacetrack report #3 1980 +// hoots, norad spacetrack report #6 1986 +// hoots, schumacher and glover 2004 +// vallado, crawford, hujsak, kelso 2006 +//---------------------------------------------------------------------------- +fn dpper(sat: Satellite, options: DpperOptions) -> DpperOutput { + var ep: f32 = options.ep; + var inclp: f32 = options.inclp; + var nodep: f32 = options.nodep; + var argpp: f32 = options.argpp; + var mp: f32 = options.mp; + + // Copy satellite attributes into local variables for convenience + // and symmetry in writing formulae. + + var alfdp: f32; + var betdp: f32; + var cosip: f32; + var sinip: f32; + var cosop: f32; + var sinop: f32; + var dalf: f32; + var dbet: f32; + var dls: f32; + var f2: f32; + var f3: f32; + var pe: f32; + var pgh: f32; + var ph: f32; + var pinc: f32; + var pl: f32; + var sinzf: f32; + var xls: f32; + var xnoh: f32; + var zf: f32; + var zm: f32; + + // ---------------------- constants ----------------------------- + let zns: f32 = 1.19459e-5; + let zes: f32 = 0.01675; + let znl: f32 = 1.5835218e-4; + let zel: f32 = 0.05490; + + // --------------- calculate time varying periodics ----------- + zm = sat.zmos + (zns * tsince); + + // be sure that the initial call has time set to zero + if (options.init) { + zm = sat.zmos; + } + zf = zm + (2.0 * zes * sin(zm)); + sinzf = sin(zf); + f2 = (0.5 * sinzf * sinzf) - 0.25; + f3 = -0.5 * sinzf * cos(zf); + + let ses: f32 = (sat.se2 * f2) + (sat.se3 * f3); + let sis: f32 = (sat.si2 * f2) + (sat.si3 * f3); + let sls: f32 = (sat.sl2 * f2) + (sat.sl3 * f3) + (sat.sl4 * sinzf); + let sghs: f32 = (sat.sgh2 * f2) + (sat.sgh3 * f3) + (sat.sgh4 * sinzf); + let shs: f32 = (sat.sh2 * f2) + (sat.sh3 * f3); + + zm = sat.zmol + (znl * tsince); + if (options.init) { zm = sat.zmol; } + + zf = zm + (2.0 * zel * sin(zm)); + sinzf = sin(zf); + f2 = (0.5 * sinzf * sinzf) - 0.25; + f3 = -0.5 * sinzf * cos(zf); + + let sel: f32 = (sat.ee2 * f2) + (sat.e3 * f3); + let sil: f32 = (sat.xi2 * f2) + (sat.xi3 * f3); + let sll: f32 = (sat.xl2 * f2) + (sat.xl3 * f3) + (sat.xl4 * sinzf); + let sghl: f32 = (sat.xgh2 * f2) + (sat.xgh3 * f3) + (sat.xgh4 * sinzf); + let shll: f32 = (sat.xh2 * f2) + (sat.xh3 * f3); + + pe = ses + sel; + pinc = sis + sil; + pl = sls + sll; + pgh = sghs + sghl; + ph = shs + shll; + + if (!options.init) { + pe -= sat.peo; + pinc -= sat.pinco; + pl -= sat.plo; + pgh -= sat.pgho; + ph -= sat.pho; + inclp += pinc; + ep += pe; + sinip = sin(inclp); + cosip = cos(inclp); + + // * ----------------- apply periodics directly ------------ */ + // sgp4fix for lyddane choice + // strn3 used original inclination - this is technically feasible + // gsfc used perturbed inclination - also technically feasible + // probably best to readjust the 0.2 limit value and limit discontinuity + // 0.2 rad = 11.45916 deg + // use next line for original strn3 approach and original inclination + // if (inclo >= 0.2) + // use next line for gsfc version and perturbed inclination + if (inclp >= 0.2) { + ph /= sinip; + pgh -= cosip * ph; + argpp += pgh; + nodep += ph; + mp += pl; + } else { + // ---- apply periodics with lyddane modification ---- + sinop = sin(nodep); + cosop = cos(nodep); + alfdp = sinip * sinop; + betdp = sinip * cosop; + dalf = (ph * cosop) + (pinc * cosip * sinop); + dbet = (-ph * sinop) + (pinc * cosip * cosop); + alfdp += dalf; + betdp += dbet; + nodep %= constants.twoPi; + + // sgp4fix for afspc written intrinsic functions + // nodep used without a trigonometric function ahead + if (nodep < 0.0 && sat.opsmode == 0.0) { + nodep += constants.twoPi; + } + xls = mp + argpp + (cosip * nodep); + dls = (pl + pgh) - (pinc * nodep * sinip); + xls += dls; + xnoh = nodep; + nodep = atan2(alfdp, betdp); + + // sgp4fix for afspc written intrinsic functions + // nodep used without a trigonometric function ahead + if (nodep < 0.0 && sat.opsmode == 0.0) { + nodep += constants.twoPi; + } + if (abs(xnoh - nodep) > constants.pi) { + if (nodep < xnoh) { + nodep += constants.twoPi; + } else { + nodep -= constants.twoPi; + } + } + mp += pl; + argpp = xls - mp - (cosip * nodep); + } + } + + return DpperOutput( + ep, + inclp, + nodep, + argpp, + mp + ); +} + +// ----------------------------------------------------------------------------- +// +// procedure dspace +// +// this procedure provides deep space contributions to mean elements for +// perturbing third body. these effects have been averaged over one +// revolution of the sun and moon. for earth resonance effects, the +// effects have been averaged over no revolutions of the satellite. +// (mean motion) +// +// author : david vallado 719-573-2600 28 jun 2005 +// +// inputs : +// d2201, d2211, d3210, d3222, d4410, d4422, d5220, d5232, d5421, d5433 - +// dedt - +// del1, del2, del3 - +// didt - +// dmdt - +// dnodt - +// domdt - +// irez - flag for resonance 0-none, 1-one day, 2-half day +// argpo - argument of perigee +// argpdot - argument of perigee dot (rate) +// t - time +// tc - +// gsto - gst +// xfact - +// xlamo - +// no - mean motion +// atime - +// em - eccentricity +// ft - +// argpm - argument of perigee +// inclm - inclination +// xli - +// mm - mean anomaly +// xni - mean motion +// nodem - right ascension of ascending node +// +// outputs : +// atime - +// em - eccentricity +// argpm - argument of perigee +// inclm - inclination +// xli - +// mm - mean anomaly +// xni - +// nodem - right ascension of ascending node +// dndt - +// nm - mean motion +// +// locals : +// delt - +// ft - +// theta - +// x2li - +// x2omi - +// xl - +// xldot - +// xnddt - +// xndt - +// xomi - +// +// coupling : +// none - +// +// references : +// hoots, roehrich, norad spacetrack report #3 1980 +// hoots, norad spacetrack report #6 1986 +// hoots, schumacher and glover 2004 +// vallado, crawford, hujsak, kelso 2006 +//---------------------------------------------------------------------------- +fn dspace(options: DspaceOptions) -> DspaceOutput { + var atime: f32 = options.atime; + var em: f32 = options.em; + var argpm: f32 = options.argpm; + var inclm: f32 = options.inclm; + var xli: f32 = options.xli; + var mm: f32 = options.mm; + var xni: f32 = options.xni; + var nodem: f32 = options.nodem; + var nm: f32 = options.nm; + + let fasx2: f32 = 0.13130908; + let fasx4: f32 = 2.8843198; + let fasx6: f32 = 0.37448087; + let g22: f32 = 5.7686396; + let g32: f32 = 0.95240898; + let g44: f32 = 1.8014998; + let g52: f32 = 1.0508330; + let g54: f32 = 4.4108898; + let rptim: f32 = 4.37526908801129966e-3; // equates to 7.29211514668855e-5 rad/sec + let stepp: f32 = 720.0; + let stepn: f32 = -720.0; + let step2: f32 = 259200.0; + + var delt: f32; + var x2li: f32; + var x2omi: f32; + var xl: f32; + var xldot: f32 = 0.0; + var xnddt: f32 = 0.0; + var xndt: f32 = 0.0; + var xomi: f32; + var dndt: f32 = 0.0; + var ft: f32 = 0.0; + + // ----------- calculate deep space resonance effects ----------- + let theta: f32 = (options.gsto + (options.tc * rptim)) % constants.twoPi; + em += options.dedt * tsince; + + inclm += options.didt * tsince; + argpm += options.domdt * tsince; + nodem += options.dnodt * tsince; + mm += options.dmdt * tsince; + + // - update resonances : numerical (euler-maclaurin) integration - // + // ------------------------- epoch restart ---------------------- // + // sgp4fix for propagator problems + // the following integration works for negative time steps and periods + // the specific changes are unknown because the original code was so convoluted + + // sgp4fix take out atime = 0.0 and fix for faster operation + + if (options.irez != 0.0) { + // sgp4fix streamline check + if ( + atime == 0.0 || + tsince * atime <= 0.0 || + abs(tsince) < abs(atime) + ) { + atime = 0.0; + xni = options.no; + xli = options.xlamo; + } + + // sgp4fix move check outside loop + if (tsince > 0.0) { + delt = stepp; + } else { + delt = stepn; + } + + var iretn: f32 = 381.0; // added for do loop + while (iretn == 381.0) { + // ------------------- dot terms calculated ------------- + // ----------- near - synchronous resonance terms ------- + if (options.irez != 2.0) { + xndt = (options.del1 * sin(xli - fasx2)) + + (options.del2 * sin(2.0 * (xli - fasx4))) + + (options.del3 * sin(3.0 * (xli - fasx6))); + xldot = xni + options.xfact; + xnddt = (options.del1 * cos(xli - fasx2)) + + (2.0 * options.del2 * cos(2.0 * (xli - fasx4))) + + (3.0 * options.del3 * cos(3.0 * (xli - fasx6))); + xnddt *= xldot; + } else { + // --------- near - half-day resonance terms -------- + xomi = options.argpo + (options.argpdot * atime); + x2omi = xomi + xomi; + x2li = xli + xli; + xndt = (options.d2201 * sin((x2omi + xli) - g22)) + + (options.d2211 * sin(xli - g22)) + + (options.d3210 * sin((xomi + xli) - g32)) + + (options.d3222 * sin((-xomi + xli) - g32)) + + (options.d4410 * sin((x2omi + x2li) - g44)) + + (options.d4422 * sin(x2li - g44)) + + (options.d5220 * sin((xomi + xli) - g52)) + + (options.d5232 * sin((-xomi + xli) - g52)) + + (options.d5421 * sin((xomi + x2li) - g54)) + + (options.d5433 * sin((-xomi + x2li) - g54)); + xldot = xni + options.xfact; + xnddt = (options.d2201 * cos((x2omi + xli) - g22)) + + (options.d2211 * cos(xli - g22)) + + (options.d3210 * cos((xomi + xli) - g32)) + + (options.d3222 * cos((-xomi + xli) - g32)) + + (options.d5220 * cos((xomi + xli) - g52)) + + (options.d5232 * cos((-xomi + xli) - g52)) + + 2.0 * ((options.d4410 * cos((x2omi + x2li) - g44)) + + (options.d4422 * cos(x2li - g44)) + + (options.d5421 * cos((xomi + x2li) - g54)) + + (options.d5433 * cos((-xomi + x2li) - g54))); + xnddt *= xldot; + } + + // ----------------------- integrator ------------------- + // sgp4fix move end checks to end of routine + if (abs(tsince - atime) >= stepp) { + iretn = 381.0; + } else { + ft = tsince - atime; + iretn = 0.0; + } + + if (iretn == 381.0) { + xli += (xldot * delt) + (xndt * step2); + xni += (xndt * delt) + (xnddt * step2); + atime += delt; + } + } + + nm = xni + (xndt * ft) + (xnddt * ft * ft * 0.5); + xl = xli + (xldot * ft) + (xndt * ft * ft * 0.5); + if (options.irez != 1.0) { + mm = (xl - (2.0 * nodem)) + (2.0 * theta); + dndt = nm - options.no; + } else { + mm = (xl - nodem - argpm) + theta; + dndt = nm - options.no; + } + nm = options.no + dndt; + } + + return DspaceOutput( + atime, + em, + argpm, + inclm, + xli, + mm, + xni, + nodem, + dndt, + nm + ); +} diff --git a/src/space/propagation/dpper.ts b/src/space/propagation/dpper.ts new file mode 100644 index 00000000..11d336f6 --- /dev/null +++ b/src/space/propagation/dpper.ts @@ -0,0 +1,284 @@ +import { Satellite } from '../sat'; +import { pi, twoPi } from '../util/constants'; + +/** + * + */ +export interface Options { + init: boolean; + ep: number; + inclp: number; + nodep: number; + argpp: number; + mp: number; +} + +/** + * + */ +export interface Output { + ep: number; + inclp: number; + nodep: number; + argpp: number; + mp: number; +} + +/* ----------------------------------------------------------------------------- + * + * procedure dpper + * + * this procedure provides deep space long period periodic contributions + * to the mean elements. by design, these periodics are zero at epoch. + * this used to be dscom which included initialization, but it's really a + * recurring function. + * + * author : david vallado 719-573-2600 28 jun 2005 + * + * inputs : + * e3 - + * ee2 - + * peo - + * pgho - + * pho - + * pinco - + * plo - + * se2 , se3 , sgh2, sgh3, sgh4, sh2, sh3, si2, si3, sl2, sl3, sl4 - + * t - + * xh2, xh3, xi2, xi3, xl2, xl3, xl4 - + * zmol - + * zmos - + * ep - eccentricity 0.0 - 1.0 + * inclo - inclination - needed for lyddane modification + * nodep - right ascension of ascending node + * argpp - argument of perigee + * mp - mean anomaly + * + * outputs : + * ep - eccentricity 0.0 - 1.0 + * inclp - inclination + * nodep - right ascension of ascending node + * argpp - argument of perigee + * mp - mean anomaly + * + * locals : + * alfdp - + * betdp - + * cosip , sinip , cosop , sinop , + * dalf - + * dbet - + * dls - + * f2, f3 - + * pe - + * pgh - + * ph - + * pinc - + * pl - + * sel , ses , sghl , sghs , shl , shs , sil , sinzf , sis , + * sll , sls + * xls - + * xnoh - + * zf - + * zm - + * + * coupling : + * none. + * + * references : + * hoots, roehrich, norad spacetrack report #3 1980 + * hoots, norad spacetrack report #6 1986 + * hoots, schumacher and glover 2004 + * vallado, crawford, hujsak, kelso 2006 + ---------------------------------------------------------------------------- */ +/** + * @param sat + * @param options + * @param tsince + */ +export default function dpper( + sat: Satellite, + options: Options, + tsince: number, // defaults to 0 (sgp4init doesn't set a time) +): Output { + const { + e3, + ee2, + peo, + pgho, + pho, + pinco, + plo, + se2, + se3, + sgh2, + sgh3, + sgh4, + sh2, + sh3, + si2, + si3, + sl2, + sl3, + sl4, + xgh2, + xgh3, + xgh4, + xh2, + xh3, + xi2, + xi3, + xl2, + xl3, + xl4, + zmol, + zmos, + opsmode, + } = sat; + + const { init } = options; + + let { ep, inclp, nodep, argpp, mp } = options; + + // Copy satellite attributes into local variables for convenience + // and symmetry in writing formulae. + + let alfdp: number; + let betdp: number; + let cosip: number; + let sinip: number; + let cosop: number; + let sinop: number; + let dalf: number; + let dbet: number; + let dls: number; + let f2: number; + let f3: number; + let pe: number; + let pgh: number; + let ph: number; + let pinc: number; + let pl: number; + let sinzf: number; + let xls: number; + let xnoh: number; + let zf: number; + let zm: number; + + // ---------------------- constants ----------------------------- + const zns = 1.19459e-5; + const zes = 0.01675; + const znl = 1.5835218e-4; + const zel = 0.0549; + + // --------------- calculate time varying periodics ----------- + zm = zmos + zns * tsince; + + // be sure that the initial call has time set to zero + if (init) zm = zmos; + zf = zm + 2.0 * zes * Math.sin(zm); + sinzf = Math.sin(zf); + f2 = 0.5 * sinzf * sinzf - 0.25; + f3 = -0.5 * sinzf * Math.cos(zf); + + const ses = se2 * f2 + se3 * f3; + const sis = si2 * f2 + si3 * f3; + const sls = sl2 * f2 + sl3 * f3 + sl4 * sinzf; + const sghs = sgh2 * f2 + sgh3 * f3 + sgh4 * sinzf; + const shs = sh2 * f2 + sh3 * f3; + + zm = zmol + znl * tsince; + if (init) { + zm = zmol; + } + + zf = zm + 2.0 * zel * Math.sin(zm); + sinzf = Math.sin(zf); + f2 = 0.5 * sinzf * sinzf - 0.25; + f3 = -0.5 * sinzf * Math.cos(zf); + + const sel = ee2 * f2 + e3 * f3; + const sil = xi2 * f2 + xi3 * f3; + const sll = xl2 * f2 + xl3 * f3 + xl4 * sinzf; + const sghl = xgh2 * f2 + xgh3 * f3 + xgh4 * sinzf; + const shll = xh2 * f2 + xh3 * f3; + + pe = ses + sel; + pinc = sis + sil; + pl = sls + sll; + pgh = sghs + sghl; + ph = shs + shll; + + if (!init) { + pe -= peo; + pinc -= pinco; + pl -= plo; + pgh -= pgho; + ph -= pho; + inclp += pinc; + ep += pe; + sinip = Math.sin(inclp); + cosip = Math.cos(inclp); + + /* ----------------- apply periodics directly ------------ */ + // sgp4fix for lyddane choice + // strn3 used original inclination - this is technically feasible + // gsfc used perturbed inclination - also technically feasible + // probably best to readjust the 0.2 limit value and limit discontinuity + // 0.2 rad = 11.45916 deg + // use next line for original strn3 approach and original inclination + // if (inclo >= 0.2) + // use next line for gsfc version and perturbed inclination + if (inclp >= 0.2) { + ph /= sinip; + pgh -= cosip * ph; + argpp += pgh; + nodep += ph; + mp += pl; + } else { + // ---- apply periodics with lyddane modification ---- + sinop = Math.sin(nodep); + cosop = Math.cos(nodep); + alfdp = sinip * sinop; + betdp = sinip * cosop; + dalf = ph * cosop + pinc * cosip * sinop; + dbet = -ph * sinop + pinc * cosip * cosop; + alfdp += dalf; + betdp += dbet; + nodep %= twoPi; + + // sgp4fix for afspc written intrinsic functions + // nodep used without a trigonometric function ahead + if (nodep < 0.0 && opsmode === 'a') { + nodep += twoPi; + } + xls = mp + argpp + cosip * nodep; + dls = pl + pgh - pinc * nodep * sinip; + xls += dls; + xnoh = nodep; + nodep = Math.atan2(alfdp, betdp); + + // sgp4fix for afspc written intrinsic functions + // nodep used without a trigonometric function ahead + if (nodep < 0.0 && opsmode === 'a') { + nodep += twoPi; + } + if (Math.abs(xnoh - nodep) > pi) { + if (nodep < xnoh) { + nodep += twoPi; + } else { + nodep -= twoPi; + } + } + mp += pl; + argpp = xls - mp - cosip * nodep; + } + } + + return { + ep, + inclp, + nodep, + argpp, + mp, + }; +} diff --git a/src/space/propagation/dscom.ts b/src/space/propagation/dscom.ts new file mode 100644 index 00000000..5504d665 --- /dev/null +++ b/src/space/propagation/dscom.ts @@ -0,0 +1,527 @@ +import { twoPi } from '../util/constants'; + +/** + * + */ +export interface Options { + epoch: number; + ep: number; + argpp: number; + tc: number; + inclp: number; + nodep: number; + np: number; +} + +/** + * + */ +export interface Output { + snodm: number; + cnodm: number; + sinim: number; + cosim: number; + sinomm: number; + + cosomm: number; + day: number; + e3: number; + ee2: number; + em: number; + + emsq: number; + gam: number; + peo: number; + pgho: number; + pho: number; + + pinco: number; + plo: number; + rtemsq: number; + se2: number; + se3: number; + + sgh2: number; + sgh3: number; + sgh4: number; + sh2: number; + sh3: number; + + si2: number; + si3: number; + sl2: number; + sl3: number; + sl4: number; + + s1: number; + s2: number; + s3: number; + s4: number; + s5: number; + + s6: number; + s7: number; + ss1: number; + ss2: number; + ss3: number; + + ss4: number; + ss5: number; + ss6: number; + ss7: number; + sz1: number; + + sz2: number; + sz3: number; + sz11: number; + sz12: number; + sz13: number; + + sz21: number; + sz22: number; + sz23: number; + sz31: number; + sz32: number; + + sz33: number; + xgh2: number; + xgh3: number; + xgh4: number; + xh2: number; + + xh3: number; + xi2: number; + xi3: number; + xl2: number; + xl3: number; + + xl4: number; + nm: number; + z1: number; + z2: number; + z3: number; + + z11: number; + z12: number; + z13: number; + z21: number; + z22: number; + + z23: number; + z31: number; + z32: number; + z33: number; + zmol: number; + + zmos: number; +} + +/* ----------------------------------------------------------------------------- + * + * procedure dscom + * + * this procedure provides deep space common items used by both the secular + * and periodics subroutines. input is provided as shown. this routine + * used to be called dpper, but the functions inside weren't well organized. + * + * author : david vallado 719-573-2600 28 jun 2005 + * + * inputs : + * epoch - + * ep - eccentricity + * argpp - argument of perigee + * tc - + * inclp - inclination + * nodep - right ascension of ascending node + * np - mean motion + * + * outputs : + * sinim , cosim , sinomm , cosomm , snodm , cnodm + * day - + * e3 - + * ee2 - + * em - eccentricity + * emsq - eccentricity squared + * gam - + * peo - + * pgho - + * pho - + * pinco - + * plo - + * rtemsq - + * se2, se3 - + * sgh2, sgh3, sgh4 - + * sh2, sh3, si2, si3, sl2, sl3, sl4 - + * s1, s2, s3, s4, s5, s6, s7 - + * ss1, ss2, ss3, ss4, ss5, ss6, ss7, sz1, sz2, sz3 - + * sz11, sz12, sz13, sz21, sz22, sz23, sz31, sz32, sz33 - + * xgh2, xgh3, xgh4, xh2, xh3, xi2, xi3, xl2, xl3, xl4 - + * nm - mean motion + * z1, z2, z3, z11, z12, z13, z21, z22, z23, z31, z32, z33 - + * zmol - + * zmos - + * + * locals : + * a1, a2, a3, a4, a5, a6, a7, a8, a9, a10 - + * betasq - + * cc - + * ctem, stem - + * x1, x2, x3, x4, x5, x6, x7, x8 - + * xnodce - + * xnoi - + * zcosg , zsing , zcosgl , zsingl , zcosh , zsinh , zcoshl , zsinhl , + * zcosi , zsini , zcosil , zsinil , + * zx - + * zy - + * + * coupling : + * none. + * + * references : + * hoots, roehrich, norad spacetrack report #3 1980 + * hoots, norad spacetrack report #6 1986 + * hoots, schumacher and glover 2004 + * vallado, crawford, hujsak, kelso 2006 + ---------------------------------------------------------------------------- */ +/** + * @param options + */ +export default function dscom(options: Options): Output { + const { epoch, ep, argpp, tc, inclp, nodep, np } = options; + + let a1: number; + let a2: number; + let a3: number; + let a4: number; + let a5: number; + let a6: number; + let a7: number; + let a8: number; + let a9: number; + let a10: number; + let cc: number; + let x1: number; + let x2: number; + let x3: number; + let x4: number; + let x5: number; + let x6: number; + let x7: number; + let x8: number; + let zcosg: number; + let zsing: number; + let zcosh: number; + let zsinh: number; + let zcosi: number; + let zsini: number; + + let ss1 = 0; + let ss2 = 0; + let ss3 = 0; + let ss4 = 0; + let ss5 = 0; + let ss6 = 0; + let ss7 = 0; + let sz1 = 0; + let sz2 = 0; + let sz3 = 0; + let sz11 = 0; + let sz12 = 0; + let sz13 = 0; + let sz21 = 0; + let sz22 = 0; + let sz23 = 0; + let sz31 = 0; + let sz32 = 0; + let sz33 = 0; + let s1 = 0; + let s2 = 0; + let s3 = 0; + let s4 = 0; + let s5 = 0; + let s6 = 0; + let s7 = 0; + let z1 = 0; + let z2 = 0; + let z3 = 0; + let z11 = 0; + let z12 = 0; + let z13 = 0; + let z21 = 0; + let z22 = 0; + let z23 = 0; + let z31 = 0; + let z32 = 0; + let z33 = 0; + + // -------------------------- constants ------------------------- + const zes = 0.01675; + const zel = 0.0549; + const c1ss = 2.9864797e-6; + const c1l = 4.7968065e-7; + const zsinis = 0.39785416; + const zcosis = 0.91744867; + const zcosgs = 0.1945905; + const zsings = -0.98088458; + + // --------------------- local variables ------------------------ + const nm = np; + const em = ep; + const snodm = Math.sin(nodep); + const cnodm = Math.cos(nodep); + const sinomm = Math.sin(argpp); + const cosomm = Math.cos(argpp); + const sinim = Math.sin(inclp); + const cosim = Math.cos(inclp); + const emsq = em * em; + const betasq = 1.0 - emsq; + const rtemsq = Math.sqrt(betasq); + + // ----------------- initialize lunar solar terms --------------- + const peo = 0.0; + const pinco = 0.0; + const plo = 0.0; + const pgho = 0.0; + const pho = 0.0; + const day = epoch + 18261.5 + tc / 1440.0; + const xnodce = (4.523602 - 9.2422029e-4 * day) % twoPi; + const stem = Math.sin(xnodce); + const ctem = Math.cos(xnodce); + const zcosil = 0.91375164 - 0.03568096 * ctem; + const zsinil = Math.sqrt(1.0 - zcosil * zcosil); + const zsinhl = (0.089683511 * stem) / zsinil; + const zcoshl = Math.sqrt(1.0 - zsinhl * zsinhl); + const gam = 5.8351514 + 0.001944368 * day; + let zx = (0.39785416 * stem) / zsinil; + const zy = zcoshl * ctem + 0.91744867 * zsinhl * stem; + zx = Math.atan2(zx, zy); + zx += gam - xnodce; + const zcosgl = Math.cos(zx); + const zsingl = Math.sin(zx); + + // ------------------------- do solar terms --------------------- + zcosg = zcosgs; + zsing = zsings; + zcosi = zcosis; + zsini = zsinis; + zcosh = cnodm; + zsinh = snodm; + cc = c1ss; + const xnoi = 1.0 / nm; + + let lsflg = 0; + while (lsflg < 2) { + lsflg += 1; + a1 = zcosg * zcosh + zsing * zcosi * zsinh; + a3 = -zsing * zcosh + zcosg * zcosi * zsinh; + a7 = -zcosg * zsinh + zsing * zcosi * zcosh; + a8 = zsing * zsini; + a9 = zsing * zsinh + zcosg * zcosi * zcosh; + a10 = zcosg * zsini; + a2 = cosim * a7 + sinim * a8; + a4 = cosim * a9 + sinim * a10; + a5 = -sinim * a7 + cosim * a8; + a6 = -sinim * a9 + cosim * a10; + + x1 = a1 * cosomm + a2 * sinomm; + x2 = a3 * cosomm + a4 * sinomm; + x3 = -a1 * sinomm + a2 * cosomm; + x4 = -a3 * sinomm + a4 * cosomm; + x5 = a5 * sinomm; + x6 = a6 * sinomm; + x7 = a5 * cosomm; + x8 = a6 * cosomm; + + z31 = 12.0 * x1 * x1 - 3.0 * x3 * x3; + z32 = 24.0 * x1 * x2 - 6.0 * x3 * x4; + z33 = 12.0 * x2 * x2 - 3.0 * x4 * x4; + + z1 = 3.0 * (a1 * a1 + a2 * a2) + z31 * emsq; + z2 = 6.0 * (a1 * a3 + a2 * a4) + z32 * emsq; + z3 = 3.0 * (a3 * a3 + a4 * a4) + z33 * emsq; + + z11 = -6.0 * a1 * a5 + emsq * (-24.0 * x1 * x7 - 6.0 * x3 * x5); + z12 = + -6.0 * (a1 * a6 + a3 * a5) + + emsq * (-24.0 * (x2 * x7 + x1 * x8) + -6.0 * (x3 * x6 + x4 * x5)); + + z13 = -6.0 * a3 * a6 + emsq * (-24.0 * x2 * x8 - 6.0 * x4 * x6); + + z21 = 6.0 * a2 * a5 + emsq * (24.0 * x1 * x5 - 6.0 * x3 * x7); + z22 = + 6.0 * (a4 * a5 + a2 * a6) + emsq * (24.0 * (x2 * x5 + x1 * x6) - 6.0 * (x4 * x7 + x3 * x8)); + z23 = 6.0 * a4 * a6 + emsq * (24.0 * x2 * x6 - 6.0 * x4 * x8); + + z1 = z1 + z1 + betasq * z31; + z2 = z2 + z2 + betasq * z32; + z3 = z3 + z3 + betasq * z33; + s3 = cc * xnoi; + s2 = (-0.5 * s3) / rtemsq; + s4 = s3 * rtemsq; + s1 = -15.0 * em * s4; + s5 = x1 * x3 + x2 * x4; + s6 = x2 * x3 + x1 * x4; + s7 = x2 * x4 - x1 * x3; + + // ----------------------- do lunar terms ------------------- + if (lsflg === 1) { + ss1 = s1; + ss2 = s2; + ss3 = s3; + ss4 = s4; + ss5 = s5; + ss6 = s6; + ss7 = s7; + sz1 = z1; + sz2 = z2; + sz3 = z3; + sz11 = z11; + sz12 = z12; + sz13 = z13; + sz21 = z21; + sz22 = z22; + sz23 = z23; + sz31 = z31; + sz32 = z32; + sz33 = z33; + zcosg = zcosgl; + zsing = zsingl; + zcosi = zcosil; + zsini = zsinil; + zcosh = zcoshl * cnodm + zsinhl * snodm; + zsinh = snodm * zcoshl - cnodm * zsinhl; + cc = c1l; + } + } + + const zmol = (4.7199672 + (0.2299715 * day - gam)) % twoPi; + const zmos = (6.2565837 + 0.017201977 * day) % twoPi; + + // ------------------------ do solar terms ---------------------- + const se2 = 2.0 * ss1 * ss6; + const se3 = 2.0 * ss1 * ss7; + const si2 = 2.0 * ss2 * sz12; + const si3 = 2.0 * ss2 * (sz13 - sz11); + const sl2 = -2.0 * ss3 * sz2; + const sl3 = -2.0 * ss3 * (sz3 - sz1); + const sl4 = -2.0 * ss3 * (-21.0 - 9.0 * emsq) * zes; + const sgh2 = 2.0 * ss4 * sz32; + const sgh3 = 2.0 * ss4 * (sz33 - sz31); + const sgh4 = -18.0 * ss4 * zes; + const sh2 = -2.0 * ss2 * sz22; + const sh3 = -2.0 * ss2 * (sz23 - sz21); + + // ------------------------ do lunar terms ---------------------- + const ee2 = 2.0 * s1 * s6; + const e3 = 2.0 * s1 * s7; + const xi2 = 2.0 * s2 * z12; + const xi3 = 2.0 * s2 * (z13 - z11); + const xl2 = -2.0 * s3 * z2; + const xl3 = -2.0 * s3 * (z3 - z1); + const xl4 = -2.0 * s3 * (-21.0 - 9.0 * emsq) * zel; + const xgh2 = 2.0 * s4 * z32; + const xgh3 = 2.0 * s4 * (z33 - z31); + const xgh4 = -18.0 * s4 * zel; + const xh2 = -2.0 * s2 * z22; + const xh3 = -2.0 * s2 * (z23 - z21); + + return { + snodm, + cnodm, + sinim, + cosim, + sinomm, + + cosomm, + day, + e3, + ee2, + em, + + emsq, + gam, + peo, + pgho, + pho, + + pinco, + plo, + rtemsq, + se2, + se3, + + sgh2, + sgh3, + sgh4, + sh2, + sh3, + + si2, + si3, + sl2, + sl3, + sl4, + + s1, + s2, + s3, + s4, + s5, + + s6, + s7, + ss1, + ss2, + ss3, + + ss4, + ss5, + ss6, + ss7, + sz1, + + sz2, + sz3, + sz11, + sz12, + sz13, + + sz21, + sz22, + sz23, + sz31, + sz32, + + sz33, + xgh2, + xgh3, + xgh4, + xh2, + + xh3, + xi2, + xi3, + xl2, + xl3, + + xl4, + nm, + z1, + z2, + z3, + + z11, + z12, + z13, + z21, + z22, + + z23, + z31, + z32, + z33, + zmol, + + zmos, + }; +} diff --git a/src/space/propagation/dsinit.ts b/src/space/propagation/dsinit.ts new file mode 100644 index 00000000..8a39a3de --- /dev/null +++ b/src/space/propagation/dsinit.ts @@ -0,0 +1,544 @@ +import { pi, twoPi, x2o3, xke } from '../util/constants'; + +/** + * + */ +export interface Options { + cosim: number; + argpo: number; + s1: number; + s2: number; + s3: number; + s4: number; + s5: number; + sinim: number; + ss1: number; + ss2: number; + ss3: number; + ss4: number; + ss5: number; + sz1: number; + sz3: number; + sz11: number; + sz13: number; + sz21: number; + sz23: number; + sz31: number; + sz33: number; + tc: number; + gsto: number; + mo: number; + mdot: number; + no: number; + nodeo: number; + nodedot: number; + xpidot: number; + z1: number; + z3: number; + z11: number; + z13: number; + z21: number; + z23: number; + z31: number; + z33: number; + ecco: number; + eccsq: number; + emsq: number; + em: number; + argpm: number; + inclm: number; + mm: number; + nm: number; + nodem: number; + irez: number; + atime: number; + d2201: number; + d2211: number; + d3210: number; + d3222: number; + d4410: number; + d4422: number; + d5220: number; + d5232: number; + d5421: number; + d5433: number; + dedt: number; + didt: number; + dmdt: number; + dnodt: number; + domdt: number; + del1: number; + del2: number; + del3: number; + xfact: number; + xlamo: number; + xli: number; + xni: number; +} + +/** + * + */ +export interface Output { + em: number; + argpm: number; + inclm: number; + mm: number; + nm: number; + nodem: number; + + irez: number; + atime: number; + + d2201: number; + d2211: number; + d3210: number; + d3222: number; + d4410: number; + + d4422: number; + d5220: number; + d5232: number; + d5421: number; + d5433: number; + + dedt: number; + didt: number; + dmdt: number; + dndt: number; + dnodt: number; + domdt: number; + + del1: number; + del2: number; + del3: number; + + xfact: number; + xlamo: number; + xli: number; + xni: number; +} + +/* ----------------------------------------------------------------------------- + * + * procedure dsinit + * + * this procedure provides deep space contributions to mean motion dot due + * to geopotential resonance with half day and one day orbits. + * + * author : david vallado 719-573-2600 28 jun 2005 + * + * inputs : + * cosim, sinim- + * emsq - eccentricity squared + * argpo - argument of perigee + * s1, s2, s3, s4, s5 - + * ss1, ss2, ss3, ss4, ss5 - + * sz1, sz3, sz11, sz13, sz21, sz23, sz31, sz33 - + * t - time + * tc - + * gsto - greenwich sidereal time rad + * mo - mean anomaly + * mdot - mean anomaly dot (rate) + * no - mean motion + * nodeo - right ascension of ascending node + * nodedot - right ascension of ascending node dot (rate) + * xpidot - + * z1, z3, z11, z13, z21, z23, z31, z33 - + * eccm - eccentricity + * argpm - argument of perigee + * inclm - inclination + * mm - mean anomaly + * xn - mean motion + * nodem - right ascension of ascending node + * + * outputs : + * em - eccentricity + * argpm - argument of perigee + * inclm - inclination + * mm - mean anomaly + * nm - mean motion + * nodem - right ascension of ascending node + * irez - flag for resonance 0-none, 1-one day, 2-half day + * atime - + * d2201, d2211, d3210, d3222, d4410, d4422, d5220, d5232, d5421, d5433 - + * dedt - + * didt - + * dmdt - + * dndt - + * dnodt - + * domdt - + * del1, del2, del3 - + * ses , sghl , sghs , sgs , shl , shs , sis , sls + * theta - + * xfact - + * xlamo - + * xli - + * xni + * + * locals : + * ainv2 - + * aonv - + * cosisq - + * eoc - + * f220, f221, f311, f321, f322, f330, f441, f442, f522, f523, f542, f543 - + * g200, g201, g211, g300, g310, g322, g410, g422, g520, g521, g532, g533 - + * sini2 - + * temp - + * temp1 - + * theta - + * xno2 - + * + * coupling : + * getgravconst + * + * references : + * hoots, roehrich, norad spacetrack report #3 1980 + * hoots, norad spacetrack report #6 1986 + * hoots, schumacher and glover 2004 + * vallado, crawford, hujsak, kelso 2006 + ---------------------------------------------------------------------------- */ +/** + * @param options + * @param tsince + */ +export default function dsinit(options: Options, tsince: number): Output { + const { + cosim, + argpo, + s1, + s2, + s3, + s4, + s5, + sinim, + ss1, + ss2, + ss3, + ss4, + ss5, + sz1, + sz3, + sz11, + sz13, + sz21, + sz23, + sz31, + sz33, + tc, + gsto, + mo, + mdot, + no, + nodeo, + nodedot, + xpidot, + z1, + z3, + z11, + z13, + z21, + z23, + z31, + z33, + ecco, + eccsq, + } = options; + + let { + emsq, + em, + argpm, + inclm, + mm, + nm, + nodem, + irez, + atime, + d2201, + d2211, + d3210, + d3222, + d4410, + d4422, + d5220, + d5232, + d5421, + d5433, + dedt, + didt, + dmdt, + dnodt, + domdt, + del1, + del2, + del3, + xfact, + xlamo, + xli, + xni, + } = options; + + let f220: number; + let f221: number; + let f311: number; + let f321: number; + let f322: number; + let f330: number; + let f441: number; + let f442: number; + let f522: number; + let f523: number; + let f542: number; + let f543: number; + let g200: number; + let g201: number; + let g211: number; + let g300: number; + let g310: number; + let g322: number; + let g410: number; + let g422: number; + let g520: number; + let g521: number; + let g532: number; + let g533: number; + let sini2: number; + let temp: number; + let temp1: number; + let xno2: number; + let ainv2: number; + let aonv: number; + let cosisq: number; + let eoc: number; + + const q22 = 1.7891679e-6; + const q31 = 2.1460748e-6; + const q33 = 2.2123015e-7; + const root22 = 1.7891679e-6; + const root44 = 7.3636953e-9; + const root54 = 2.1765803e-9; + + // eslint-disable-next-line no-loss-of-precision + const rptim = 4.37526908801129966e-3; // equates to 7.29211514668855e-5 rad/sec + const root32 = 3.7393792e-7; + const root52 = 1.1428639e-7; + const znl = 1.5835218e-4; + const zns = 1.19459e-5; + + // -------------------- deep space initialization ------------ + irez = 0; + if (nm < 0.0052359877 && nm > 0.0034906585) { + irez = 1; + } + if (nm >= 8.26e-3 && nm <= 9.24e-3 && em >= 0.5) { + irez = 2; + } + + // ------------------------ do solar terms ------------------- + const ses = ss1 * zns * ss5; + const sis = ss2 * zns * (sz11 + sz13); + const sls = -zns * ss3 * (sz1 + sz3 - 14.0 - 6.0 * emsq); + const sghs = ss4 * zns * (sz31 + sz33 - 6.0); + let shs = -zns * ss2 * (sz21 + sz23); + + // sgp4fix for 180 deg incl + if (inclm < 5.2359877e-2 || inclm > pi - 5.2359877e-2) { + shs = 0.0; + } + if (sinim !== 0.0) { + shs /= sinim; + } + const sgs = sghs - cosim * shs; + + // ------------------------- do lunar terms ------------------ + dedt = ses + s1 * znl * s5; + didt = sis + s2 * znl * (z11 + z13); + dmdt = sls - znl * s3 * (z1 + z3 - 14.0 - 6.0 * emsq); + const sghl = s4 * znl * (z31 + z33 - 6.0); + let shll = -znl * s2 * (z21 + z23); + + // sgp4fix for 180 deg incl + if (inclm < 5.2359877e-2 || inclm > pi - 5.2359877e-2) { + shll = 0.0; + } + domdt = sgs + sghl; + dnodt = shs; + if (sinim !== 0.0) { + domdt -= (cosim / sinim) * shll; + dnodt += shll / sinim; + } + + // ----------- calculate deep space resonance effects -------- + const dndt = 0.0; + const theta = (gsto + tc * rptim) % twoPi; + em += dedt * tsince; + inclm += didt * tsince; + argpm += domdt * tsince; + nodem += dnodt * tsince; + mm += dmdt * tsince; + + // sgp4fix for negative inclinations + // the following if statement should be commented out + // if (inclm < 0.0) + // { + // inclm = -inclm; + // argpm = argpm - pi; + // nodem = nodem + pi; + // } + + // -------------- initialize the resonance terms ------------- + if (irez !== 0) { + aonv = (nm / xke) ** x2o3; + + // ---------- geopotential resonance for 12 hour orbits ------ + if (irez === 2) { + cosisq = cosim * cosim; + const emo = em; + em = ecco; + const emsqo = emsq; + emsq = eccsq; + eoc = em * emsq; + g201 = -0.306 - (em - 0.64) * 0.44; + + if (em <= 0.65) { + g211 = 3.616 - 13.247 * em + 16.29 * emsq; + g310 = -19.302 + 117.39 * em - 228.419 * emsq + 156.591 * eoc; + g322 = -18.9068 + 109.7927 * em - 214.6334 * emsq + 146.5816 * eoc; + g410 = -41.122 + 242.694 * em - 471.094 * emsq + 313.953 * eoc; + g422 = -146.407 + 841.88 * em - 1629.014 * emsq + 1083.435 * eoc; + g520 = -532.114 + 3017.977 * em - 5740.032 * emsq + 3708.276 * eoc; + } else { + g211 = -72.099 + 331.819 * em - 508.738 * emsq + 266.724 * eoc; + g310 = -346.844 + 1582.851 * em - 2415.925 * emsq + 1246.113 * eoc; + g322 = -342.585 + 1554.908 * em - 2366.899 * emsq + 1215.972 * eoc; + g410 = -1052.797 + 4758.686 * em - 7193.992 * emsq + 3651.957 * eoc; + g422 = -3581.69 + 16178.11 * em - 24462.77 * emsq + 12422.52 * eoc; + if (em > 0.715) { + g520 = -5149.66 + 29936.92 * em - 54087.36 * emsq + 31324.56 * eoc; + } else { + g520 = 1464.74 - 4664.75 * em + 3763.64 * emsq; + } + } + if (em < 0.7) { + g533 = -919.2277 + 4988.61 * em - 9064.77 * emsq + 5542.21 * eoc; + g521 = -822.71072 + 4568.6173 * em - 8491.4146 * emsq + 5337.524 * eoc; + g532 = -853.666 + 4690.25 * em - 8624.77 * emsq + 5341.4 * eoc; + } else { + g533 = -37995.78 + 161616.52 * em - 229838.2 * emsq + 109377.94 * eoc; + g521 = -51752.104 + 218913.95 * em - 309468.16 * emsq + 146349.42 * eoc; + g532 = -40023.88 + 170470.89 * em - 242699.48 * emsq + 115605.82 * eoc; + } + sini2 = sinim * sinim; + f220 = 0.75 * (1.0 + 2.0 * cosim + cosisq); + f221 = 1.5 * sini2; + f321 = 1.875 * sinim * (1.0 - 2.0 * cosim - 3.0 * cosisq); + f322 = -1.875 * sinim * (1.0 + 2.0 * cosim - 3.0 * cosisq); + f441 = 35.0 * sini2 * f220; + f442 = 39.375 * sini2 * sini2; + + f522 = + 9.84375 * + sinim * + (sini2 * (1.0 - 2.0 * cosim - 5.0 * cosisq) + + 0.33333333 * (-2.0 + 4.0 * cosim + 6.0 * cosisq)); + f523 = + sinim * + (4.92187512 * sini2 * (-2.0 - 4.0 * cosim + 10.0 * cosisq) + + 6.56250012 * (1.0 + 2.0 * cosim - 3.0 * cosisq)); + f542 = + 29.53125 * sinim * (2.0 - 8.0 * cosim + cosisq * (-12.0 + 8.0 * cosim + 10.0 * cosisq)); + f543 = + 29.53125 * sinim * (-2.0 - 8.0 * cosim + cosisq * (12.0 + 8.0 * cosim - 10.0 * cosisq)); + + xno2 = nm * nm; + ainv2 = aonv * aonv; + temp1 = 3.0 * xno2 * ainv2; + temp = temp1 * root22; + d2201 = temp * f220 * g201; + d2211 = temp * f221 * g211; + temp1 *= aonv; + temp = temp1 * root32; + d3210 = temp * f321 * g310; + d3222 = temp * f322 * g322; + temp1 *= aonv; + temp = 2.0 * temp1 * root44; + d4410 = temp * f441 * g410; + d4422 = temp * f442 * g422; + temp1 *= aonv; + temp = temp1 * root52; + d5220 = temp * f522 * g520; + d5232 = temp * f523 * g532; + temp = 2.0 * temp1 * root54; + d5421 = temp * f542 * g521; + d5433 = temp * f543 * g533; + xlamo = (mo + nodeo + nodeo - (theta + theta)) % twoPi; + xfact = mdot + dmdt + 2.0 * (nodedot + dnodt - rptim) - no; + em = emo; + emsq = emsqo; + } + + // ---------------- synchronous resonance terms -------------- + if (irez === 1) { + g200 = 1.0 + emsq * (-2.5 + 0.8125 * emsq); + g310 = 1.0 + 2.0 * emsq; + g300 = 1.0 + emsq * (-6.0 + 6.60937 * emsq); + f220 = 0.75 * (1.0 + cosim) * (1.0 + cosim); + f311 = 0.9375 * sinim * sinim * (1.0 + 3.0 * cosim) - 0.75 * (1.0 + cosim); + f330 = 1.0 + cosim; + f330 *= 1.875 * f330 * f330; + del1 = 3.0 * nm * nm * aonv * aonv; + del2 = 2.0 * del1 * f220 * g200 * q22; + del3 = 3.0 * del1 * f330 * g300 * q33 * aonv; + del1 = del1 * f311 * g310 * q31 * aonv; + xlamo = (mo + nodeo + argpo - theta) % twoPi; + xfact = mdot + xpidot + dmdt + domdt + dnodt - (no + rptim); + } + + // ------------ for sgp4, initialize the integrator ---------- + xli = xlamo; + xni = no; + atime = 0.0; + nm = no + dndt; + } + + return { + em, + argpm, + inclm, + mm, + nm, + nodem, + + irez, + atime, + + d2201, + d2211, + d3210, + d3222, + d4410, + + d4422, + d5220, + d5232, + d5421, + d5433, + + dedt, + didt, + dmdt, + dndt, + dnodt, + domdt, + + del1, + del2, + del3, + + xfact, + xlamo, + xli, + xni, + }; +} diff --git a/src/space/propagation/dspace.ts b/src/space/propagation/dspace.ts new file mode 100644 index 00000000..f051369d --- /dev/null +++ b/src/space/propagation/dspace.ts @@ -0,0 +1,322 @@ +import { twoPi } from '../util/constants'; + +/** + * + */ +export interface Options { + irez: number; + d2201: number; + d2211: number; + d3210: number; + d3222: number; + d4410: number; + d4422: number; + d5220: number; + d5232: number; + d5421: number; + d5433: number; + dedt: number; + del1: number; + del2: number; + del3: number; + didt: number; + dmdt: number; + dnodt: number; + domdt: number; + argpo: number; + argpdot: number; + tc: number; + gsto: number; + xfact: number; + xlamo: number; + no: number; + atime: number; + em: number; + argpm: number; + inclm: number; + xli: number; + mm: number; + xni: number; + nodem: number; + nm: number; +} + +/** + * + */ +export interface Output { + atime: number; + em: number; // eccentricity + argpm: number; // argument of perigee + inclm: number; // inclination + xli: number; + mm: number; // mean anomaly + xni: number; + nodem: number; // right ascension of ascending node + dndt: number; + nm: number; // mean motion +} + +/* ----------------------------------------------------------------------------- + * + * procedure dspace + * + * this procedure provides deep space contributions to mean elements for + * perturbing third body. these effects have been averaged over one + * revolution of the sun and moon. for earth resonance effects, the + * effects have been averaged over no revolutions of the satellite. + * (mean motion) + * + * author : david vallado 719-573-2600 28 jun 2005 + * + * inputs : + * d2201, d2211, d3210, d3222, d4410, d4422, d5220, d5232, d5421, d5433 - + * dedt - + * del1, del2, del3 - + * didt - + * dmdt - + * dnodt - + * domdt - + * irez - flag for resonance 0-none, 1-one day, 2-half day + * argpo - argument of perigee + * argpdot - argument of perigee dot (rate) + * t - time + * tc - + * gsto - gst + * xfact - + * xlamo - + * no - mean motion + * atime - + * em - eccentricity + * ft - + * argpm - argument of perigee + * inclm - inclination + * xli - + * mm - mean anomaly + * xni - mean motion + * nodem - right ascension of ascending node + * + * outputs : + * atime - + * em - eccentricity + * argpm - argument of perigee + * inclm - inclination + * xli - + * mm - mean anomaly + * xni - + * nodem - right ascension of ascending node + * dndt - + * nm - mean motion + * + * locals : + * delt - + * ft - + * theta - + * x2li - + * x2omi - + * xl - + * xldot - + * xnddt - + * xndt - + * xomi - + * + * coupling : + * none - + * + * references : + * hoots, roehrich, norad spacetrack report #3 1980 + * hoots, norad spacetrack report #6 1986 + * hoots, schumacher and glover 2004 + * vallado, crawford, hujsak, kelso 2006 + ---------------------------------------------------------------------------- */ +/** + * @param options + * @param tsince + */ +export default function dspace(options: Options, tsince: number): Output { + const { + irez, + d2201, + d2211, + d3210, + d3222, + d4410, + d4422, + d5220, + d5232, + d5421, + d5433, + dedt, + del1, + del2, + del3, + didt, + dmdt, + dnodt, + domdt, + argpo, + argpdot, + tc, + gsto, + xfact, + xlamo, + no, + } = options; + + let { atime, em, argpm, inclm, xli, mm, xni, nodem, nm } = options; + + const fasx2 = 0.13130908; + const fasx4 = 2.8843198; + const fasx6 = 0.37448087; + const g22 = 5.7686396; + const g32 = 0.95240898; + const g44 = 1.8014998; + const g52 = 1.050833; + const g54 = 4.4108898; + + // eslint-disable-next-line no-loss-of-precision + const rptim = 4.37526908801129966e-3; // equates to 7.29211514668855e-5 rad/sec + const stepp = 720.0; + const stepn = -720.0; + const step2 = 259200.0; + + let delt: number; + let x2li: number; + let x2omi: number; + let xl: number; + let xldot = 0; + let xnddt = 0; + let xndt = 0; + let xomi: number; + let dndt = 0.0; + let ft = 0.0; + + // ----------- calculate deep space resonance effects ----------- + const theta = (gsto + tc * rptim) % twoPi; + em += dedt * tsince; + + inclm += didt * tsince; + argpm += domdt * tsince; + nodem += dnodt * tsince; + mm += dmdt * tsince; + + // sgp4fix for negative inclinations + // the following if statement should be commented out + // if (inclm < 0.0) + // { + // inclm = -inclm; + // argpm = argpm - pi; + // nodem = nodem + pi; + // } + + /* - update resonances : numerical (euler-maclaurin) integration - */ + /* ------------------------- epoch restart ---------------------- */ + // sgp4fix for propagator problems + // the following integration works for negative time steps and periods + // the specific changes are unknown because the original code was so convoluted + + // sgp4fix take out atime = 0.0 and fix for faster operation + + if (irez !== 0) { + // sgp4fix streamline check + if (atime === 0.0 || tsince * atime <= 0.0 || Math.abs(tsince) < Math.abs(atime)) { + atime = 0.0; + xni = no; + xli = xlamo; + } + + // sgp4fix move check outside loop + if (tsince > 0.0) { + delt = stepp; + } else { + delt = stepn; + } + + let iretn = 381; // added for do loop + while (iretn === 381) { + // ------------------- dot terms calculated ------------- + // ----------- near - synchronous resonance terms ------- + if (irez !== 2) { + xndt = + del1 * Math.sin(xli - fasx2) + + del2 * Math.sin(2.0 * (xli - fasx4)) + + del3 * Math.sin(3.0 * (xli - fasx6)); + xldot = xni + xfact; + xnddt = + del1 * Math.cos(xli - fasx2) + + 2.0 * del2 * Math.cos(2.0 * (xli - fasx4)) + + 3.0 * del3 * Math.cos(3.0 * (xli - fasx6)); + xnddt *= xldot; + } else { + // --------- near - half-day resonance terms -------- + xomi = argpo + argpdot * atime; + x2omi = xomi + xomi; + x2li = xli + xli; + xndt = + d2201 * Math.sin(x2omi + xli - g22) + + d2211 * Math.sin(xli - g22) + + d3210 * Math.sin(xomi + xli - g32) + + d3222 * Math.sin(-xomi + xli - g32) + + d4410 * Math.sin(x2omi + x2li - g44) + + d4422 * Math.sin(x2li - g44) + + d5220 * Math.sin(xomi + xli - g52) + + d5232 * Math.sin(-xomi + xli - g52) + + d5421 * Math.sin(xomi + x2li - g54) + + d5433 * Math.sin(-xomi + x2li - g54); + xldot = xni + xfact; + xnddt = + d2201 * Math.cos(x2omi + xli - g22) + + d2211 * Math.cos(xli - g22) + + d3210 * Math.cos(xomi + xli - g32) + + d3222 * Math.cos(-xomi + xli - g32) + + d5220 * Math.cos(xomi + xli - g52) + + d5232 * Math.cos(-xomi + xli - g52) + + 2.0 * + (d4410 * Math.cos(x2omi + x2li - g44) + + d4422 * Math.cos(x2li - g44) + + d5421 * Math.cos(xomi + x2li - g54) + + d5433 * Math.cos(-xomi + x2li - g54)); + xnddt *= xldot; + } + + // ----------------------- integrator ------------------- + // sgp4fix move end checks to end of routine + if (Math.abs(tsince - atime) >= stepp) { + iretn = 381; + } else { + ft = tsince - atime; + iretn = 0; + } + + if (iretn === 381) { + xli += xldot * delt + xndt * step2; + xni += xndt * delt + xnddt * step2; + atime += delt; + } + } + + nm = xni + xndt * ft + xnddt * ft * ft * 0.5; + xl = xli + xldot * ft + xndt * ft * ft * 0.5; + if (irez !== 1) { + mm = xl - 2.0 * nodem + 2.0 * theta; + dndt = nm - no; + } else { + mm = xl - nodem - argpm + theta; + dndt = nm - no; + } + nm = no + dndt; + } + + return { + atime, + em, + argpm, + inclm, + xli, + mm, + xni, + nodem, + dndt, + nm, + }; +} diff --git a/src/space/propagation/initl.ts b/src/space/propagation/initl.ts new file mode 100644 index 00000000..51ae21bf --- /dev/null +++ b/src/space/propagation/initl.ts @@ -0,0 +1,167 @@ +/* eslint-disable no-loss-of-precision */ +import gstime from '../util/time'; +import { j2, twoPi, x2o3, xke } from '../util/constants'; + +import type { Method, OperationMode } from '../sat'; + +/** + * + */ +export interface Options { + ecco: number; + epoch: number; + inclo: number; + no: number; + opsmode: OperationMode; +} + +/** + * + */ +export interface Output { + no: number; + method: Method; + ainv: number; + ao: number; + con41: number; + con42: number; + cosio: number; + cosio2: number; + eccsq: number; + omeosq: number; + posq: number; + rp: number; + rteosq: number; + sinio: number; + gsto: number; +} + +/* ----------------------------------------------------------------------------- + * + * procedure initl + * + * this procedure initializes the sgp4 propagator. all the initialization is + * consolidated here instead of having multiple loops inside other routines. + * + * author : david vallado 719-573-2600 28 jun 2005 + * + * inputs : + * ecco - eccentricity 0.0 - 1.0 + * epoch - epoch time in days from jan 0, 1950. 0 hr + * inclo - inclination of satellite + * no - mean motion of satellite + * satn - satellite number + * + * outputs : + * ainv - 1.0 / a + * ao - semi major axis + * con41 - + * con42 - 1.0 - 5.0 cos(i) + * cosio - cosine of inclination + * cosio2 - cosio squared + * eccsq - eccentricity squared + * method - flag for deep space 'd', 'n' + * omeosq - 1.0 - ecco * ecco + * posq - semi-parameter squared + * rp - radius of perigee + * rteosq - square root of (1.0 - ecco*ecco) + * sinio - sine of inclination + * gsto - gst at time of observation rad + * no - mean motion of satellite + * + * locals : + * ak - + * d1 - + * del - + * adel - + * po - + * + * coupling : + * getgravconst + * gstime - find greenwich sidereal time from the julian date + * + * references : + * hoots, roehrich, norad spacetrack report #3 1980 + * hoots, norad spacetrack report #6 1986 + * hoots, schumacher and glover 2004 + * vallado, crawford, hujsak, kelso 2006 + ---------------------------------------------------------------------------- */ +/** + * @param options + */ +export default function initl(options: Options): Output { + const { ecco, epoch, inclo, opsmode } = options; + + let { no } = options; + + // sgp4fix use old way of finding gst + // ----------------------- earth constants --------------------- + // sgp4fix identify constants and allow alternate values + + // ------------- calculate auxillary epoch quantities ---------- + const eccsq = ecco * ecco; + const omeosq = 1.0 - eccsq; + const rteosq = Math.sqrt(omeosq); + const cosio = Math.cos(inclo); + const cosio2 = cosio * cosio; + + // ------------------ un-kozai the mean motion ----------------- + const ak = (xke / no) ** x2o3; + const d1 = (0.75 * j2 * (3.0 * cosio2 - 1.0)) / (rteosq * omeosq); + let delPrime = d1 / (ak * ak); + const adel = + ak * + (1.0 - delPrime * delPrime - delPrime * (1.0 / 3.0 + (134.0 * delPrime * delPrime) / 81.0)); + delPrime = d1 / (adel * adel); + no /= 1.0 + delPrime; + + const ao = (xke / no) ** x2o3; + const sinio = Math.sin(inclo); + const po = ao * omeosq; + const con42 = 1.0 - 5.0 * cosio2; + const con41 = -con42 - cosio2 - cosio2; + const ainv = 1.0 / ao; + const posq = po * po; + const rp = ao * (1.0 - ecco); + const method = 'n'; + + // sgp4fix modern approach to finding sidereal time + let gsto; + if (opsmode === 'a') { + // sgp4fix use old way of finding gst + // count integer number of days from 0 jan 1970 + const ts70 = epoch - 7305.0; + const ds70 = Math.floor(ts70 + 1.0e-8); + const tfrac = ts70 - ds70; + + // find greenwich location at epoch + const c1 = 1.72027916940703639e-2; + const thgr70 = 1.7321343856509374; + const fk5r = 5.07551419432269442e-15; + const c1p2p = c1 + twoPi; + gsto = (thgr70 + c1 * ds70 + c1p2p * tfrac + ts70 * ts70 * fk5r) % twoPi; + if (gsto < 0.0) { + gsto += twoPi; + } + } else { + gsto = gstime(epoch + 2433281.5); + } + + return { + no, + method, + ainv, + ao, + con41, + con42, + cosio, + cosio2, + eccsq, + omeosq, + posq, + rp, + rteosq, + sinio, + gsto, + }; +} diff --git a/src/space/propagation/sgp4.ts b/src/space/propagation/sgp4.ts new file mode 100644 index 00000000..fb074438 --- /dev/null +++ b/src/space/propagation/sgp4.ts @@ -0,0 +1,494 @@ +import { earthRadius, j2, j3oj2, pi, twoPi, vkmpersec, x2o3, xke } from '../util/constants'; + +import { Satellite } from '../sat'; +import dpper from './dpper'; +import dspace from './dspace'; + +/** + * + */ +export interface ErrorOutput { + type: number; + error: string; +} + +/** + * + */ +export interface Output { + position: { + x: number; + y: number; + z: number; + }; + velocity: { + x: number; + y: number; + z: number; + }; +} + +/* ---------------------------------------------------------------------------- + * + * procedure sgp4 + * + * this procedure is the sgp4 prediction model from space command. this is an + * updated and combined version of sgp4 and sdp4, which were originally + * published separately in spacetrack report //3. this version follows the + * methodology from the aiaa paper (2006) describing the history and + * development of the code. + * + * author : david vallado 719-573-2600 28 jun 2005 + * + * inputs : + * tle - initialised structure from sgp4init() call. + * tsince - time since epoch (minutes) + * + * outputs : + * r - position vector km + * v - velocity km/sec + * return code - non-zero on error. + * 1 - mean elements, ecc >= 1.0 or ecc < -0.001 or a < 0.95 er + * 2 - mean motion less than 0.0 + * 3 - pert elements, ecc < 0.0 or ecc > 1.0 + * 4 - semi-latus rectum < 0.0 + * 5 - epoch elements are sub-orbital + * 6 - satellite has decayed + * + * locals : + * am - + * axnl, aynl - + * betal - + * cosim , sinim , cosomm , sinomm , cnod , snod , cos2u , + * sin2u , coseo1 , sineo1 , cosi , sini , cosip , sinip , + * cosisq , cossu , sinsu , cosu , sinu + * delm - + * delomg - + * dndt - + * eccm - + * emsq - + * ecose - + * el2 - + * eo1 - + * eccp - + * esine - + * argpm - + * argpp - + * omgadf - + * pl - + * r - + * rtemsq - + * rdotl - + * rl - + * rvdot - + * rvdotl - + * su - + * t2 , t3 , t4 , tc + * tem5, temp , temp1 , temp2 , tempa , tempe , templ + * u , ux , uy , uz , vx , vy , vz + * inclm - inclination + * mm - mean anomaly + * nm - mean motion + * nodem - right asc of ascending node + * xinc - + * xincp - + * xl - + * xlm - + * mp - + * xmdf - + * xmx - + * xmy - + * nodedf - + * xnode - + * nodep - + * np - + * + * coupling : + * getgravconst- + * dpper + * dspace + * + * references : + * hoots, roehrich, norad spacetrack report //3 1980 + * hoots, norad spacetrack report //6 1986 + * hoots, schumacher and glover 2004 + * vallado, crawford, hujsak, kelso 2006 + ---------------------------------------------------------------------------- */ +/** + * @param sat + * @param tsince + */ +export default function sgp4(sat: Satellite, tsince: number): ErrorOutput | Output { + const { + anomaly, + motion, + eccentricity, + inclination, + method, + drag, + mdot, + perigee, + argpdot, + ascension, + nodedot, + nodecf, + cc1, + cc4, + cc5, + t2cof, + isimp, + omgcof, + eta, + xmcof, + delmo, + d2, + d3, + d4, + sinmao, + t3cof, + t4cof, + t5cof, + irez, + d2201, + d2211, + d3210, + d3222, + d4410, + d4422, + d5220, + d5232, + d5421, + d5433, + dedt, + del1, + del2, + del3, + didt, + dmdt, + dnodt, + domdt, + opsmode, + gsto, + xfact, + xlamo, + atime, + xli, + xni, + } = sat; + + let { aycof, xlcof, con41, x1mth2, x7thm1 } = sat; + + let coseo1 = 0; + let sineo1 = 0; + let cosip: number; + let sinip: number; + let cosisq: number; + let delm: number; + let delomg: number; + let eo1: number; + let argpm: number; + let argpp: number; + let su: number; + let t3: number; + let t4: number; + let tc: number; + let tem5: number; + let temp: number; + let tempa: number; + let tempe: number; + let templ: number; + let inclm: number; + let mm: number; + let nm: number; + let nodem: number; + let xincp: number; + let xlm: number; + let mp: number; + let nodep: number; + + /* ------------------ set mathematical constants --------------- */ + // sgp4fix divisor for divide by zero check on inclination + // the old check used 1.0 + cos(pi-1.0e-9), but then compared it to + // 1.5 e-12, so the threshold was changed to 1.5e-12 for consistency + + const temp4 = 1.5e-12; + + // ------- update for secular gravity and atmospheric drag ----- + const xmdf = anomaly + mdot * tsince; + const argpdf = perigee + argpdot * tsince; + const nodedf = ascension + nodedot * tsince; + argpm = argpdf; + mm = xmdf; + const t2 = tsince * tsince; + nodem = nodedf + nodecf * t2; + tempa = 1.0 - cc1 * tsince; + tempe = drag * cc4 * tsince; + templ = t2cof * t2; + + if (isimp !== 1) { + delomg = omgcof * tsince; + // sgp4fix use mutliply for speed instead of pow + const delmtemp = 1.0 + eta * Math.cos(xmdf); + delm = xmcof * (delmtemp * delmtemp * delmtemp - delmo); + temp = delomg + delm; + mm = xmdf + temp; + argpm = argpdf - temp; + t3 = t2 * tsince; + t4 = t3 * tsince; + tempa = tempa - d2 * t2 - d3 * t3 - d4 * t4; + tempe += drag * cc5 * (Math.sin(mm) - sinmao); + templ = templ + t3cof * t3 + t4 * (t4cof + tsince * t5cof); + } + nm = motion; + let em = eccentricity; + inclm = inclination; + if (method === 'd') { + tc = tsince; + + const dspaceOptions = { + irez, + d2201, + d2211, + d3210, + d3222, + d4410, + d4422, + d5220, + d5232, + d5421, + d5433, + dedt, + del1, + del2, + del3, + didt, + dmdt, + dnodt, + domdt, + argpo: perigee, + argpdot, + tc, + gsto, + xfact, + xlamo, + no: motion, + atime, + em, + argpm, + inclm, + xli, + mm, + xni, + nodem, + nm, + }; + + const dspaceResult = dspace(dspaceOptions, tsince); + + ({ em, argpm, inclm, mm, nodem, nm } = dspaceResult); + } + + if (nm <= 0.0) { + // sgp4fix add return + return { + type: 2, + error: `error nm ${nm}`, + }; + } + + const am = (xke / nm) ** x2o3 * tempa * tempa; + nm = xke / am ** 1.5; + em -= tempe; + + // fix tolerance for error recognition + // sgp4fix am is fixed from the previous nm check + if (em >= 1.0 || em < -0.001) { + // || (am < 0.95) + // sgp4fix to return if there is an error in eccentricity + return { + type: 1, + error: `error em ${em}`, + }; + } + + // sgp4fix fix tolerance to avoid a divide by zero + if (em < 1.0e-6) { + em = 1.0e-6; + } + mm += motion * templ; + xlm = mm + argpm + nodem; + + nodem %= twoPi; + argpm %= twoPi; + xlm %= twoPi; + mm = (xlm - argpm - nodem) % twoPi; + + // ----------------- compute extra mean quantities ------------- + const sinim = Math.sin(inclm); + const cosim = Math.cos(inclm); + + // -------------------- add lunar-solar periodics -------------- + let ep = em; + xincp = inclm; + argpp = argpm; + nodep = nodem; + mp = mm; + sinip = sinim; + cosip = cosim; + if (method === 'd') { + const dpperParameters = { + init: false, + ep, + inclp: xincp, + nodep, + argpp, + mp, + opsmode, + }; + const dpperResult = dpper(sat, dpperParameters, tsince); + + ({ ep, nodep, argpp, mp } = dpperResult); + + xincp = dpperResult.inclp; + + if (xincp < 0.0) { + xincp = -xincp; + nodep += pi; + argpp -= pi; + } + if (ep < 0.0 || ep > 1.0) { + // sgp4fix add return + return { + type: 3, + error: `error ep ${ep}`, + }; + } + } + + // -------------------- long period periodics ------------------ + if (method === 'd') { + sinip = Math.sin(xincp); + cosip = Math.cos(xincp); + aycof = -0.5 * j3oj2 * sinip; + + // sgp4fix for divide by zero for xincp = 180 deg + if (Math.abs(cosip + 1.0) > 1.5e-12) { + xlcof = (-0.25 * j3oj2 * sinip * (3.0 + 5.0 * cosip)) / (1.0 + cosip); + } else { + xlcof = (-0.25 * j3oj2 * sinip * (3.0 + 5.0 * cosip)) / temp4; + } + } + + const axnl = ep * Math.cos(argpp); + temp = 1.0 / (am * (1.0 - ep * ep)); + const aynl = ep * Math.sin(argpp) + temp * aycof; + const xl = mp + argpp + nodep + temp * xlcof * axnl; + + // --------------------- solve kepler's equation --------------- + const u = (xl - nodep) % twoPi; + eo1 = u; + tem5 = 9999.9; + let ktr = 1; + + // sgp4fix for kepler iteration + // the following iteration needs better limits on corrections + while (Math.abs(tem5) >= 1.0e-12 && ktr <= 10) { + sineo1 = Math.sin(eo1); + coseo1 = Math.cos(eo1); + tem5 = 1.0 - coseo1 * axnl - sineo1 * aynl; + tem5 = (u - aynl * coseo1 + axnl * sineo1 - eo1) / tem5; + if (Math.abs(tem5) >= 0.95) { + if (tem5 > 0.0) { + tem5 = 0.95; + } else { + tem5 = -0.95; + } + } + eo1 += tem5; + ktr += 1; + } + + // ------------- short period preliminary quantities ----------- + const ecose = axnl * coseo1 + aynl * sineo1; + const esine = axnl * sineo1 - aynl * coseo1; + const el2 = axnl * axnl + aynl * aynl; + const pl = am * (1.0 - el2); + if (pl < 0.0) { + // sgp4fix add return + return { + type: 4, + error: `error pl ${pl}`, + }; + } + + const rl = am * (1.0 - ecose); + const rdotl = (Math.sqrt(am) * esine) / rl; + const rvdotl = Math.sqrt(pl) / rl; + const betal = Math.sqrt(1.0 - el2); + temp = esine / (1.0 + betal); + const sinu = (am / rl) * (sineo1 - aynl - axnl * temp); + const cosu = (am / rl) * (coseo1 - axnl + aynl * temp); + su = Math.atan2(sinu, cosu); + const sin2u = (cosu + cosu) * sinu; + const cos2u = 1.0 - 2.0 * sinu * sinu; + temp = 1.0 / pl; + const temp1 = 0.5 * j2 * temp; + const temp2 = temp1 * temp; + + // -------------- update for short period periodics ------------ + if (method === 'd') { + cosisq = cosip * cosip; + con41 = 3.0 * cosisq - 1.0; + x1mth2 = 1.0 - cosisq; + x7thm1 = 7.0 * cosisq - 1.0; + } + + const mrt = rl * (1.0 - 1.5 * temp2 * betal * con41) + 0.5 * temp1 * x1mth2 * cos2u; + + // sgp4fix for decaying satellites + if (mrt < 1.0) { + return { + type: 6, + error: `decay condition ${mrt}`, + }; + } + + su -= 0.25 * temp2 * x7thm1 * sin2u; + const xnode = nodep + 1.5 * temp2 * cosip * sin2u; + const xinc = xincp + 1.5 * temp2 * cosip * sinip * cos2u; + const mvt = rdotl - (nm * temp1 * x1mth2 * sin2u) / xke; + const rvdot = rvdotl + (nm * temp1 * (x1mth2 * cos2u + 1.5 * con41)) / xke; + + // --------------------- orientation vectors ------------------- + const sinsu = Math.sin(su); + const cossu = Math.cos(su); + const snod = Math.sin(xnode); + const cnod = Math.cos(xnode); + const sini = Math.sin(xinc); + const cosi = Math.cos(xinc); + const xmx = -snod * cosi; + const xmy = cnod * cosi; + const ux = xmx * sinsu + cnod * cossu; + const uy = xmy * sinsu + snod * cossu; + const uz = sini * sinsu; + const vx = xmx * cossu - cnod * sinsu; + const vy = xmy * cossu - snod * sinsu; + const vz = sini * cossu; + + // --------- position and velocity (in km and km/sec) ---------- + const r = { + x: mrt * ux * earthRadius, + y: mrt * uy * earthRadius, + z: mrt * uz * earthRadius, + }; + const v = { + x: (mvt * ux + rvdot * vx) * vkmpersec, + y: (mvt * uy + rvdot * vy) * vkmpersec, + z: (mvt * uz + rvdot * vz) * vkmpersec, + }; + + return { + position: r, + velocity: v, + }; +} diff --git a/src/space/propagation/sgp4init.ts b/src/space/propagation/sgp4init.ts new file mode 100644 index 00000000..06be1da5 --- /dev/null +++ b/src/space/propagation/sgp4init.ts @@ -0,0 +1,570 @@ +import { earthRadius, j2, j3oj2, j4, pi, x2o3 } from '../util/constants'; + +import { Satellite } from '../sat'; +import dpper from './dpper'; +import dscom from './dscom'; +import dsinit from './dsinit'; +import initl from './initl'; + +/* ----------------------------------------------------------------------------- + * + * procedure sgp4init + * + * this procedure initializes variables for sgp4. + * + * author : david vallado 719-573-2600 28 jun 2005 + * author : david vallado 719-573-2600 28 jun 2005 + * + * inputs : + * opsmode - mode of operation afspc or improved 'a', 'i' + * satn - satellite number + * drag - sgp4 type drag coefficient kg/m2er + * ecco - eccentricity + * epoch - epoch time in days from jan 0, 1950. 0 hr + * argpo - argument of perigee (output if ds) + * inclo - inclination + * mo - mean anomaly (output if ds) + * no - mean motion + * nodeo - right ascension of ascending node + * + * outputs : + * rec - common values for subsequent calls + * return code - non-zero on error. + * 1 - mean elements, ecc >= 1.0 or ecc < -0.001 or a < 0.95 er + * 2 - mean motion less than 0.0 + * 3 - pert elements, ecc < 0.0 or ecc > 1.0 + * 4 - semi-latus rectum < 0.0 + * 5 - epoch elements are sub-orbital + * 6 - satellite has decayed + * + * locals : + * cnodm , snodm , cosim , sinim , cosomm , sinomm + * cc1sq , cc2 , cc3 + * coef , coef1 + * cosio4 - + * day - + * dndt - + * em - eccentricity + * emsq - eccentricity squared + * eeta - + * etasq - + * gam - + * argpm - argument of perigee + * nodem - + * inclm - inclination + * mm - mean anomaly + * nm - mean motion + * perige - perigee + * pinvsq - + * psisq - + * qzms24 - + * rtemsq - + * s1, s2, s3, s4, s5, s6, s7 - + * sfour - + * ss1, ss2, ss3, ss4, ss5, ss6, ss7 - + * sz1, sz2, sz3 + * sz11, sz12, sz13, sz21, sz22, sz23, sz31, sz32, sz33 - + * tc - + * temp - + * temp1, temp2, temp3 - + * tsi - + * xpidot - + * xhdot1 - + * z1, z2, z3 - + * z11, z12, z13, z21, z22, z23, z31, z32, z33 - + * + * coupling : + * getgravconst- + * initl - + * dscom - + * dpper - + * dsinit - + * sgp4 - + * + * references : + * hoots, roehrich, norad spacetrack report #3 1980 + * hoots, norad spacetrack report #6 1986 + * hoots, schumacher and glover 2004 + * vallado, crawford, hujsak, kelso 2006 + ---------------------------------------------------------------------------- */ +/** + * @param sat + */ +export default function sgp4init(sat: Satellite): void { + const epoch = sat.jdsatepoch - 2433281.5; + + let cosim: number; + let sinim: number; + let cc1sq: number; + let cc2: number; + let cc3: number; + let coef: number; + let coef1: number; + let cosio4: number; + let em: number; + let emsq: number; + let eeta: number; + let etasq: number; + let argpm: number; + let nodem: number; + let inclm: number; + let mm: number; + let nm: number; + let perige: number; + let pinvsq: number; + let psisq: number; + let qzms24: number; + let s1: number; + let s2: number; + let s3: number; + let s4: number; + let s5: number; + let sfour: number; + let ss1: number; + let ss2: number; + let ss3: number; + let ss4: number; + let ss5: number; + let sz1: number; + let sz3: number; + let sz11: number; + let sz13: number; + let sz21: number; + let sz23: number; + let sz31: number; + let sz33: number; + let tc: number; + let temp: number; + let temp1: number; + let temp2: number; + let temp3: number; + let tsi: number; + let xpidot: number; + let xhdot1: number; + let z1: number; + let z3: number; + let z11: number; + let z13: number; + let z21: number; + let z23: number; + let z31: number; + let z33: number; + + /* ------------------------ initialization --------------------- */ + // sgp4fix divisor for divide by zero check on inclination + // the old check used 1.0 + Math.cos(pi-1.0e-9), but then compared it to + // 1.5 e-12, so the threshold was changed to 1.5e-12 for consistency + const temp4 = 1.5e-12; + + // sgp4fix - note the following variables are also passed directly via sat. + // it is possible to streamline the sgp4init call by deleting the "x" + // variables, but the user would need to set the sat.* values first. we + // include the additional assignments in case twoline2rv is not used. + + // ------------------------ earth constants ----------------------- + // sgp4fix identify constants and allow alternate values + + const ss = 78.0 / earthRadius + 1.0; + // sgp4fix use multiply for speed instead of pow + const qzms2ttemp = (120.0 - 78.0) / earthRadius; + const qzms2t = qzms2ttemp * qzms2ttemp * qzms2ttemp * qzms2ttemp; + + sat.init = true; + + const initlOptions = { + // satn, + ecco: sat.eccentricity, + + epoch, + inclo: sat.inclination, + no: sat.motion, + + opsmode: sat.opsmode, + }; + + const initlResult = initl(initlOptions); + + const { ao, con42, cosio, cosio2, eccsq, omeosq, posq, rp, rteosq, sinio } = initlResult; + + sat.motion = initlResult.no; + sat.con41 = initlResult.con41; + sat.gsto = initlResult.gsto; + // const a = (sat.motion * tumin) ** (-2.0 / 3.0); + // const alta = a * (1.0 + sat.eccentricity) - 1.0; + // const altp = a * (1.0 - sat.eccentricity) - 1.0; + + // sgp4fix remove this check as it is unnecessary + // the mrt check in sgp4 handles decaying satellite cases even if the starting + // condition is below the surface of te earth + // if (rp < 1.0) + // { + // printf("// *** satn%d epoch elts sub-orbital ***\n", satn); + // sat.error = 5; + // } + + if (omeosq >= 0.0 || sat.motion >= 0.0) { + sat.isimp = 0; + if (rp < 220.0 / earthRadius + 1.0) { + sat.isimp = 1; + } + sfour = ss; + qzms24 = qzms2t; + perige = (rp - 1.0) * earthRadius; + + // - for perigees below 156 km, s and qoms2t are altered - + if (perige < 156.0) { + sfour = perige - 78.0; + if (perige < 98.0) { + sfour = 20.0; + } + + // sgp4fix use multiply for speed instead of pow + const qzms24temp = (120.0 - sfour) / earthRadius; + qzms24 = qzms24temp * qzms24temp * qzms24temp * qzms24temp; + sfour = sfour / earthRadius + 1.0; + } + pinvsq = 1.0 / posq; + + tsi = 1.0 / (ao - sfour); + sat.eta = ao * sat.eccentricity * tsi; + etasq = sat.eta * sat.eta; + eeta = sat.eccentricity * sat.eta; + psisq = Math.abs(1.0 - etasq); + coef = qzms24 * tsi ** 4.0; + coef1 = coef / psisq ** 3.5; + cc2 = + coef1 * + sat.motion * + (ao * (1.0 + 1.5 * etasq + eeta * (4.0 + etasq)) + + ((0.375 * j2 * tsi) / psisq) * sat.con41 * (8.0 + 3.0 * etasq * (8.0 + etasq))); + sat.cc1 = sat.drag * cc2; + cc3 = 0.0; + if (sat.eccentricity > 1.0e-4) { + cc3 = (-2.0 * coef * tsi * j3oj2 * sat.motion * sinio) / sat.eccentricity; + } + sat.x1mth2 = 1.0 - cosio2; + sat.cc4 = + 2.0 * + sat.motion * + coef1 * + ao * + omeosq * + (sat.eta * (2.0 + 0.5 * etasq) + + sat.eccentricity * (0.5 + 2.0 * etasq) - + ((j2 * tsi) / (ao * psisq)) * + (-3.0 * sat.con41 * (1.0 - 2.0 * eeta + etasq * (1.5 - 0.5 * eeta)) + + 0.75 * + sat.x1mth2 * + (2.0 * etasq - eeta * (1.0 + etasq)) * + Math.cos(2.0 * sat.perigee))); + sat.cc5 = 2.0 * coef1 * ao * omeosq * (1.0 + 2.75 * (etasq + eeta) + eeta * etasq); + cosio4 = cosio2 * cosio2; + temp1 = 1.5 * j2 * pinvsq * sat.motion; + temp2 = 0.5 * temp1 * j2 * pinvsq; + temp3 = -0.46875 * j4 * pinvsq * pinvsq * sat.motion; + sat.mdot = + sat.motion + + 0.5 * temp1 * rteosq * sat.con41 + + 0.0625 * temp2 * rteosq * (13.0 - 78.0 * cosio2 + 137.0 * cosio4); + sat.argpdot = + -0.5 * temp1 * con42 + + 0.0625 * temp2 * (7.0 - 114.0 * cosio2 + 395.0 * cosio4) + + temp3 * (3.0 - 36.0 * cosio2 + 49.0 * cosio4); + xhdot1 = -temp1 * cosio; + sat.nodedot = + xhdot1 + (0.5 * temp2 * (4.0 - 19.0 * cosio2) + 2.0 * temp3 * (3.0 - 7.0 * cosio2)) * cosio; + xpidot = sat.argpdot + sat.nodedot; + sat.omgcof = sat.drag * cc3 * Math.cos(sat.perigee); + sat.xmcof = 0.0; + if (sat.eccentricity > 1.0e-4) { + sat.xmcof = (-x2o3 * coef * sat.drag) / eeta; + } + sat.nodecf = 3.5 * omeosq * xhdot1 * sat.cc1; + sat.t2cof = 1.5 * sat.cc1; + + // sgp4fix for divide by zero with xinco = 180 deg + if (Math.abs(cosio + 1.0) > 1.5e-12) { + sat.xlcof = (-0.25 * j3oj2 * sinio * (3.0 + 5.0 * cosio)) / (1.0 + cosio); + } else { + sat.xlcof = (-0.25 * j3oj2 * sinio * (3.0 + 5.0 * cosio)) / temp4; + } + sat.aycof = -0.5 * j3oj2 * sinio; + + // sgp4fix use multiply for speed instead of pow + const delmotemp = 1.0 + sat.eta * Math.cos(sat.anomaly); + sat.delmo = delmotemp * delmotemp * delmotemp; + sat.sinmao = Math.sin(sat.anomaly); + sat.x7thm1 = 7.0 * cosio2 - 1.0; + + // --------------- deep space initialization ------------- + if ((2 * pi) / sat.motion >= 225.0) { + sat.method = 'd'; + sat.isimp = 1; + tc = 0.0; + inclm = sat.inclination; + + const dscomOptions = { + epoch, + ep: sat.eccentricity, + argpp: sat.perigee, + tc, + inclp: sat.inclination, + nodep: sat.ascension, + + np: sat.motion, + + e3: sat.e3, + ee2: sat.ee2, + + peo: sat.peo, + pgho: sat.pgho, + pho: sat.pho, + pinco: sat.pinco, + + plo: sat.plo, + se2: sat.se2, + se3: sat.se3, + + sgh2: sat.sgh2, + sgh3: sat.sgh3, + sgh4: sat.sgh4, + + sh2: sat.sh2, + sh3: sat.sh3, + si2: sat.si2, + si3: sat.si3, + + sl2: sat.sl2, + sl3: sat.sl3, + sl4: sat.sl4, + + xgh2: sat.xgh2, + xgh3: sat.xgh3, + xgh4: sat.xgh4, + xh2: sat.xh2, + + xh3: sat.xh3, + xi2: sat.xi2, + xi3: sat.xi3, + xl2: sat.xl2, + + xl3: sat.xl3, + xl4: sat.xl4, + + zmol: sat.zmol, + zmos: sat.zmos, + }; + + const dscomResult = dscom(dscomOptions); + + sat.e3 = dscomResult.e3; + sat.ee2 = dscomResult.ee2; + + sat.peo = dscomResult.peo; + sat.pgho = dscomResult.pgho; + sat.pho = dscomResult.pho; + + sat.pinco = dscomResult.pinco; + sat.plo = dscomResult.plo; + sat.se2 = dscomResult.se2; + sat.se3 = dscomResult.se3; + + sat.sgh2 = dscomResult.sgh2; + sat.sgh3 = dscomResult.sgh3; + sat.sgh4 = dscomResult.sgh4; + sat.sh2 = dscomResult.sh2; + sat.sh3 = dscomResult.sh3; + + sat.si2 = dscomResult.si2; + sat.si3 = dscomResult.si3; + sat.sl2 = dscomResult.sl2; + sat.sl3 = dscomResult.sl3; + sat.sl4 = dscomResult.sl4; + + ({ + sinim, + cosim, + em, + emsq, + s1, + s2, + s3, + s4, + s5, + ss1, + ss2, + ss3, + ss4, + ss5, + sz1, + sz3, + sz11, + sz13, + sz21, + sz23, + sz31, + sz33, + } = dscomResult); + + sat.xgh2 = dscomResult.xgh2; + sat.xgh3 = dscomResult.xgh3; + sat.xgh4 = dscomResult.xgh4; + sat.xh2 = dscomResult.xh2; + sat.xh3 = dscomResult.xh3; + sat.xi2 = dscomResult.xi2; + sat.xi3 = dscomResult.xi3; + sat.xl2 = dscomResult.xl2; + sat.xl3 = dscomResult.xl3; + sat.xl4 = dscomResult.xl4; + sat.zmol = dscomResult.zmol; + sat.zmos = dscomResult.zmos; + + ({ nm, z1, z3, z11, z13, z21, z23, z31, z33 } = dscomResult); + + const dpperOptions = { + inclo: inclm, + init: sat.init, + ep: sat.eccentricity, + inclp: sat.inclination, + nodep: sat.ascension, + argpp: sat.perigee, + mp: sat.anomaly, + opsmode: sat.opsmode, + }; + + const dpperResult = dpper(sat, dpperOptions, 0); + + sat.eccentricity = dpperResult.ep; + sat.inclination = dpperResult.inclp; + sat.ascension = dpperResult.nodep; + sat.perigee = dpperResult.argpp; + sat.anomaly = dpperResult.mp; + + argpm = 0.0; + nodem = 0.0; + mm = 0.0; + + const dsinitOptions = { + cosim, + emsq, + argpo: sat.perigee, + s1, + s2, + s3, + s4, + s5, + sinim, + ss1, + ss2, + ss3, + ss4, + ss5, + sz1, + sz3, + sz11, + sz13, + sz21, + sz23, + sz31, + sz33, + tc, + gsto: sat.gsto, + mo: sat.anomaly, + mdot: sat.mdot, + no: sat.motion, + nodeo: sat.ascension, + nodedot: sat.nodedot, + xpidot, + z1, + z3, + z11, + z13, + z21, + z23, + z31, + z33, + ecco: sat.eccentricity, + eccsq, + em, + argpm, + inclm, + mm, + nm, + nodem, + irez: sat.irez, + atime: sat.atime, + d2201: sat.d2201, + d2211: sat.d2211, + d3210: sat.d3210, + d3222: sat.d3222, + d4410: sat.d4410, + d4422: sat.d4422, + d5220: sat.d5220, + d5232: sat.d5232, + d5421: sat.d5421, + d5433: sat.d5433, + dedt: sat.dedt, + didt: sat.didt, + dmdt: sat.dmdt, + dnodt: sat.dnodt, + domdt: sat.domdt, + del1: sat.del1, + del2: sat.del2, + del3: sat.del3, + xfact: sat.xfact, + xlamo: sat.xlamo, + xli: sat.xli, + xni: sat.xni, + }; + + const dsinitResult = dsinit(dsinitOptions, 0); + + sat.irez = dsinitResult.irez; + sat.atime = dsinitResult.atime; + sat.d2201 = dsinitResult.d2201; + sat.d2211 = dsinitResult.d2211; + + sat.d3210 = dsinitResult.d3210; + sat.d3222 = dsinitResult.d3222; + sat.d4410 = dsinitResult.d4410; + sat.d4422 = dsinitResult.d4422; + sat.d5220 = dsinitResult.d5220; + + sat.d5232 = dsinitResult.d5232; + sat.d5421 = dsinitResult.d5421; + sat.d5433 = dsinitResult.d5433; + sat.dedt = dsinitResult.dedt; + sat.didt = dsinitResult.didt; + + sat.dmdt = dsinitResult.dmdt; + sat.dnodt = dsinitResult.dnodt; + sat.domdt = dsinitResult.domdt; + sat.del1 = dsinitResult.del1; + + sat.del2 = dsinitResult.del2; + sat.del3 = dsinitResult.del3; + sat.xfact = dsinitResult.xfact; + sat.xlamo = dsinitResult.xlamo; + sat.xli = dsinitResult.xli; + + sat.xni = dsinitResult.xni; + } + + // ----------- set variables if not deep space ----------- + if (sat.isimp !== 1) { + cc1sq = sat.cc1 * sat.cc1; + sat.d2 = 4.0 * ao * tsi * cc1sq; + temp = (sat.d2 * tsi * sat.cc1) / 3.0; + sat.d3 = (17.0 * ao + sfour) * temp; + sat.d4 = 0.5 * temp * ao * tsi * (221.0 * ao + 31.0 * sfour) * sat.cc1; + sat.t3cof = sat.d2 + 2.0 * cc1sq; + sat.t4cof = 0.25 * (3.0 * sat.d3 + sat.cc1 * (12.0 * sat.d2 + 10.0 * cc1sq)); + sat.t5cof = + 0.2 * + (3.0 * sat.d4 + + 12.0 * sat.cc1 * sat.d3 + + 6.0 * sat.d2 * sat.d2 + + 15.0 * cc1sq * (2.0 * sat.d2 + cc1sq)); + } + } +} diff --git a/src/space/sat.ts b/src/space/sat.ts new file mode 100644 index 00000000..20b71a44 --- /dev/null +++ b/src/space/sat.ts @@ -0,0 +1,536 @@ +import sgp4 from './propagation/sgp4'; +import sgp4init from './propagation/sgp4init'; +import { days2mdhms, jday } from './util/time'; +import { deg2rad, minutesPerDay, pi } from './util/constants'; + +import type { ErrorOutput as SGP4ErrorOutput, Output as SGP4Output } from './propagation/sgp4'; + +/** + * + */ +export type Classification = 'U' | 'C' | 'S'; // (U: unclassified, C: classified, S: secret) +/** + * + */ +export type OperationMode = 'a' | 'i'; // mode of operation afspc or improved 'a', 'i' +/** + * + */ +export type Method = 'd' | 'n'; // flag for deep space 'd', 'n' + +/** + * + */ +export interface TLEData { + name: string; + number: number; + class: Classification; + id: string; + date: string | Date; + fdmm: number; + sdmm: number; + drag: number; + ephemeris: number; + esn: number; + inclination: number; + ascension: number; + eccentricity: number; + perigee: number; + anomaly: number; + motion: number; + revolution: number; + rms?: number; +} + +/** + * + */ +export interface TLEDataCelestrak { + OBJECT_NAME: string; + OBJECT_ID: string; + EPOCH: string; + MEAN_MOTION: number; + ECCENTRICITY: number; + INCLINATION: number; + RA_OF_ASC_NODE: number; + ARG_OF_PERICENTER: number; + MEAN_ANOMALY: number; + EPHEMERIS_TYPE: number; + CLASSIFICATION_TYPE: string; + NORAD_CAT_ID: number; + ELEMENT_SET_NO: number; + REV_AT_EPOCH: number; + BSTAR: number; + MEAN_MOTION_DOT: number; + MEAN_MOTION_DDOT: number; + RMS: string; + DATA_SOURCE: string; +} + +// TLE example +// STARLINK-1007 +// 1 44713C 19074A 23048.53451389 -.00009219 00000+0 -61811-3 0 482 +// 2 44713 53.0512 157.2379 0001140 81.3827 74.7980 15.06382459 15 + +/** + * + */ +export class Satellite { + init = false; + // Line 0 + name = 'default'; + // Line 1 + number = 0; // (satnum) Satellite catalog number or NORAD (North American Aerospace Defense) Catalog Number + class: Classification = 'U'; // Classification (U: unclassified, C: classified, S: secret) + id = 'null'; // International Designator + date: Date = new Date(); // (epochyr + epochdays) + epochyr = 0; + epochdays = 0; + jdsatepoch; + fdmm = 0; // (ndot) First derivative of mean motion; the ballistic coefficient + sdmm = 0; // (nddot) Second derivative of mean motion (decimal point assumed) + drag = 0; // (bstar) B*, the drag term, or radiation pressure coefficient (decimal point assumed) + ephemeris = 0; // Ephemeris type (always zero; only used in undistributed TLE data) + esn = 0; // Element set number. Incremented when a new TLE is generated for this object. + // Line 2 + inclination = 0; // Inclination (degrees) + ascension = 0; // Right ascension of the ascending node (degrees) + eccentricity = 0; // Eccentricity (decimal point assumed) + perigee = 0; // Argument of perigee (degrees) + anomaly = 0; // Mean anomaly (degrees) + motion = 0; // Mean motion (revolutions per day) + revolution = 0; // Revolution number at epoch (revolutions) + // extra + opsmode: OperationMode = 'i'; + rms?: number; + // ----------- all near earth variables ------------ + isimp = 0; + method: Method = 'n'; + aycof = 0; + con41 = 0; + cc1 = 0; + cc4 = 0; + cc5 = 0; + d2 = 0; + d3 = 0; + d4 = 0; + delmo = 0; + eta = 0; + argpdot = 0; + omgcof = 0; + sinmao = 0; + t2cof = 0; + t3cof = 0; + t4cof = 0; + t5cof = 0; + x1mth2 = 0; + x7thm1 = 0; + mdot = 0; + nodedot = 0; + xlcof = 0; + xmcof = 0; + nodecf = 0; + // ----------- all deep space variables ------------ + irez = 0; + d2201 = 0; + d2211 = 0; + d3210 = 0; + d3222 = 0; + d4410 = 0; + d4422 = 0; + d5220 = 0; + d5232 = 0; + d5421 = 0; + d5433 = 0; + dedt = 0; + del1 = 0; + del2 = 0; + del3 = 0; + didt = 0; + dmdt = 0; + dnodt = 0; + domdt = 0; + e3 = 0; + ee2 = 0; + peo = 0; + pgho = 0; + pho = 0; + pinco = 0; + plo = 0; + se2 = 0; + se3 = 0; + sgh2 = 0; + sgh3 = 0; + sgh4 = 0; + sh2 = 0; + sh3 = 0; + si2 = 0; + si3 = 0; + sl2 = 0; + sl3 = 0; + sl4 = 0; + gsto = 0; + xfact = 0; + xgh2 = 0; + xgh3 = 0; + xgh4 = 0; + xh2 = 0; + xh3 = 0; + xi2 = 0; + xi3 = 0; + xl2 = 0; + xl3 = 0; + xl4 = 0; + xlamo = 0; + zmol = 0; + zmos = 0; + atime = 0; + xli = 0; + xni = 0; + /** + * @param data + * @param initialize + */ + constructor(data: TLEData | string, initialize = true) { + if (typeof data === 'string') this.#parseTLE(data); + else this.#parseJSON(data); + // convert degrees to rad + this.inclination *= deg2rad; + this.ascension *= deg2rad; + this.perigee *= deg2rad; + this.anomaly *= deg2rad; + // convert revolution from deg/day to rad/minute + this.motion /= 1440 / (2 * pi); // rad/min (229.1831180523293) + // find sgp4epoch time of element set + // remember that sgp4 uses units of days from 0 jan 1950 (sgp4epoch) + // and minutes from the epoch (time) + const year = this.epochyr < 57 ? this.epochyr + 2000 : this.epochyr + 1900; + const mdhmsResult = days2mdhms(year, this.epochdays); + const { mon, day, hr, minute, sec } = mdhmsResult; + this.jdsatepoch = jday(year, mon, day, hr, minute, sec, 0); + + if (initialize) sgp4init(this); + } + + /** API */ + + /** + * @param time + */ + propagate(time: Date): SGP4ErrorOutput | SGP4Output { + const j = jday(time); + return sgp4(this, (j - this.jdsatepoch) * minutesPerDay); + } + + // time in minutes since epoch + /** + * @param time + */ + sgp4(time: number): SGP4ErrorOutput | SGP4Output { + return sgp4(this, time); + } + + // to gpu variables + /** + * + */ + gpu(): number[] { + return [ + this.anomaly, + this.motion, + this.eccentricity, + this.inclination, + this.method === 'd' ? 0 : 1, // 0 -> 'd', 1 -> 'n' + this.opsmode === 'a' ? 0 : 1, // 0 -> 'a'; 1 -> 'i' + this.drag, + this.mdot, + this.perigee, + this.argpdot, + this.ascension, + this.nodedot, + this.nodecf, + this.cc1, + this.cc4, + this.cc5, + this.t2cof, + this.isimp, + this.omgcof, + this.eta, + this.xmcof, + this.delmo, + this.d2, + this.d3, + this.d4, + this.sinmao, + this.t3cof, + this.t4cof, + this.t5cof, + this.irez, + this.d2201, + this.d2211, + this.d3210, + this.d3222, + this.d4410, + this.d4422, + this.d5220, + this.d5232, + this.d5421, + this.d5433, + this.dedt, + this.del1, + this.del2, + this.del3, + this.didt, + this.dmdt, + this.dnodt, + this.domdt, + this.gsto, + this.xfact, + this.xlamo, + this.atime, + this.xli, + this.xni, + this.aycof, + this.xlcof, + this.con41, + this.x1mth2, + this.x7thm1, + this.zmos, + this.zmol, + this.se2, + this.se3, + this.si2, + this.si3, + this.sl2, + this.sl3, + this.sl4, + this.sgh2, + this.sgh3, + this.sgh4, + this.sh2, + this.sh3, + this.ee2, + this.e3, + this.xi2, + this.xi3, + this.xl2, + this.xl3, + this.xl4, + this.xgh2, + this.xgh3, + this.xgh4, + this.xh2, + this.xh3, + this.peo, + this.pinco, + this.plo, + this.pgho, + this.pho, + ]; + } + + /** INTERNAL */ + + /** + * @param data + */ + #parseJSON(data: TLEData): void { + if (typeof data.date === 'string') data.date = new Date(data.date); + for (const [key, value] of Object.entries(data)) { + if (key in this) this[key as keyof this] = value; + } + + this.epochyr = this.date.getFullYear() % 100; + const start = new Date(Date.UTC(this.date.getFullYear(), 0, 0)); + this.epochdays = jday(this.date) - jday(start); + } + + // https://en.wikipedia.org/wiki/Two-line_element_set + /** + * @param value + */ + #parseTLE(value: string): void { + const lines = trim(String(value)).split(/\r?\n/g); + let line: string; + let checksum: number; + + // Line 0 + if (lines.length === 3) { + this.name = trim(lines.shift() as string); + if (this.name.substring(0, 2) === '0 ') this.name = this.name.substring(2); + } + + // Line 1 + line = lines.shift() as string; + checksum = check(line); + + if (checksum !== Number(line.substring(68, 69))) { + throw new Error( + `Line 1 checksum mismatch: ${checksum} != ${line.substring(68, 69)}: ${line}`, + ); + } + + this.number = this.#parseFloat(alpha5Converter(line.substring(2, 7))); + this.class = trim(line.substring(7, 9)) as Classification; + this.id = trim(line.substring(9, 18)); + this.date = this.#parseDate(line.substring(18, 33)); + this.fdmm = this.#parseFloat(line.substring(33, 44)); + this.sdmm = this.#parseFloat(line.substring(44, 53)); + this.drag = this.#parseDrag(line.substring(53, 62)); + this.ephemeris = this.#parseFloat(line.substring(62, 64)); + this.esn = this.#parseFloat(line.substring(64, 68)); + + // Line 2 + line = lines.shift() as string; + checksum = check(line); + + if (checksum !== Number(line.substring(68, 69))) { + throw new Error( + `Line 2 checksum mismatch: ${checksum} != ${line.substring(68, 69)}: ${line}`, + ); + } + + this.inclination = this.#parseFloat(line.substring(8, 17)); + this.ascension = this.#parseFloat(line.substring(17, 26)); + this.eccentricity = this.#parseFloat('0.' + line.substring(26, 34)); + this.perigee = this.#parseFloat(line.substring(34, 43)); + this.anomaly = this.#parseFloat(line.substring(43, 52)); + this.motion = this.#parseFloat(line.substring(52, 63)); + this.revolution = this.#parseFloat(line.substring(63, 68)); + } + + /** + * @param value + */ + #parseFloat(value: string): number { + const pattern = /([-])?([.\d]+)([+-]\d+)?/; + const match = pattern.exec(value); + + if (match !== null) { + const sign = match[1] === '-' ? -1 : 1; + const power = match[3] !== undefined ? 'e' + match[3] : 'e0'; + const value = match[2]; + return sign * parseFloat(value + power); + } + + return NaN; + } + + /** + * @param value + */ + #parseDrag(value: string): number { + const pattern = /([-])?([.\d]+)([+-]\d+)?/; + const match = pattern.exec(value); + + if (match !== null) { + const sign = match[1] === '-' ? -1 : 1; + const power = match[3] !== undefined ? 'e' + match[3] : 'e0'; + const value = !match[2].includes('.') ? '0.' + match[2] : match[2]; + return sign * parseFloat(value + power); + } + + return NaN; + } + + /** + * @param value + */ + #parseDate(value: string): Date { + value = String(value).replace(/^\s+|\s+$/, ''); + + const epoch = (this.epochyr = parseInt(value.substring(0, 2), 10)); + const days = (this.epochdays = parseFloat(value.substring(2))); + + let year = new Date().getFullYear(); + const currentEpoch = year % 100; + const century = year - currentEpoch; + + year = epoch > currentEpoch + 1 ? century - 100 + epoch : century + epoch; + + const day = Math.floor(days); + const hours = 24 * (days - day); + const hour = Math.floor(hours); + const minutes = 60 * (hours - hour); + const minute = Math.floor(minutes); + const seconds = 60 * (minutes - minute); + const second = Math.floor(seconds); + const millisecond = 1000 * (seconds - second); + + const utc = Date.UTC(year, 0, day, hour, minute, second, millisecond); + + return new Date(utc); + } +} + +// JSON https://celestrak.org/NORAD/elements/supplemental/index.php?FORMAT=json example: +/** + * @param data + */ +export function convertCelestrak(data: TLEDataCelestrak): TLEData { + // convert date to UTC to avoid javascripts localization issues + const date = new Date(data.EPOCH); + const utc = Date.UTC( + date.getFullYear(), + date.getMonth(), + date.getDate(), + date.getHours(), + date.getMinutes(), + date.getSeconds(), + date.getMilliseconds(), + ); + return { + name: data.OBJECT_NAME, + number: data.NORAD_CAT_ID, + class: data.CLASSIFICATION_TYPE as Classification, + id: data.OBJECT_ID, + date: new Date(utc), + fdmm: data.MEAN_MOTION_DOT, + sdmm: data.MEAN_MOTION_DDOT, + drag: data.BSTAR, + ephemeris: data.EPHEMERIS_TYPE, + esn: data.ELEMENT_SET_NO, + inclination: data.INCLINATION, + ascension: data.RA_OF_ASC_NODE, + eccentricity: data.ECCENTRICITY, + perigee: data.ARG_OF_PERICENTER, + anomaly: data.MEAN_ANOMALY, + motion: data.MEAN_MOTION, + revolution: data.REV_AT_EPOCH, + rms: parseFloat(data.RMS), + }; +} + +/** + * @param line + */ +function check(line: string): number { + let sum = 0; + + for (const char of line.substring(0, 68)) { + // deno-lint-ignore no-explicit-any + if (!isNaN(char as unknown as number)) sum += Number(char); + else if (char === '-') sum++; + } + + return sum % 10; +} + +// NOTE: Alpha5 skips I and O +/** + * @param str + */ +function alpha5Converter(str: string): string { + const firstChar = str.charAt(0); + // deno-lint-ignore no-explicit-any + if (!isNaN(firstChar as unknown as number)) return str; + const alpha5Index = 'ABCDEFGHJKLMNPQRSTUVWXYZ'.indexOf(firstChar); + return String(alpha5Index + 10) + str.slice(1); +} + +/** + * @param str + */ +function trim(str: string): string { + return str.replace(/^[\s\uFEFF\xA0]+|[\s\uFEFF\xA0]+$/g, ''); +} diff --git a/src/space/util/constants.ts b/src/space/util/constants.ts new file mode 100644 index 00000000..de5f5b59 --- /dev/null +++ b/src/space/util/constants.ts @@ -0,0 +1,15 @@ +export const pi = Math.PI; +export const twoPi = pi * 2; +export const deg2rad = pi / 180.0; +export const rad2deg = 180 / pi; +export const minutesPerDay = 1440.0; +export const mu = 398600.8; // in km3 / s2 +export const earthRadius = 6378.135; // in km +export const xke = 60.0 / Math.sqrt((earthRadius * earthRadius * earthRadius) / mu); +export const vkmpersec = (earthRadius * xke) / 60.0; +export const tumin = 1.0 / xke; +export const j2 = 0.001082616; +export const j3 = -0.00000253881; +export const j4 = -0.00000165597; +export const j3oj2 = j3 / j2; +export const x2o3 = 2.0 / 3.0; diff --git a/src/space/util/time.ts b/src/space/util/time.ts new file mode 100644 index 00000000..918a47b4 --- /dev/null +++ b/src/space/util/time.ts @@ -0,0 +1,299 @@ +import { deg2rad, twoPi } from './constants'; + +/* ----------------------------------------------------------------------------- + * + * procedure days2mdhms + * + * this procedure converts the day of the year, days, to the equivalent month + * day, hour, minute and second. + * + * algorithm : set up array for the number of days per month + * find leap year - use 1900 because 2000 is a leap year + * loop through a temp value while the value is < the days + * perform int conversions to the correct day and month + * convert remainder into h m s using type conversions + * + * author : david vallado 719-573-2600 1 mar 2001 + * + * inputs description range / units + * year - year 1900 .. 2100 + * days - julian day of the year 0.0 .. 366.0 + * + * outputs : + * mon - month 1 .. 12 + * day - day 1 .. 28,29,30,31 + * hr - hour 0 .. 23 + * min - minute 0 .. 59 + * sec - second 0.0 .. 59.999 + * + * locals : + * dayofyr - day of year + * temp - temporary extended values + * inttemp - temporary int value + * i - index + * lmonth[12] - int array containing the number of days per month + * + * coupling : + * none. + * --------------------------------------------------------------------------- */ +/** + * @param year + * @param days + */ +export function days2mdhms( + year: number, + days: number, +): { mon: number; day: number; hr: number; minute: number; sec: number } { + const lmonth = [31, year % 4 === 0 ? 29 : 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31]; + const dayofyr = Math.floor(days); + + // ----------------- find month and day of month ---------------- + let i = 1; + let inttemp = 0; + while (dayofyr > inttemp + lmonth[i - 1] && i < 12) { + inttemp += lmonth[i - 1]; + i += 1; + } + + const mon = i; + const day = dayofyr - inttemp; + + // ----------------- find hours minutes and seconds ------------- + let temp = (days - dayofyr) * 24.0; + const hr = Math.floor(temp); + temp = (temp - hr) * 60.0; + const minute = Math.floor(temp); + const sec = (temp - minute) * 60.0; + + return { + mon, + day, + hr, + minute, + sec, + }; +} + +/* ----------------------------------------------------------------------------- + * + * procedure jday + * + * this procedure finds the julian date given the year, month, day, and time. + * the julian date is defined by each elapsed day since noon, jan 1, 4713 bc. + * + * algorithm : calculate the answer in one step for efficiency + * + * author : david vallado 719-573-2600 1 mar 2001 + * + * inputs description range / units + * year - year 1900 .. 2100 + * mon - month 1 .. 12 + * day - day 1 .. 28,29,30,31 + * hr - universal time hour 0 .. 23 + * min - universal time min 0 .. 59 + * sec - universal time sec 0.0 .. 59.999 + * + * outputs : + * jd - julian date days from 4713 bc + * + * locals : + * none. + * + * coupling : + * none. + * + * references : + * vallado 2007, 189, alg 14, ex 3-14 + * + * --------------------------------------------------------------------------- */ +/** + * @param year + * @param mon + * @param day + * @param hr + * @param minute + * @param sec + * @param msec + */ +function jdayInternal( + year: number, + mon: number, + day: number, + hr: number, + minute: number, + sec: number, + msec = 0, +): number { + return ( + 367.0 * year - + Math.floor(7 * (year + Math.floor((mon + 9) / 12.0)) * 0.25) + + Math.floor((275 * mon) / 9.0) + + day + + 1721013.5 + + ((msec / 60000 + sec / 60.0 + minute) / 60.0 + hr) / 24.0 // ut in days + // # - 0.5*sgn(100.0*year + mon - 190002.5) + 0.5; + ); +} + +/** + * @param year + * @param mon + * @param day + * @param hr + * @param min + * @param sec + * @param msec + */ +export function jday( + year: number | Date, + mon = 0, + day = 0, + hr = 0, + min = 0, + sec = 0, + msec = 0, +): number { + if (year instanceof Date) { + const date = year; + return jdayInternal( + date.getUTCFullYear(), + date.getUTCMonth() + 1, // Note, this function requires months in range 1-12. + date.getUTCDate(), + date.getUTCHours(), + date.getUTCMinutes(), + date.getUTCSeconds(), + date.getUTCMilliseconds(), + ); + } + + return jdayInternal(year, mon, day, hr, min, sec, msec); +} + +/* ----------------------------------------------------------------------------- + * + * procedure invjday + * + * this procedure finds the year, month, day, hour, minute and second + * given the julian date. tu can be ut1, tdt, tdb, etc. + * + * algorithm : set up starting values + * find leap year - use 1900 because 2000 is a leap year + * find the elapsed days through the year in a loop + * call routine to find each individual value + * + * author : david vallado 719-573-2600 1 mar 2001 + * + * inputs description range / units + * jd - julian date days from 4713 bc + * + * outputs : + * year - year 1900 .. 2100 + * mon - month 1 .. 12 + * day - day 1 .. 28,29,30,31 + * hr - hour 0 .. 23 + * min - minute 0 .. 59 + * sec - second 0.0 .. 59.999 + * + * locals : + * days - day of year plus fractional + * portion of a day days + * tu - julian centuries from 0 h + * jan 0, 1900 + * temp - temporary double values + * leapyrs - number of leap years from 1900 + * + * coupling : + * days2mdhms - finds month, day, hour, minute and second given days and year + * + * references : + * vallado 2007, 208, alg 22, ex 3-13 + * --------------------------------------------------------------------------- */ +/** + * @param jd + * @param asArray + */ +export function invjday( + jd: number, + asArray: boolean, +): Date | [number, number, number, number, number, number] { + // --------------- find year and days of the year - + const temp = jd - 2415019.5; + const tu = temp / 365.25; + let year = 1900 + Math.floor(tu); + let leapyrs = Math.floor((year - 1901) * 0.25); + + // optional nudge by 8.64x10-7 sec to get even outputs + let days = temp - ((year - 1900) * 365.0 + leapyrs) + 0.00000000001; + + // ------------ check for case of beginning of a year ----------- + if (days < 1.0) { + year -= 1; + leapyrs = Math.floor((year - 1901) * 0.25); + days = temp - ((year - 1900) * 365.0 + leapyrs); + } + + // ----------------- find remaing data ------------------------- + const mdhms = days2mdhms(year, days); + + const { mon, day, hr, minute } = mdhms; + + const sec = mdhms.sec - 0.000000864; + + if (asArray) { + return [year, mon, day, hr, minute, Math.floor(sec)]; + } + + return new Date(Date.UTC(year, mon - 1, day, hr, minute, Math.floor(sec))); +} + +/* ----------------------------------------------------------------------------- + * + * function gstime + * + * this function finds the greenwich sidereal time. + * + * author : david vallado 719-573-2600 1 mar 2001 + * + * inputs description range / units + * jdut1 - julian date in ut1 days from 4713 bc + * + * outputs : + * gstime - greenwich sidereal time 0 to 2pi rad + * + * locals : + * temp - temporary variable for doubles rad + * tut1 - julian centuries from the + * jan 1, 2000 12 h epoch (ut1) + * + * coupling : + * none + * + * references : + * vallado 2004, 191, eq 3-45 + * --------------------------------------------------------------------------- */ +/** + * @param jdut1 + */ +function gstimeInternal(jdut1: number): number { + const tut1 = (jdut1 - 2451545.0) / 36525.0; + + let temp = + -6.2e-6 * tut1 * tut1 * tut1 + + 0.093104 * tut1 * tut1 + + (876600.0 * 3600 + 8640184.812866) * tut1 + + 67310.54841; // # sec + temp = ((temp * deg2rad) / 240.0) % twoPi; // 360/86400 = 1/240, to deg, to rad + + // ------------------------ check quadrants --------------------- + if (temp < 0.0) temp += twoPi; + + return temp; +} + +/** + * @param time + */ +export default function gstime(time: Date | number): number { + if (time instanceof Date) return gstimeInternal(jday(time)); + return gstimeInternal(time); +} diff --git a/src/dataStructures/delaunator.ts b/src/tools/delaunator.ts similarity index 100% rename from src/dataStructures/delaunator.ts rename to src/tools/delaunator.ts diff --git a/src/tools/index.ts b/src/tools/index.ts new file mode 100644 index 00000000..11bc3c3c --- /dev/null +++ b/src/tools/index.ts @@ -0,0 +1,3 @@ +export { default as Delaunator } from './delaunator'; +export { default as Orthodrome } from './orthodrome'; +export { default as polylabel } from './polylabel'; diff --git a/src/dataStructures/orthodrome.ts b/src/tools/orthodrome.ts similarity index 100% rename from src/dataStructures/orthodrome.ts rename to src/tools/orthodrome.ts diff --git a/src/dataStructures/polylabel.ts b/src/tools/polylabel.ts similarity index 94% rename from src/dataStructures/polylabel.ts rename to src/tools/polylabel.ts index 6af4a1ea..bece4635 100644 --- a/src/dataStructures/polylabel.ts +++ b/src/tools/polylabel.ts @@ -1,4 +1,4 @@ -import PriorityQueue from './priorityQueue'; +import PriorityQueue from '../dataStructures/priorityQueue'; import { VectorPoint, VectorPolygon } from '../geometry'; /** @@ -30,7 +30,10 @@ export default function polylabel( if (cellSize === precision) return { x: minX, y: minY, m: { distance: 0 } }; // a priority queue of cells in order of their "potential" (max distance to polygon) - const cellQueue = new PriorityQueue([], (a: Cell, b: Cell): number => b.max - a.max); + const cellQueue = new PriorityQueue( + [], + (a: PolyLabelCell, b: PolyLabelCell): number => b.max - a.max, + ); // take centroid as the first best guess let bestCell = getCentroidCell(polygon); @@ -82,7 +85,7 @@ export default function polylabel( } /** A cell in the polygon label algorithm */ -export interface Cell { +export interface PolyLabelCell { /** cell center x */ x: number; /** cell center y */ @@ -102,7 +105,7 @@ export interface Cell { * @param polygon - the vector polygon * @returns - the cell */ -function buildCell(x: number, y: number, h: number, polygon: VectorPolygon): Cell { +function buildCell(x: number, y: number, h: number, polygon: VectorPolygon): PolyLabelCell { const d = pointToPolygonDist(x, y, polygon); return { x, y, h, d, max: d + h * Math.SQRT2 }; } @@ -138,7 +141,7 @@ function pointToPolygonDist(x: number, y: number, polygon: VectorPolygon): numbe * @param polygon - the vector polygon * @returns - the centroid as a cell */ -function getCentroidCell(polygon: VectorPolygon): Cell { +function getCentroidCell(polygon: VectorPolygon): PolyLabelCell { let area = 0; let x = 0; let y = 0; diff --git a/src/util/base64.ts b/src/util/base64.ts deleted file mode 100644 index 662e32a2..00000000 --- a/src/util/base64.ts +++ /dev/null @@ -1,13 +0,0 @@ -/** - * pollyfill for string to array buffer - * @param base64 - base64 encoded string - * @returns converted ArrayBuffer of the string data - */ -export function base64ToArrayBuffer(base64: string): ArrayBuffer { - const binaryString = atob(base64); - const len = binaryString.length; - const bytes = new Uint8Array(len); - for (let i = 0; i < len; i++) bytes[i] = binaryString.charCodeAt(i); - - return bytes.buffer as ArrayBuffer; -} diff --git a/src/util/index.ts b/src/util/index.ts index cd4a1e7c..03d5a806 100644 --- a/src/util/index.ts +++ b/src/util/index.ts @@ -1,5 +1,19 @@ export * from './gzip'; +/** + * pollyfill for string to array buffer + * @param base64 - base64 encoded string + * @returns converted ArrayBuffer of the string data + */ +export function base64ToArrayBuffer(base64: string): ArrayBuffer { + const binaryString = atob(base64); + const len = binaryString.length; + const bytes = new Uint8Array(len); + for (let i = 0; i < len; i++) bytes[i] = binaryString.charCodeAt(i); + + return bytes.buffer as ArrayBuffer; +} + /** * @param uint8arrays - the Uint8Arrays to concatenate * @returns - the concatenated Uint8Array diff --git a/src/util/lzw.ts b/src/util/lzw.ts new file mode 100644 index 00000000..0c850ce3 --- /dev/null +++ b/src/util/lzw.ts @@ -0,0 +1,157 @@ +const MIN_BITS = 9; +const CLEAR_CODE = 256; // clear code +const EOI_CODE = 257; // end of information +const MAX_BYTELENGTH = 12; + +/** + * @param array + * @param position + * @param length + */ +function getByte(array, position, length) { + const d = position % 8; + const a = Math.floor(position / 8); + const de = 8 - d; + const ef = position + length - (a + 1) * 8; + let fg = 8 * (a + 2) - (position + length); + const dg = (a + 2) * 8 - position; + fg = Math.max(0, fg); + if (a >= array.length) { + console.warn('ran off the end of the buffer before finding EOI_CODE (end on input code)'); + return EOI_CODE; + } + let chunk1 = array[a] & (2 ** (8 - d) - 1); + chunk1 <<= length - de; + let chunks = chunk1; + if (a + 1 < array.length) { + let chunk2 = array[a + 1] >>> fg; + chunk2 <<= Math.max(0, length - dg); + chunks += chunk2; + } + if (ef > 8 && a + 2 < array.length) { + const hi = (a + 3) * 8 - (position + length); + const chunk3 = array[a + 2] >>> hi; + chunks += chunk3; + } + return chunks; +} + +/** + * @param dest + * @param source + */ +function appendReversed(dest, source) { + for (let i = source.length - 1; i >= 0; i--) { + dest.push(source[i]); + } + return dest; +} + +/** + * @param input + */ +function decompress(input: ArrayBufferLike): Uint8Array { + const dictionaryIndex = new Uint16Array(4093); + const dictionaryChar = new Uint8Array(4093); + for (let i = 0; i <= 257; i++) { + dictionaryIndex[i] = 4096; + dictionaryChar[i] = i; + } + let dictionaryLength = 258; + let byteLength = MIN_BITS; + let position = 0; + + /** + * + */ + function initDictionary() { + dictionaryLength = 258; + byteLength = MIN_BITS; + } + /** + * @param array + */ + function getNext(array) { + const byte = getByte(array, position, byteLength); + position += byteLength; + return byte; + } + /** + * @param i + * @param c + */ + function addToDictionary(i, c) { + dictionaryChar[dictionaryLength] = c; + dictionaryIndex[dictionaryLength] = i; + dictionaryLength++; + return dictionaryLength - 1; + } + /** + * @param n + */ + function getDictionaryReversed(n) { + const rev = []; + for (let i = n; i !== 4096; i = dictionaryIndex[i]) { + rev.push(dictionaryChar[i]); + } + return rev; + } + + const result = []; + initDictionary(); + const array = new Uint8Array(input); + let code = getNext(array); + let oldCode; + while (code !== EOI_CODE) { + if (code === CLEAR_CODE) { + initDictionary(); + code = getNext(array); + while (code === CLEAR_CODE) { + code = getNext(array); + } + + if (code === EOI_CODE) { + break; + } else if (code > CLEAR_CODE) { + throw new Error(`corrupted code at scanline ${code}`); + } else { + const val = getDictionaryReversed(code); + appendReversed(result, val); + oldCode = code; + } + } else if (code < dictionaryLength) { + const val = getDictionaryReversed(code); + appendReversed(result, val); + addToDictionary(oldCode, val[val.length - 1]); + oldCode = code; + } else { + const oldVal = getDictionaryReversed(oldCode); + if (!oldVal) { + throw new Error( + `Bogus entry. Not in dictionary, ${oldCode} / ${dictionaryLength}, position: ${position}`, + ); + } + appendReversed(result, oldVal); + result.push(oldVal[oldVal.length - 1]); + addToDictionary(oldCode, oldVal[oldVal.length - 1]); + oldCode = code; + } + + if (dictionaryLength + 1 >= 2 ** byteLength) { + if (byteLength === MAX_BYTELENGTH) { + oldCode = undefined; + } else { + byteLength++; + } + } + code = getNext(array); + } + return new Uint8Array(result); +} + +/** + * @param buffer + */ +export default function decodeBlock(buffer: ArrayBufferLike): ArrayBufferLike { + return decompress(buffer).buffer; +} diff --git a/src/util/polyfills/dataview.ts b/src/util/polyfills/dataview.ts new file mode 100644 index 00000000..85ba5ef2 --- /dev/null +++ b/src/util/polyfills/dataview.ts @@ -0,0 +1,84 @@ +// DataView Utility functions for float32 to float16 conversion and vice versa +export {}; + +declare global { + /** Extend the DataView interface to include getFloat16 and setFloat16 */ + interface DataView { + getFloat16(byteOffset: number, littleEndian?: boolean): number; + setFloat16(byteOffset: number, value: number, littleEndian?: boolean): void; + } +} + +/** + * @param value + */ +function float32ToFloat16(value: number): number { + const floatView = new Float32Array(1); + const int32View = new Uint32Array(floatView.buffer); + floatView[0] = value; + + const sign = (int32View[0] >> 16) & 0x8000; + const exponent = ((int32View[0] >> 23) & 0xff) - 112; + let fraction = (int32View[0] >> 13) & 0x03ff; + + if (exponent <= 0) { + // Subnormal numbers or zero + if (exponent < -10) return sign; // Underflow to zero + fraction = (fraction | 0x0400) >> (1 - exponent); + return sign | fraction; + } else if (exponent === 143) { + // NaN or Infinity + return sign | 0x7c00 | (fraction ? 1 : 0); + } + + // Normalized number + return sign | (exponent << 10) | fraction; +} + +/** + * @param hbits + */ +function float16ToFloat32(hbits: number): number { + const s = (hbits & 0x8000) >> 15; + const e = (hbits & 0x7c00) >> 10; + const f = hbits & 0x03ff; + + if (e === 0) { + // Subnormal number + return (s ? -1 : 1) * Math.pow(2, -14) * (f / Math.pow(2, 10)); + } else if (e === 31) { + // NaN or Infinity + return f ? NaN : (s ? -1 : 1) * Infinity; + } + + // Normalized number + return (s ? -1 : 1) * Math.pow(2, e - 15) * (1 + f / Math.pow(2, 10)); +} + +// Polyfill for DataView.getFloat16 and DataView.setFloat16 +if (!('getFloat16' in DataView.prototype)) { + /** + * @param byteOffset + * @param littleEndian + */ + DataView.prototype.getFloat16 = function (byteOffset: number, littleEndian = false): number { + const value = this.getUint16(byteOffset, littleEndian); + return float16ToFloat32(value); + }; +} + +if (!('setFloat16' in DataView.prototype)) { + /** + * @param byteOffset + * @param value + * @param littleEndian + */ + DataView.prototype.setFloat16 = function ( + byteOffset: number, + value: number, + littleEndian = false, + ): void { + const float16Value = float32ToFloat16(value); + this.setUint16(byteOffset, float16Value, littleEndian); + }; +} diff --git a/src/writers/file.ts b/src/writers/file.ts new file mode 100644 index 00000000..3390a2a4 --- /dev/null +++ b/src/writers/file.ts @@ -0,0 +1,54 @@ +import { createWriteStream } from 'fs'; +import { open } from 'fs/promises'; + +import type { Writable } from 'stream'; // Needed for type annotation if desired +import type { Writer } from '.'; + +/** The File writer is to be used by bun/node/deno on the local filesystem. */ +export default class FileWriter implements Writer { + #stream: Writable; + #textEncoder = new TextEncoder(); + + /** @param file - the location of the PMTiles data in the FS */ + constructor(readonly file: string) { + this.#stream = createWriteStream(file, { flags: 'a+' }); // Open with append mode and create stream + } + + /** + * @param data - the data to write + * @param offset - where in the buffer to start + */ + async write(data: Uint8Array, offset: number): Promise { + const fd = await open(this.file, 'r+'); // Open file for reading and writing + try { + await fd.write(data, 0, data.length, offset); // Write at the specified offset + } finally { + await fd.close(); // Close the file after writing + } + } + + /** @param data - the data to append */ + async append(data: Uint8Array): Promise { + return new Promise((resolve, reject) => { + this.#stream.write(data, (err) => { + if (err) reject(err); + else resolve(); + }); + }); + } + + /** @param string - the string to append */ + async appendString(string: string): Promise { + await this.append(this.#textEncoder.encode(string)); + } + + /** @param data - the data to append */ + appendSync(data: Uint8Array): void { + this.#stream.write(data); // Write data synchronously + } + + /** @param string - the string to append */ + appendStringSync(string: string): void { + this.appendSync(this.#textEncoder.encode(string)); + } +} diff --git a/src/writers/index.ts b/src/writers/index.ts new file mode 100644 index 00000000..b5301321 --- /dev/null +++ b/src/writers/index.ts @@ -0,0 +1,61 @@ +import type { Face, Metadata } from 's2-tilejson'; + +export * from './pmtiles'; +export * from './file'; + +/** The defacto interface for all writers. */ +export interface Writer { + write(data: Uint8Array, offset: number): Promise; + append(data: Uint8Array): Promise; + appendSync(data: Uint8Array): void; + appendString(string: string): Promise; + appendStringSync(string: string): void; +} + +/** A base interface for all tile stores. */ +export interface TileWriter { + writeTileXYZ(zoom: number, x: number, y: number, data: Uint8Array): Promise; + writeTileS2(face: Face, zoom: number, x: number, y: number, data: Uint8Array): Promise; + commit(metadata: Metadata): Promise; +} + +/** Buffer writer is used on smaller datasets that are easy to write in memory. Faster then the Filesystem */ +export class BufferWriter { + #buffer: number[] = []; + #textEncoder = new TextEncoder(); + + /** @param data - the data to append */ + async append(data: Uint8Array): Promise { + for (let i = 0; i < data.byteLength; i++) this.#buffer.push(data[i]); + } + + /** @param string - the string to append */ + async appendString(string: string): Promise { + await this.append(this.#textEncoder.encode(string)); + } + + /** @param data - the data to append */ + appendSync(data: Uint8Array): void { + for (let i = 0; i < data.byteLength; i++) this.#buffer.push(data[i]); + } + + /** @param string - the string to append */ + appendStringSync(string: string): void { + this.appendSync(this.#textEncoder.encode(string)); + } + + /** + * @param data - the data to write + * @param offset - where in the buffer to start + */ + async write(data: Uint8Array, offset: number): Promise { + for (let i = 0; i < data.byteLength; i++) { + this.#buffer[offset + i] = data[i]; + } + } + + /** @returns - the buffer */ + commit(): Uint8Array { + return new Uint8Array(this.#buffer); + } +} diff --git a/src/writers/pmtiles/index.ts b/src/writers/pmtiles/index.ts new file mode 100644 index 00000000..2fed983d --- /dev/null +++ b/src/writers/pmtiles/index.ts @@ -0,0 +1,5 @@ +export * from './writer'; +export * from './pmtiles'; +export * from './s2pmtiles'; +export * from './varint'; +export * from './writer'; diff --git a/src/writers/pmtiles/pmtiles.ts b/src/writers/pmtiles/pmtiles.ts new file mode 100644 index 00000000..1dc0a131 --- /dev/null +++ b/src/writers/pmtiles/pmtiles.ts @@ -0,0 +1,211 @@ +import { writeVarint } from './varint'; + +import type { Point } from 's2-tools/geometry'; + +/** PMTiles v3 directory entry. */ +export interface Entry { + tileID: number; + offset: number; + length: number; + runLength: number; +} + +/** + * Enum representing a compression algorithm used. + * 0 = unknown compression, for if you must use a different or unspecified algorithm. + * 1 = no compression. + * 2 = gzip + * 3 = brotli + * 4 = zstd + */ +export enum Compression { + /** unknown compression, for if you must use a different or unspecified algorithm. */ + Unknown = 0, + /** no compression. */ + None = 1, + /** gzip. */ + Gzip = 2, + /** brotli. */ + Brotli = 3, + /** zstd. */ + Zstd = 4, +} + +/** + * Describe the type of tiles stored in the archive. + * 0 is unknown/other, 1 is "MVT" vector tiles. + */ +export enum TileType { + /** unknown/other. */ + Unknown = 0, + /** Vector tiles. */ + Pbf = 1, + /** Image tiles. */ + Png = 2, + /** Image tiles. */ + Jpeg = 3, + /** Image tiles. */ + Webp = 4, + /** Image tiles. */ + Avif = 5, +} + +/** + * PMTiles v3 header storing basic archive-level information. + */ +export interface Header { + specVersion: number; + rootDirectoryOffset: number; + rootDirectoryLength: number; + jsonMetadataOffset: number; + jsonMetadataLength: number; + leafDirectoryOffset: number; + leafDirectoryLength?: number; + tileDataOffset: number; + tileDataLength?: number; + numAddressedTiles: number; + numTileEntries: number; + numTileContents: number; + clustered: boolean; + internalCompression: Compression; + tileCompression: Compression; + tileType: TileType; + minZoom: number; + maxZoom: number; + etag?: string; +} + +export const HEADER_SIZE_BYTES = 127; + +export const ROOT_SIZE = 16_384; + +/** + * @param n - the rotation size + * @param xy - the point + * @param rx - the x rotation + * @param ry - the y rotation + */ +function rotate(n: number, xy: Point, rx: number, ry: number): void { + if (ry === 0) { + if (rx === 1) { + xy[0] = n - 1 - xy[0]; + xy[1] = n - 1 - xy[1]; + } + const t = xy[0]; + xy[0] = xy[1]; + xy[1] = t; + } +} + +const tzValues: number[] = [ + 0, 1, 5, 21, 85, 341, 1365, 5461, 21845, 87381, 349525, 1398101, 5592405, 22369621, 89478485, + 357913941, 1431655765, 5726623061, 22906492245, 91625968981, 366503875925, 1466015503701, + 5864062014805, 23456248059221, 93824992236885, 375299968947541, 1501199875790165, +]; + +/** + * Convert Z,X,Y to a Hilbert TileID. + * @param zoom - the zoom level + * @param x - the x coordinate + * @param y - the y coordinate + * @returns - the Hilbert encoded TileID + */ +export function zxyToTileID(zoom: number, x: number, y: number): number { + if (zoom > 26) { + throw Error('Tile zoom level exceeds max safe number limit (26)'); + } + if (x > 2 ** zoom - 1 || y > 2 ** zoom - 1) { + throw Error('tile x/y outside zoom level bounds'); + } + + const acc = tzValues[zoom]; + const n = 2 ** zoom; + let rx = 0; + let ry = 0; + let d = 0; + const xy: [x: number, y: number] = [x, y]; + let s = n / 2; + while (true) { + rx = (xy[0] & s) > 0 ? 1 : 0; + ry = (xy[1] & s) > 0 ? 1 : 0; + d += s * s * ((3 * rx) ^ ry); + rotate(s, xy, rx, ry); + if (s <= 1) break; + s = s / 2; + } + return acc + d; +} + +/** + * @param header - the header object + * @returns the raw header bytes + */ +export function headerToBytes(header: Header): Uint8Array { + const dv = new DataView(new ArrayBuffer(HEADER_SIZE_BYTES)); + dv.setUint16(0, 0x4d50, true); + dv.setUint8(7, header.specVersion); + setUint64(dv, 8, header.rootDirectoryOffset); + setUint64(dv, 16, header.rootDirectoryLength); + setUint64(dv, 24, header.jsonMetadataOffset); + setUint64(dv, 32, header.jsonMetadataLength); + setUint64(dv, 40, header.leafDirectoryOffset); + setUint64(dv, 48, header.leafDirectoryLength ?? 0); + setUint64(dv, 56, header.tileDataOffset); + setUint64(dv, 64, header.tileDataLength ?? 0); + setUint64(dv, 72, header.numAddressedTiles); + setUint64(dv, 80, header.numTileEntries); + setUint64(dv, 88, header.numTileContents); + dv.setUint8(96, header.clustered ? 1 : 0); + dv.setUint8(97, header.internalCompression); + dv.setUint8(98, header.tileCompression); + dv.setUint8(99, header.tileType); + dv.setUint8(100, header.minZoom); + dv.setUint8(101, header.maxZoom); + + return new Uint8Array(dv.buffer, dv.byteOffset, dv.byteLength); +} + +/** + * @param entries - the directory entries + * @returns - the serialized directory + */ +export function serializeDir(entries: Entry[]): Uint8Array { + const data = { buf: new Uint8Array(0), pos: 0 }; + + writeVarint(entries.length, data); + + let lastID = 0; + for (let i = 0; i < entries.length; i++) { + const diff = entries[i].tileID - lastID; + writeVarint(diff, data); + lastID = entries[i].tileID; + } + + for (let i = 0; i < entries.length; i++) writeVarint(entries[i].runLength, data); + for (let i = 0; i < entries.length; i++) writeVarint(entries[i].length, data); + for (let i = 0; i < entries.length; i++) { + if (i > 0 && entries[i].offset == entries[i - 1].offset + entries[i - 1].length) { + writeVarint(0, data); + } else { + writeVarint(entries[i].offset + 1, data); + } + } + + return new Uint8Array(data.buf.buffer, data.buf.byteOffset, data.pos); +} + +/** + * Take a large 64-bit number and encode it into a DataView + * @param dv - a DataView + * @param offset - the offset in the DataView + * @param value - the encoded 64-bit number + */ +export function setUint64(dv: DataView, offset: number, value: number): void { + // dv.setUint32(offset + 4, value % 2 ** 32, true); + // dv.setUint32(offset, Math.floor(value / 2 ** 32), true); + const wh = Math.floor(value / 2 ** 32); + const wl = value >>> 0; // Keep the lower 32 bits as an unsigned 32-bit integer + + dv.setUint32(offset, wl, true); // Set the lower 32 bits + dv.setUint32(offset + 4, wh, true); // Set the upper 32 bits +} diff --git a/src/writers/pmtiles/s2pmtiles.ts b/src/writers/pmtiles/s2pmtiles.ts new file mode 100644 index 00000000..28532946 --- /dev/null +++ b/src/writers/pmtiles/s2pmtiles.ts @@ -0,0 +1,79 @@ +import { headerToBytes, setUint64 } from './pmtiles'; + +import type { Entry, Header } from './pmtiles'; + +/** Store entries for each Face */ +export interface S2Entries { + 0: Entry[]; + 1: Entry[]; + 2: Entry[]; + 3: Entry[]; + 4: Entry[]; + 5: Entry[]; +} + +/** S2PMTiles v3 header storing basic archive-level information. */ +export interface S2Header extends Header { + rootDirectoryOffset1: number; + rootDirectoryLength1: number; + rootDirectoryOffset2: number; + rootDirectoryLength2: number; + rootDirectoryOffset3: number; + rootDirectoryLength3: number; + rootDirectoryOffset4: number; + rootDirectoryLength4: number; + rootDirectoryOffset5: number; + rootDirectoryLength5: number; + leafDirectoryOffset1: number; + leafDirectoryLength1: number; + leafDirectoryOffset2: number; + leafDirectoryLength2: number; + leafDirectoryOffset3: number; + leafDirectoryLength3: number; + leafDirectoryOffset4: number; + leafDirectoryLength4: number; + leafDirectoryOffset5: number; + leafDirectoryLength5: number; +} + +export const S2_HEADER_SIZE_BYTES = 262; + +export const S2_ROOT_SIZE = 98_304; + +/** + * @param header - the header object + * @returns the raw header bytes + */ +export function s2HeaderToBytes(header: S2Header): Uint8Array { + const defaultHeader: Uint8Array = headerToBytes(header); + const base = new Uint8Array(S2_HEADER_SIZE_BYTES); + base.set(defaultHeader, 0); + const dv = new DataView(base.buffer); + // re-write the magic number and spec version + dv.setUint8(0, 'S'.charCodeAt(0)); + dv.setUint8(1, '2'.charCodeAt(0)); + dv.setUint8(7, 1); + // now add the rest of the header + setUint64(dv, 102, header.rootDirectoryOffset1); + setUint64(dv, 110, header.rootDirectoryLength1); + setUint64(dv, 118, header.rootDirectoryOffset2); + setUint64(dv, 126, header.rootDirectoryLength2); + setUint64(dv, 134, header.rootDirectoryOffset3); + setUint64(dv, 142, header.rootDirectoryLength3); + setUint64(dv, 150, header.rootDirectoryOffset4); + setUint64(dv, 158, header.rootDirectoryLength4); + setUint64(dv, 166, header.rootDirectoryOffset5); + setUint64(dv, 174, header.rootDirectoryLength5); + setUint64(dv, 182, header.leafDirectoryOffset1); + setUint64(dv, 190, header.leafDirectoryLength1); + setUint64(dv, 198, header.leafDirectoryOffset2); + setUint64(dv, 206, header.leafDirectoryLength2); + setUint64(dv, 214, header.leafDirectoryOffset3); + setUint64(dv, 222, header.leafDirectoryLength3); + setUint64(dv, 230, header.leafDirectoryOffset4); + setUint64(dv, 238, header.leafDirectoryLength4); + setUint64(dv, 246, header.leafDirectoryOffset5); + setUint64(dv, 254, header.leafDirectoryLength5); + + return base; +} diff --git a/src/writers/pmtiles/varint.ts b/src/writers/pmtiles/varint.ts new file mode 100644 index 00000000..6c1375f6 --- /dev/null +++ b/src/writers/pmtiles/varint.ts @@ -0,0 +1,111 @@ +import type { VarintBufPos } from 's2-tools/readers/pmtiles'; + +/** + * Write a varint. Can be max 64-bits. Numbers are coerced to an unsigned + * while number before using this function. + * @param val - any whole unsigned number. + * @param bufPos - the buffer with it's position to write at + */ +export function writeVarint(val: number, bufPos: VarintBufPos): void { + if (val > 0xfffffff || val < 0) { + writeBigVarint(val, bufPos); + return; + } + + realloc(bufPos, 4); + + bufPos.buf[bufPos.pos++] = (val & 0x7f) | (val > 0x7f ? 0x80 : 0); + if (val <= 0x7f) return; + bufPos.buf[bufPos.pos++] = ((val >>>= 7) & 0x7f) | (val > 0x7f ? 0x80 : 0); + if (val <= 0x7f) return; + bufPos.buf[bufPos.pos++] = ((val >>>= 7) & 0x7f) | (val > 0x7f ? 0x80 : 0); + if (val <= 0x7f) return; + bufPos.buf[bufPos.pos++] = (val >>> 7) & 0x7f; +} + +/** + * Write a varint larger then 54-bits. + * @param val - the number + * @param bufPos - the buffer with it's position to write at + */ +export function writeBigVarint(val: number, bufPos: VarintBufPos): void { + let low = val % 0x100000000 | 0; + let high = (val / 0x100000000) | 0; + + if (val < 0) { + low = ~(-val % 0x100000000); + high = ~(-val / 0x100000000); + + if ((low ^ 0xffffffff) !== 0) { + low = (low + 1) | 0; + } else { + low = 0; + high = (high + 1) | 0; + } + } + + if (val >= 0x10000000000000000n || val < -0x10000000000000000n) { + throw new Error("Given varint doesn't fit into 10 bytes"); + } + + realloc(bufPos, 10); + + writeBigVarintLow(low, high, bufPos); + writeBigVarintHigh(high, bufPos); +} + +/** + * Write a varint larger then 54-bits on the low end + * @param low - lower 32 bits + * @param _high - unused "high" bits + * @param bufPos - the buffer with it's position to write at + */ +export function writeBigVarintLow(low: number, _high: number, bufPos: VarintBufPos): void { + bufPos.buf[bufPos.pos++] = (low & 0x7f) | 0x80; + low >>>= 7; + bufPos.buf[bufPos.pos++] = (low & 0x7f) | 0x80; + low >>>= 7; + bufPos.buf[bufPos.pos++] = (low & 0x7f) | 0x80; + low >>>= 7; + bufPos.buf[bufPos.pos++] = (low & 0x7f) | 0x80; + low >>>= 7; + bufPos.buf[bufPos.pos] = low & 0x7f; +} + +/** + * Write a varint larger then 54-bits on the high end + * @param high - the high 32 bits + * @param bufPos - the buffer with it's position to write at + */ +export function writeBigVarintHigh(high: number, bufPos: VarintBufPos): void { + const lsb = (high & 0x07) << 4; + + bufPos.buf[bufPos.pos++] |= lsb | ((high >>>= 3) !== 0 ? 0x80 : 0); + if (high === 0) return; + bufPos.buf[bufPos.pos++] = (high & 0x7f) | ((high >>>= 7) !== 0 ? 0x80 : 0); + if (high === 0) return; + bufPos.buf[bufPos.pos++] = (high & 0x7f) | ((high >>>= 7) !== 0 ? 0x80 : 0); + if (high === 0) return; + bufPos.buf[bufPos.pos++] = (high & 0x7f) | ((high >>>= 7) !== 0 ? 0x80 : 0); + if (high === 0) return; + bufPos.buf[bufPos.pos++] = (high & 0x7f) | ((high >>>= 7) !== 0 ? 0x80 : 0); + if (high === 0) return; + bufPos.buf[bufPos.pos++] = high & 0x7f; +} + +/** + * Allocate more space in the buffer + * @param bufPos - the buffer with it's position + * @param min - the minimum number of bytes to allocate + */ +function realloc(bufPos: VarintBufPos, min: number): void { + let length = bufPos.buf.length > 0 ? bufPos.buf.length : 16; + + while (length < bufPos.pos + min) length *= 2; + + if (length !== bufPos.buf.length) { + const buf = new Uint8Array(length); + buf.set(bufPos.buf); + bufPos.buf = buf; + } +} diff --git a/src/writers/pmtiles/writer.ts b/src/writers/pmtiles/writer.ts new file mode 100644 index 00000000..0632e962 --- /dev/null +++ b/src/writers/pmtiles/writer.ts @@ -0,0 +1,417 @@ +import { concatUint8Arrays } from '../../util'; +import { Compression, ROOT_SIZE, headerToBytes, serializeDir, zxyToTileID } from './pmtiles'; +import { S2_HEADER_SIZE_BYTES, S2_ROOT_SIZE, s2HeaderToBytes } from './s2pmtiles'; + +import type { Entry, Header, TileType } from './pmtiles'; +import type { Face, Metadata } from 's2-tilejson'; +import type { S2Entries, S2Header } from './s2pmtiles'; +import type { TileWriter, Writer } from '..'; + +/** Write a PMTiles file. */ +export class S2PMTilesWriter implements TileWriter { + #tileEntries: Entry[] = []; + #s2tileEntries: S2Entries = { 0: [], 1: [], 2: [], 3: [], 4: [], 5: [] }; + #offset = 0; + #addressedTiles = 0; + #clustered = true; + #minZoom = 30; + #maxZoom = 0; + /** + * @param writer - the writer to append to + * @param type - the tile type + * @param compression - the compression algorithm + */ + constructor( + readonly writer: Writer, + readonly type: TileType, + readonly compression: Compression = Compression.Gzip, + ) { + this.writer.appendSync(new Uint8Array(S2_ROOT_SIZE)); + } + + /** + * Write a tile to the PMTiles file given its (z, x, y) coordinates. + * @param zoom - the zoom level + * @param x - the tile X coordinate + * @param y - the tile Y coordinate + * @param data - the tile data to store + */ + async writeTileXYZ(zoom: number, x: number, y: number, data: Uint8Array): Promise { + this.#minZoom = Math.min(this.#minZoom, zoom); + this.#maxZoom = Math.max(this.#maxZoom, zoom); + const tileID = zxyToTileID(zoom, x, y); + await this.writeTile(tileID, data); + } + + /** + * Write a tile to the PMTiles file given its (face, zoom, x, y) coordinates. + * @param face - the Open S2 projection face + * @param zoom - the zoom level + * @param x - the tile X coordinate + * @param y - the tile Y coordinate + * @param data - the tile data to store + */ + async writeTileS2( + face: Face, + zoom: number, + x: number, + y: number, + data: Uint8Array, + ): Promise { + this.#minZoom = Math.min(this.#minZoom, zoom); + this.#maxZoom = Math.max(this.#maxZoom, zoom); + const tileID = zxyToTileID(zoom, x, y); + await this.writeTile(tileID, data, face); + } + + /** + * Write a tile to the PMTiles file given its tile ID. + * @param tileID - the tile ID + * @param data - the tile data + * @param face - If it exists, then we are storing S2 data + */ + async writeTile(tileID: number, data: Uint8Array, face?: Face): Promise { + data = await compress(data, this.compression); + const length = data.length; + const tileEntries = face !== undefined ? this.#s2tileEntries[face] : this.#tileEntries; + if (tileEntries.length > 0 && tileID < (tileEntries.at(-1) as Entry).tileID) { + this.#clustered = false; + } + + const offset = this.#offset; + await this.writer.append(data); + tileEntries.push({ tileID, offset, length, runLength: 1 }); + this.#offset += length; + + this.#addressedTiles++; + } + + /** + * Finish writing by building the header with root and leaf directories + * @param metadata - the metadata to store + */ + async commit(metadata: Metadata): Promise { + if (this.#tileEntries.length === 0) await this.#commitS2(metadata); + else await this.#commit(metadata); + } + + /** + * Finish writing by building the header with root and leaf directories + * @param metadata - the metadata to store + */ + async #commit(metadata: Metadata): Promise { + const tileEntries = this.#tileEntries; + // keep tile entries sorted + tileEntries.sort((a, b) => a.tileID - b.tileID); + // build metadata + const metaBuffer = Buffer.from(JSON.stringify(metadata)); + let metauint8 = new Uint8Array(metaBuffer.buffer, metaBuffer.byteOffset, metaBuffer.byteLength); + metauint8 = await compress(metauint8, this.compression); + + // optimize directories + const { rootBytes, leavesBytes } = await optimizeDirectories( + tileEntries, + ROOT_SIZE - S2_HEADER_SIZE_BYTES - metauint8.byteLength, + this.compression, + ); + + // build header data + const rootDirectoryOffset = S2_HEADER_SIZE_BYTES; + const rootDirectoryLength = rootBytes.byteLength; + const jsonMetadataOffset = rootDirectoryOffset + rootDirectoryLength; + const jsonMetadataLength = metauint8.byteLength; + const leafDirectoryOffset = this.#offset + S2_ROOT_SIZE; + const leafDirectoryLength = leavesBytes.byteLength; + this.#offset += leavesBytes.byteLength; + await this.writer.append(leavesBytes); + + // build header + const header: Header = { + specVersion: 3, + rootDirectoryOffset, + rootDirectoryLength, + jsonMetadataOffset, + jsonMetadataLength, + leafDirectoryOffset, + leafDirectoryLength, + tileDataOffset: S2_ROOT_SIZE, + tileDataLength: this.#offset, + numAddressedTiles: this.#addressedTiles, + numTileEntries: tileEntries.length, + numTileContents: tileEntries.length, + clustered: this.#clustered, + internalCompression: this.compression, + tileCompression: this.compression, + tileType: this.type, + minZoom: this.#minZoom, + maxZoom: this.#maxZoom, + }; + const serialzedHeader = headerToBytes(header); + + // write header + await this.writer.write(serialzedHeader, 0); + await this.writer.write(rootBytes, rootDirectoryOffset); + await this.writer.write(metauint8, jsonMetadataOffset); + } + + /** + * Finish writing by building the header with root and leaf directories + * @param metadata - the metadata to store + */ + async #commitS2(metadata: Metadata): Promise { + const { compression } = this; + const tileEntries = this.#s2tileEntries[0]; + const tileEntries1 = this.#s2tileEntries[1]; + const tileEntries2 = this.#s2tileEntries[2]; + const tileEntries3 = this.#s2tileEntries[3]; + const tileEntries4 = this.#s2tileEntries[4]; + const tileEntries5 = this.#s2tileEntries[5]; + // keep tile entries sorted + tileEntries.sort((a, b) => a.tileID - b.tileID); + tileEntries1.sort((a, b) => a.tileID - b.tileID); + tileEntries2.sort((a, b) => a.tileID - b.tileID); + tileEntries3.sort((a, b) => a.tileID - b.tileID); + tileEntries4.sort((a, b) => a.tileID - b.tileID); + tileEntries5.sort((a, b) => a.tileID - b.tileID); + // build metadata + const metaBuffer = Buffer.from(JSON.stringify(metadata)); + let metauint8 = new Uint8Array(metaBuffer.buffer, metaBuffer.byteOffset, metaBuffer.byteLength); + metauint8 = await compress(metauint8, this.compression); + + // optimize directories + const { rootBytes, leavesBytes } = await optimizeDirectories( + tileEntries, + ROOT_SIZE - S2_HEADER_SIZE_BYTES - metauint8.byteLength, + compression, + ); + const { rootBytes: rootBytes1, leavesBytes: leavesBytes1 } = await optimizeDirectories( + tileEntries1, + ROOT_SIZE - S2_HEADER_SIZE_BYTES - metauint8.byteLength, + compression, + ); + const { rootBytes: rootBytes2, leavesBytes: leavesBytes2 } = await optimizeDirectories( + tileEntries2, + ROOT_SIZE - S2_HEADER_SIZE_BYTES - metauint8.byteLength, + compression, + ); + const { rootBytes: rootBytes3, leavesBytes: leavesBytes3 } = await optimizeDirectories( + tileEntries3, + ROOT_SIZE - S2_HEADER_SIZE_BYTES - metauint8.byteLength, + compression, + ); + const { rootBytes: rootBytes4, leavesBytes: leavesBytes4 } = await optimizeDirectories( + tileEntries4, + ROOT_SIZE - S2_HEADER_SIZE_BYTES - metauint8.byteLength, + compression, + ); + const { rootBytes: rootBytes5, leavesBytes: leavesBytes5 } = await optimizeDirectories( + tileEntries5, + ROOT_SIZE - S2_HEADER_SIZE_BYTES - metauint8.byteLength, + compression, + ); + + // build header data + const rootDirectoryOffset = S2_HEADER_SIZE_BYTES; + const rootDirectoryLength = rootBytes.byteLength; + const rootDirectoryOffset1 = rootDirectoryOffset + rootDirectoryLength; + const rootDirectoryLength1 = rootBytes1.byteLength; + const rootDirectoryOffset2 = rootDirectoryOffset1 + rootDirectoryLength1; + const rootDirectoryLength2 = rootBytes2.byteLength; + const rootDirectoryOffset3 = rootDirectoryOffset2 + rootDirectoryLength2; + const rootDirectoryLength3 = rootBytes3.byteLength; + const rootDirectoryOffset4 = rootDirectoryOffset3 + rootDirectoryLength3; + const rootDirectoryLength4 = rootBytes4.byteLength; + const rootDirectoryOffset5 = rootDirectoryOffset4 + rootDirectoryLength4; + const rootDirectoryLength5 = rootBytes5.byteLength; + // metadata + const jsonMetadataOffset = rootDirectoryOffset5 + rootDirectoryLength5; + const jsonMetadataLength = metauint8.byteLength; + // leafs + const leafDirectoryOffset = this.#offset + S2_ROOT_SIZE; + const leafDirectoryLength = leavesBytes.byteLength; + this.#offset += leafDirectoryLength; + await this.writer.append(leavesBytes); + const leafDirectoryOffset1 = this.#offset + S2_ROOT_SIZE; + const leafDirectoryLength1 = leavesBytes1.byteLength; + this.#offset += leafDirectoryLength1; + await this.writer.append(leavesBytes1); + const leafDirectoryOffset2 = this.#offset + S2_ROOT_SIZE; + const leafDirectoryLength2 = leavesBytes2.byteLength; + this.#offset += leafDirectoryLength2; + await this.writer.append(leavesBytes2); + const leafDirectoryOffset3 = this.#offset + S2_ROOT_SIZE; + const leafDirectoryLength3 = leavesBytes3.byteLength; + this.#offset += leafDirectoryLength3; + await this.writer.append(leavesBytes3); + const leafDirectoryOffset4 = this.#offset + S2_ROOT_SIZE; + const leafDirectoryLength4 = leavesBytes4.byteLength; + this.#offset += leafDirectoryLength4; + await this.writer.append(leavesBytes4); + const leafDirectoryOffset5 = this.#offset + S2_ROOT_SIZE; + const leafDirectoryLength5 = leavesBytes5.byteLength; + this.#offset += leafDirectoryLength5; + await this.writer.append(leavesBytes5); + // build header + const header: S2Header = { + specVersion: 3, + rootDirectoryOffset, + rootDirectoryLength, + rootDirectoryOffset1, + rootDirectoryLength1, + rootDirectoryOffset2, + rootDirectoryLength2, + rootDirectoryOffset3, + rootDirectoryLength3, + rootDirectoryOffset4, + rootDirectoryLength4, + rootDirectoryOffset5, + rootDirectoryLength5, + jsonMetadataOffset, + jsonMetadataLength, + leafDirectoryOffset, + leafDirectoryLength, + leafDirectoryOffset1, + leafDirectoryLength1, + leafDirectoryOffset2, + leafDirectoryLength2, + leafDirectoryOffset3, + leafDirectoryLength3, + leafDirectoryOffset4, + leafDirectoryLength4, + leafDirectoryOffset5, + leafDirectoryLength5, + tileDataOffset: S2_ROOT_SIZE, + tileDataLength: this.#offset, + numAddressedTiles: this.#addressedTiles, + numTileEntries: tileEntries.length, + numTileContents: tileEntries.length, + clustered: this.#clustered, + internalCompression: this.compression, + tileCompression: this.compression, + tileType: this.type, + minZoom: this.#minZoom, + maxZoom: this.#maxZoom, + }; + const serialzedHeader = s2HeaderToBytes(header); + + // write header + await this.writer.write(serialzedHeader, 0); + await this.writer.write(rootBytes, rootDirectoryOffset); + await this.writer.write(rootBytes1, rootDirectoryOffset1); + await this.writer.write(rootBytes2, rootDirectoryOffset2); + await this.writer.write(rootBytes3, rootDirectoryOffset3); + await this.writer.write(rootBytes4, rootDirectoryOffset4); + await this.writer.write(rootBytes5, rootDirectoryOffset5); + await this.writer.write(metauint8, jsonMetadataOffset); + } +} + +/** The result of an optimized directory computation */ +interface OptimizedDirectory { + /** The root directory bytes */ + rootBytes: Uint8Array; + /** The leaf directories bytes */ + leavesBytes: Uint8Array; + /** The number of leaf directories */ + numLeaves: number; +} + +/** + * @param entries - the tile entries + * @param leafSize - the max leaf size + * @param compression - the compression + * @returns - the optimized directories + */ +async function buildRootsLeaves( + entries: Entry[], + leafSize: number, + compression: Compression, +): Promise { + const rootEntries: Entry[] = []; + let leavesBytes = new Uint8Array(0); + let numLeaves = 0; + + let i = 0; + while (i < entries.length) { + numLeaves += 1; + const serialized = await compress(serializeDir(entries.slice(i, i + leafSize)), compression); + rootEntries.push({ + tileID: entries[i].tileID, + offset: leavesBytes.length, + length: serialized.length, + runLength: 0, + }); + leavesBytes = await concatUint8Arrays([leavesBytes, serialized]); + i += leafSize; + } + + return { + rootBytes: await compress(serializeDir(rootEntries), compression), + leavesBytes, + numLeaves, + }; +} + +/** + * @param entries - the tile entries + * @param targetRootLength - the max leaf size + * @param compression - the compression + * @returns - the optimized directories + */ +async function optimizeDirectories( + entries: Entry[], + targetRootLength: number, + compression: Compression, +): Promise { + const testBytes = await compress(serializeDir(entries), compression); + if (testBytes.length < targetRootLength) + return { rootBytes: testBytes, leavesBytes: new Uint8Array(0), numLeaves: 0 }; + + let leafSize = 4096; + while (true) { + const build = await buildRootsLeaves(entries, leafSize, compression); + if (build.rootBytes.length < targetRootLength) return build; + leafSize *= 2; + } +} + +// /** +// * @param a - the first array +// * @param b - the second array +// * @returns - the combined array of the two starting with "a" +// */ +// function concatUint8Arrays(a: Uint8Array, b: Uint8Array): Uint8Array { +// const result = new Uint8Array(a.length + b.length); +// result.set(a, 0); +// result.set(b, a.length); +// return result; +// } + +/** + * @param input - the input Uint8Array + * @param compression - the compression + * @returns - the compressed Uint8Array or the original if compression is None + */ +async function compress(input: Uint8Array, compression: Compression): Promise { + if (compression === Compression.None) return input; + else if (compression === Compression.Gzip) return await compressGzip(input); + else throw new Error(`Unsupported compression: ${compression}`); +} + +/** + * @param input - the input Uint8Array + * @returns - the compressed Uint8Array + */ +async function compressGzip(input: Uint8Array): Promise { + // Convert the string to a byte stream. + const stream = new Blob([input]).stream(); + + // Create a compressed stream. + const compressedStream = stream.pipeThrough(new CompressionStream('gzip')); + + // Read all the bytes from this stream. + const chunks = []; + for await (const chunk of compressedStream) chunks.push(chunk); + + return await concatUint8Arrays(chunks); +} diff --git a/src/writers/tile.ts b/src/writers/tile.ts new file mode 100644 index 00000000..4183e3c0 --- /dev/null +++ b/src/writers/tile.ts @@ -0,0 +1,62 @@ +import { exists, mkdir, writeFile } from 'fs/promises'; + +import type { Metadata } from 's2-tilejson'; +import type { TileWriter } from '.'; + +/** This is a filesystem Tile writer that organizes data via folders. */ +export default class LocalTileWriter implements TileWriter { + /** + * @param path - the location to write the data + * @param fileType - the file ending to write + */ + constructor( + readonly path: string, + readonly fileType: string = 'vector.pbf', + ) { + // check that the folder exists + const folderExists = exists(this.path); + if (!folderExists) throw new Error(`Folder ${this.path} does not exist.`); + } + + /** + * Write a tile to the PMTiles file given its (z, x, y) coordinates. + * @param zoom - the zoom level + * @param x - the tile X coordinate + * @param y - the tile Y coordinate + * @param data - the tile data to store + */ + async writeTileXYZ(zoom: number, x: number, y: number, data: Uint8Array): Promise { + // if folders don't exist, create it + const folders = `${this.path}/${zoom}/${x}`; + if (!exists(folders)) await mkdir(folders, { recursive: true }); + + await writeFile(`${folders}/${y}.${this.fileType}`, data); + } + + /** + * Write a tile to the PMTiles file given its (face, zoom, x, y) coordinates. + * @param face - the Open S2 projection face + * @param zoom - the zoom level + * @param x - the tile X coordinate + * @param y - the tile Y coordinate + * @param data - the tile data to store + */ + async writeTileS2( + face: number, + zoom: number, + x: number, + y: number, + data: Uint8Array, + ): Promise { + // if folders don't exist, create it + const folders = `${this.path}/${face}/${zoom}/${x}`; + if (!exists(folders)) await mkdir(folders, { recursive: true }); + + await writeFile(`${folders}/${y}.${this.fileType}`, data); + } + + /** @param metadata - the metadata about all the tiles to store */ + async commit(metadata: Metadata): Promise { + await writeFile(`${this.path}/metadata.json`, JSON.stringify(metadata)); + } +} diff --git a/tests/converters/fixtures/point-feature.geojson b/tests/converters/fixtures/point-feature.geojson new file mode 100644 index 00000000..12aef4f2 --- /dev/null +++ b/tests/converters/fixtures/point-feature.geojson @@ -0,0 +1 @@ +{"type":"Feature","properties":{"name":"Melbourne"},"geometry":{"type":"Point","coordinates":[144.9584,-37.8173]}} diff --git a/tests/converters/fixtures/points.geojson b/tests/converters/fixtures/points.geojson new file mode 100644 index 00000000..80e61caa --- /dev/null +++ b/tests/converters/fixtures/points.geojson @@ -0,0 +1,8 @@ +{ + "type": "FeatureCollection", + "features": [ + {"type":"Feature","properties":{"name":"Melbourne"},"geometry":{"type":"Point","coordinates":[144.9584,-37.8173]}}, + {"type":"Feature","properties":{"name":"Canberra"},"geometry":{"type":"Point","coordinates":[149.1009,-35.3039]}}, + {"type":"Feature","properties":{"name":"Sydney"},"geometry":{"type":"Point","coordinates":[151.2144,-33.8766]}} + ] +} diff --git a/tests/converters/fixtures/points.geojsonld b/tests/converters/fixtures/points.geojsonld new file mode 100644 index 00000000..7ae14a13 --- /dev/null +++ b/tests/converters/fixtures/points.geojsonld @@ -0,0 +1,3 @@ +{"type":"Feature","properties":{"name":"Melbourne"},"geometry":{"type":"Point","coordinates":[144.9584,-37.8173]}} +{"type":"Feature","properties":{"name":"Canberra"},"geometry":{"type":"Point","coordinates":[149.1009,-35.3039]}} +{"type":"Feature","properties":{"name":"Sydney"},"geometry":{"type":"Point","coordinates":[151.2144,-33.8766]}} diff --git a/tests/converters/toJSON.test.ts b/tests/converters/toJSON.test.ts new file mode 100644 index 00000000..cafc335b --- /dev/null +++ b/tests/converters/toJSON.test.ts @@ -0,0 +1,244 @@ +import { BufferReader } from '../../src/readers'; +import { BufferWriter } from '../../src/writers'; +import FileReader from '../../src/readers/file'; +import { JSONReader, NewLineDelimitedJSONReader } from '../../src/readers/json'; +import { expect, test } from 'bun:test'; +import { toJSON, toJSONLD } from '../../src/converters'; + +import type { VectorFeatures } from '../../src/geometry'; + +test('toJSON', async () => { + const fileReader = new FileReader(`${__dirname}/fixtures/points.geojson`); + const jsonReader = new JSONReader(fileReader); + const bufWriter = new BufferWriter(); + await toJSON(bufWriter, [jsonReader]); + const string = new TextDecoder().decode(bufWriter.commit()); + expect(string).toEqual( + '{\n\t"type": "S2FeatureCollection",\n\t"features": [\n\t\t{"type":"S2Feature","face":3,"properties":{"name":"Melbourne"},"geometry":{"type":"Point","is3D":false,"coordinates":{"x":0.980307055282927,"y":0.1191097721694171},"vecBBox":[0.980307055282927,0.1191097721694171,0.980307055282927,0.1191097721694171]}},\n\t\t{"type":"S2Feature","face":3,"properties":{"name":"Canberra"},"geometry":{"type":"Point","is3D":false,"coordinates":{"x":0.9321761149504832,"y":0.16402766817497416},"vecBBox":[0.9321761149504832,0.16402766817497416,0.9321761149504832,0.16402766817497416]}},\n\t\t{"type":"S2Feature","face":3,"properties":{"name":"Sydney"},"geometry":{"type":"Point","is3D":false,"coordinates":{"x":0.908036698755368,"y":0.18632281680962381},"vecBBox":[0.908036698755368,0.18632281680962381,0.908036698755368,0.18632281680962381]}}\n\t],\n\t"faces": [3]\n}', + ); + expect(JSON.parse(string)).toEqual({ + faces: [3], + features: [ + { + face: 3, + geometry: { + coordinates: { x: 0.980307055282927, y: 0.1191097721694171 }, + is3D: false, + type: 'Point', + vecBBox: [0.980307055282927, 0.1191097721694171, 0.980307055282927, 0.1191097721694171], + }, + properties: { + name: 'Melbourne', + }, + type: 'S2Feature', + }, + { + face: 3, + geometry: { + coordinates: { x: 0.9321761149504832, y: 0.16402766817497416 }, + is3D: false, + type: 'Point', + vecBBox: [ + 0.9321761149504832, 0.16402766817497416, 0.9321761149504832, 0.16402766817497416, + ], + }, + properties: { + name: 'Canberra', + }, + type: 'S2Feature', + }, + { + face: 3, + geometry: { + coordinates: { x: 0.908036698755368, y: 0.18632281680962381 }, + is3D: false, + type: 'Point', + vecBBox: [0.908036698755368, 0.18632281680962381, 0.908036698755368, 0.18632281680962381], + }, + properties: { + name: 'Sydney', + }, + type: 'S2Feature', + }, + ], + type: 'S2FeatureCollection', + }); +}); + +test('toJSON - WM & bbox & onFeature', async () => { + const fileReader = new FileReader(`${__dirname}/fixtures/points.geojson`); + const jsonReader = new JSONReader(fileReader); + const bufWriter = new BufferWriter(); + /** + * @param feature - the feature to modify + * @returns the modified feature + */ + const onFeature = (feature: VectorFeatures): VectorFeatures | undefined => { + if (feature.properties.name === 'Canberra') return; + feature.properties.name = 'Redacted'; + return feature; + }; + await toJSON(bufWriter, [jsonReader], { projection: 'WM', buildBBox: true, onFeature }); + const string = new TextDecoder().decode(bufWriter.commit()); + expect(string).toEqual( + '{\n\t"type": "FeatureCollection",\n\t"features": [\n\t\t{"type":"VectorFeature","properties":{"name":"Redacted"},"geometry":{"type":"Point","is3D":false,"coordinates":{"x":144.9584,"y":-37.8173},"bbox":[144.9584,-37.8173,144.9584,-37.8173]}},\n\t\t{"type":"VectorFeature","properties":{"name":"Redacted"},"geometry":{"type":"Point","is3D":false,"coordinates":{"x":151.2144,"y":-33.8766},"bbox":[151.2144,-33.8766,151.2144,-33.8766]}}\n\t],\n\t"faces": [0],\n\t"bbox": [144.9584,-37.8173,151.2144,-33.8766]\n}', + ); + expect(JSON.parse(string)).toEqual({ + bbox: [144.9584, -37.8173, 151.2144, -33.8766], + faces: [0], + features: [ + { + geometry: { + bbox: [144.9584, -37.8173, 144.9584, -37.8173], + coordinates: { + x: 144.9584, + y: -37.8173, + }, + is3D: false, + type: 'Point', + }, + properties: { + name: 'Redacted', + }, + type: 'VectorFeature', + }, + { + geometry: { + bbox: [151.2144, -33.8766, 151.2144, -33.8766], + coordinates: { + x: 151.2144, + y: -33.8766, + }, + is3D: false, + type: 'Point', + }, + properties: { + name: 'Redacted', + }, + type: 'VectorFeature', + }, + ], + type: 'FeatureCollection', + }); +}); + +test('toJSONLD', async () => { + const fileReader = new FileReader(`${__dirname}/fixtures/points.geojson`); + const jsonReader = new JSONReader(fileReader); + const bufWriter = new BufferWriter(); + await toJSONLD(bufWriter, [jsonReader]); + const string = new TextDecoder().decode(bufWriter.commit()); + expect(string).toEqual( + '{"type":"S2Feature","face":3,"properties":{"name":"Melbourne"},"geometry":{"type":"Point","is3D":false,"coordinates":{"x":0.980307055282927,"y":0.1191097721694171},"vecBBox":[0.980307055282927,0.1191097721694171,0.980307055282927,0.1191097721694171]}}\n{"type":"S2Feature","face":3,"properties":{"name":"Canberra"},"geometry":{"type":"Point","is3D":false,"coordinates":{"x":0.9321761149504832,"y":0.16402766817497416},"vecBBox":[0.9321761149504832,0.16402766817497416,0.9321761149504832,0.16402766817497416]}}\n{"type":"S2Feature","face":3,"properties":{"name":"Sydney"},"geometry":{"type":"Point","is3D":false,"coordinates":{"x":0.908036698755368,"y":0.18632281680962381},"vecBBox":[0.908036698755368,0.18632281680962381,0.908036698755368,0.18632281680962381]}}\n', + ); + + const bufReader = new BufferReader(bufWriter.commit().buffer); + const nlReader = new NewLineDelimitedJSONReader(bufReader); + const data = await Array.fromAsync(nlReader); + expect(data).toEqual([ + { + face: 3, + geometry: { + coordinates: { + x: 0.980307055282927, + y: 0.1191097721694171, + }, + is3D: false, + type: 'Point', + vecBBox: [0.980307055282927, 0.1191097721694171, 0.980307055282927, 0.1191097721694171], + }, + properties: { + name: 'Melbourne', + }, + type: 'S2Feature', + }, + { + face: 3, + geometry: { + coordinates: { + x: 0.9321761149504832, + y: 0.16402766817497416, + }, + is3D: false, + type: 'Point', + vecBBox: [0.9321761149504832, 0.16402766817497416, 0.9321761149504832, 0.16402766817497416], + }, + properties: { + name: 'Canberra', + }, + type: 'S2Feature', + }, + { + face: 3, + geometry: { + coordinates: { + x: 0.908036698755368, + y: 0.18632281680962381, + }, + is3D: false, + type: 'Point', + vecBBox: [0.908036698755368, 0.18632281680962381, 0.908036698755368, 0.18632281680962381], + }, + properties: { + name: 'Sydney', + }, + type: 'S2Feature', + }, + ]); +}); + +test('toJSONLD - WM & bbox & onFeature', async () => { + const fileReader = new FileReader(`${__dirname}/fixtures/points.geojson`); + const jsonReader = new JSONReader(fileReader); + const bufWriter = new BufferWriter(); + /** + * @param feature - the feature to modify + * @returns the modified feature + */ + const onFeature = (feature: VectorFeatures): VectorFeatures | undefined => { + if (feature.properties.name === 'Canberra') return; + feature.properties.name = 'Redacted'; + return feature; + }; + await toJSONLD(bufWriter, [jsonReader], { projection: 'WM', onFeature, buildBBox: true }); + const string = new TextDecoder().decode(bufWriter.commit()); + expect(string).toEqual( + '{"type":"VectorFeature","properties":{"name":"Redacted"},"geometry":{"type":"Point","is3D":false,"coordinates":{"x":144.9584,"y":-37.8173},"bbox":[144.9584,-37.8173,144.9584,-37.8173]}}\n{"type":"VectorFeature","properties":{"name":"Redacted"},"geometry":{"type":"Point","is3D":false,"coordinates":{"x":151.2144,"y":-33.8766},"bbox":[151.2144,-33.8766,151.2144,-33.8766]}}\n', + ); + + const bufReader = new BufferReader(bufWriter.commit().buffer); + const nlReader = new NewLineDelimitedJSONReader(bufReader); + const data = await Array.fromAsync(nlReader); + expect(data).toEqual([ + { + geometry: { + bbox: [144.9584, -37.8173, 144.9584, -37.8173], + coordinates: { + x: 144.9584, + y: -37.8173, + }, + is3D: false, + type: 'Point', + }, + properties: { + name: 'Redacted', + }, + type: 'VectorFeature', + }, + { + geometry: { + bbox: [151.2144, -33.8766, 151.2144, -33.8766], + coordinates: { + x: 151.2144, + y: -33.8766, + }, + is3D: false, + type: 'Point', + }, + properties: { + name: 'Redacted', + }, + type: 'VectorFeature', + }, + ]); +}); diff --git a/tests/dataStructures/cache.test.ts b/tests/dataStructures/cache.test.ts new file mode 100644 index 00000000..d0a2bd57 --- /dev/null +++ b/tests/dataStructures/cache.test.ts @@ -0,0 +1,28 @@ +import { describe, expect, test } from 'bun:test'; + +import DirCache from '../../src/dataStructures/cache'; + +describe('dirCache', () => { + const dirCache = new DirCache(5); + + test('test functionality', () => { + expect(dirCache.set(1, 2)).toBe(dirCache); + expect(dirCache.get(1)).toEqual(2); + expect(dirCache.delete(1)).toBe(true); + }); + + test('test max size', () => { + dirCache.set(1, 2); + dirCache.set(2, 3); + dirCache.set(3, 4); + dirCache.set(4, 5); + dirCache.set(5, 6); + dirCache.set(6, 7); + dirCache.set(7, 8); + dirCache.set(4, 9); + + expect(dirCache.size).toBe(5); + expect(dirCache.get(2)).toEqual(undefined as unknown as number); + expect(dirCache.get(3)).toEqual(4); + }); +}); diff --git a/tests/dataStructures/priorityQueue.test.ts b/tests/dataStructures/priorityQueue.test.ts index 981fe8fc..38980244 100644 --- a/tests/dataStructures/priorityQueue.test.ts +++ b/tests/dataStructures/priorityQueue.test.ts @@ -1,7 +1,7 @@ import PriorityQueue from '../../src/dataStructures/priorityQueue'; import { beforeAll, expect, test } from 'bun:test'; -import type { CompareFunction } from '../../src/dataStructures/priorityQueue'; +import type { PriorityCompare } from '../../src/dataStructures/priorityQueue'; const data: number[] = []; let sorted: number[] = []; @@ -64,7 +64,7 @@ test('handle object type', () => { * @param b - the second element * @returns - comparison result */ - const comparitor: CompareFunction = (a: StructTest, b: StructTest): 0 | 1 | -1 => + const comparitor: PriorityCompare = (a: StructTest, b: StructTest): 0 | 1 | -1 => a.x < b.x ? -1 : a.x > b.x ? 1 : 0; const queue = new PriorityQueue([], comparitor); diff --git a/tests/polyfill.ts b/tests/polyfill.ts index edfd1834..bdbd8824 100644 --- a/tests/polyfill.ts +++ b/tests/polyfill.ts @@ -38,22 +38,22 @@ const make = (ctx, handle) => }), }); -// // @ts-expect-error - polyfill exception -// globalThis.CompressionStream ??= class CompressionStream { -// /** -// * @param format - the format to use -// */ -// constructor(format) { -// make( -// this, -// format === 'deflate' -// ? zlib.createDeflate() -// : format === 'gzip' -// ? zlib.createGzip() -// : zlib.createDeflateRaw(), -// ); -// } -// }; +// @ts-expect-error - polyfill exception +globalThis.CompressionStream ??= class CompressionStream { + /** + * @param format - the format to use + */ + constructor(format) { + make( + this, + format === 'deflate' + ? zlib.createDeflate() + : format === 'gzip' + ? zlib.createGzip() + : zlib.createDeflateRaw(), + ); + } +}; // @ts-expect-error - polyfill exception globalThis.DecompressionStream ??= class DecompressionStream { diff --git a/tests/proj4/fixtures/BETA2007.gsb b/tests/proj4/fixtures/BETA2007.gsb new file mode 100644 index 00000000..69cd3346 Binary files /dev/null and b/tests/proj4/fixtures/BETA2007.gsb differ diff --git a/tests/proj4/index.test.ts b/tests/proj4/index.test.ts new file mode 100644 index 00000000..bdb76495 --- /dev/null +++ b/tests/proj4/index.test.ts @@ -0,0 +1,248 @@ +import { Mercator, Transformer, injectAllDefinitions } from '../../src/proj4'; +import { describe, expect, it } from 'bun:test'; + +import { TEST_DATA } from './testData'; + +// was untested: +// - src/proj4/projections/cea.ts +// - src/proj4/projections/gstmerc.ts +// - src/proj4/projections/ortho.ts +// - src/proj4/projections/somerc.ts + +describe('basic tests', () => { + it('should parse mercator and units', () => { + const transform = new Transformer(); + transform.setSource( + '+proj=merc +a=6378137 +b=6378137 +lat_ts=0.0 +lon_0=0.0 +x_0=0.0 +y_0=0 +units=m +k=1.0 +nadgrids=@null +no_defs', + ); + const testMerc = transform.source; + expect(testMerc).toBeInstanceOf(Mercator); + // @ts-expect-error - we just want to check internal properties + expect(testMerc.units).toBe('m'); + expect(transform.forward({ x: 0, y: 0 })).toEqual({ x: 0, y: 0 }); + expect(transform.forward({ x: 4156404, y: 7480076.5 })).toEqual({ + x: 37.33761240175515, + y: 55.604470490269755, + }); + }); +}); + +describe('proj2proj', () => { + it('should work transforming from one projection to another', () => { + const transform = new Transformer(); + injectAllDefinitions(transform); + const sweref99tm = '+proj=utm +zone=33 +ellps=GRS80 +towgs84=0,0,0,0,0,0,0 +units=m +no_defs'; + const rt90 = + '+lon_0=15.808277777799999 +lat_0=0.0 +k=1.0 +x_0=1500000.0 +y_0=0.0 +proj=tmerc +ellps=bessel +units=m +towgs84=414.1,41.3,603.1,-0.855,2.141,-7.023,0 +no_defs'; + transform.setSource(sweref99tm); + transform.setDestination(rt90); + const rslt = transform.forward({ x: 319180, y: 6399862 }); + expect(rslt).toEqual({ x: 1271137.9275601413, y: 6404230.291459565 }); + }); +}); + +describe('test data', () => { + TEST_DATA.forEach((testPoint, i) => { + it(`${testPoint.code} (${i})`, () => { + const transform = new Transformer(); + injectAllDefinitions(transform); + transform.setSource(testPoint.code); + const { ll, xy, acc } = testPoint; + const to = transform.forward({ x: xy[0], y: xy[1], z: xy[2] }); + expect(to.x).toBeCloseTo(ll[0], acc?.ll ?? -1); + expect(to.y).toBeCloseTo(ll[1], acc?.ll ?? -1); + if ('z' in to) { + expect(to.z).toBeCloseTo(ll[2], acc?.ll ?? -1); + } + const from = transform.inverse({ x: ll[0], y: ll[1], z: ll[2] }); + expect(from.x).toBeCloseTo(xy[0], acc?.xy ?? -1); + expect(from.y).toBeCloseTo(xy[1], acc?.xy ?? -1); + if ('z' in from) { + expect(from.z).toBeCloseTo(xy[2], acc?.xy ?? -1); + } + }); + }); +}); + +describe('axes should be invertable with proj4.transform()', function () { + const enu = '+proj=longlat +axis=enu'; + const esu = '+proj=longlat +axis=esu'; + const wnu = '+proj=longlat +axis=wnu'; + const transform = new Transformer(enu, esu); + injectAllDefinitions(transform); + const result = transform.forward({ x: 40, y: 50 }, true); + expect(result.x).toBeCloseTo(40, 5); + expect(result.y).toBeCloseTo(-50, 5); + transform.setDestination(wnu); + const result2 = transform.forward({ x: 40, y: 50 }, true); + expect(result2.x).toBeCloseTo(-40, 5); + expect(result2.y).toBeCloseTo(50, 5); +}); + +// describe('Nadgrids BETA2007', function () { +// const tests = [ +// ['EPSG:31466', 'EPSG:4326', 2559552, 5670982, 6.850861772, 51.170707759, 0.0000001, 0.01], +// [ +// 'EPSG:31466', +// 'EPSG:3857', +// 2559552, +// 5670982, +// 762634.443931574, +// 6651545.68026527, +// 0.01, +// 0.01, +// ], +// [ +// 'EPSG:31466', +// 'EPSG:25832', +// 2559552, +// 5670982, +// 349757.381712518, +// 5671004.06504954, +// 0.01, +// 0.01, +// ], +// ]; + +// /** +// * @param buffer +// */ +// function initializeNadgrid(buffer) { +// proj4.nadgrid('BETA2007.gsb', buffer); +// proj4.defs( +// 'EPSG:31466', +// '+proj=tmerc +lat_0=0 +lon_0=6 +k=1 +x_0=2500000 +y_0=0 +ellps=bessel +nadgrids=BETA2007.gsb +units=m +no_defs +type=crs', +// ); +// proj4.defs( +// 'EPSG:25832', +// '+proj=utm +zone=32 +ellps=GRS80 +towgs84=0,0,0,0,0,0,0 +units=m +no_defs +type=crs', +// ); +// } + +// before(function (done) { +// if (typeof XMLHttpRequest !== 'undefined') { +// const xhr = new XMLHttpRequest(); +// xhr.open('GET', 'BETA2007.gsb', true); +// xhr.responseType = 'arraybuffer'; +// xhr.addEventListener('load', function () { +// initializeNadgrid(xhr.response); +// done(); +// }); +// xhr.addEventListener('error', done); +// xhr.send(); +// } else if (typeof require === 'function') { +// const fs = require('fs'); +// const path = require('path'); +// fs.readFile(path.join(__dirname, 'BETA2007.gsb'), function (err, data) { +// if (err) { +// done(err); +// } else { +// initializeNadgrid(data.buffer); +// done(); +// } +// }); +// } +// }); + +// tests.forEach(function (test) { +// const fromProj = test[0]; +// const toProj = test[1]; +// const fromX = test[2]; +// const fromY = test[3]; +// const toX = test[4]; +// const toY = test[5]; +// const fromPrecision = test[6]; +// const toPrecision = test[7]; +// it('should transform ' + fromProj + ' to ' + toProj, function () { +// const transformed = proj4(fromProj, toProj, [fromX, fromY]); +// assert.approximately(transformed[0], toX, fromPrecision); +// assert.approximately(transformed[1], toY, fromPrecision); +// }); +// it('should transform ' + toProj + ' to ' + fromProj, function () { +// const transformed = proj4(toProj, fromProj, [toX, toY]); +// assert.approximately(transformed[0], fromX, toPrecision); +// assert.approximately(transformed[1], fromY, toPrecision); +// }); +// }); +// }); + +// describe('Nadgrids ntv2', function () { +// const tests = [ +// [-44.382211538462, 40.3768, -44.380749, 40.377457], // just inside the lower limit +// [-87.617788, 59.623262, -87.617659, 59.623441], // just inside the upper limit +// [-44.5, 40.5, -44.498553, 40.500632], // inside the first square +// [-60, 50, -59.999192, 50.000058], // a general point towards the middle of the grid +// [0, 0, 0, 0], // fall back to null +// ]; + +// let converter; + +// /** +// * @param buffer +// */ +// function initializeNadgrid(buffer) { +// proj4.nadgrid('ntv2', buffer); +// proj4.defs('ntv2_from', '+proj=longlat +ellps=clrk66 +nadgrids=@ignorable,ntv2,null'); +// proj4.defs('ntv2_to', '+proj=longlat +datum=WGS84 +no_defs'); +// converter = proj4('ntv2_from', 'ntv2_to'); +// } + +// before(function (done) { +// if (typeof XMLHttpRequest !== 'undefined') { +// const xhr = new XMLHttpRequest(); +// xhr.open('GET', 'ntv2_0_downsampled.gsb', true); +// xhr.responseType = 'arraybuffer'; +// xhr.addEventListener('load', function () { +// initializeNadgrid(xhr.response); +// done(); +// }); +// xhr.addEventListener('error', done); +// xhr.send(); +// } else if (typeof require === 'function') { +// const fs = require('fs'); +// const path = require('path'); +// fs.readFile(path.join(__dirname, 'ntv2_0_downsampled.gsb'), function (err, data) { +// if (err) { +// done(err); +// } else { +// initializeNadgrid(data.buffer); +// done(); +// } +// }); +// } +// }); + +// tests.forEach(function (test) { +// const fromLng = test[0]; +// const fromLat = test[1]; +// const toLng = test[2]; +// const toLat = test[3]; +// it('should interpolate ' + [fromLng, fromLat] + ' to ' + [toLng, toLat], function () { +// const actual = converter.forward([fromLng, fromLat]); +// assert.approximately(actual[0], toLng, 0.000001); +// assert.approximately(actual[1], toLat, 0.000001); +// }); +// }); + +// const inverseTests = [ +// [-44.5, 40.5, -44.498553, 40.500632], +// [-60, 50, -59.999192, 50.000058], +// ]; + +// inverseTests.forEach(function (test) { +// const fromLng = test[0]; +// const fromLat = test[1]; +// const toLng = test[2]; +// const toLat = test[3]; +// it( +// 'should inverse interpolate ' + [toLng, toLat] + ' to ' + [fromLng, fromLat], +// function () { +// const actual = converter.inverse([toLng, toLat]); +// assert.approximately(actual[0], fromLng, 0.000001); +// assert.approximately(actual[1], fromLat, 0.000001); +// }, +// ); +// }); +// }); +// }); +// } diff --git a/tests/proj4/testData.ts b/tests/proj4/testData.ts new file mode 100644 index 00000000..ed8d77da --- /dev/null +++ b/tests/proj4/testData.ts @@ -0,0 +1,965 @@ +export const TEST_DATA = [ + { + code: 'PROJCS["CH1903 / LV03",GEOGCS["CH1903",DATUM["D_CH1903",SPHEROID["Bessel_1841",6377397.155,299.1528128]],PRIMEM["Greenwich",0],UNIT["Degree",0.017453292519943295]],PROJECTION["Hotine_Oblique_Mercator_Azimuth_Center"],PARAMETER["latitude_of_center",46.95240555555556],PARAMETER["longitude_of_center",7.439583333333333],PARAMETER["azimuth",90],PARAMETER["scale_factor",1],PARAMETER["false_easting",600000],PARAMETER["false_northing",200000],UNIT["Meter",1]]', + xy: [660013.4882918689, 185172.17110117766], + ll: [8.225, 46.815], + acc: { + xy: 0.1, + ll: 5, + }, + }, + { + code: 'PROJCS["CH1903 / LV03",GEOGCS["CH1903",DATUM["CH1903",SPHEROID["Bessel 1841",6377397.155,299.1528128,AUTHORITY["EPSG","7004"]],TOWGS84[674.4,15.1,405.3,0,0,0,0],AUTHORITY["EPSG","6149"]],PRIMEM["Greenwich",0,AUTHORITY["EPSG","8901"]],UNIT["degree",0.0174532925199433,AUTHORITY["EPSG","9122"]],AUTHORITY["EPSG","4149"]],PROJECTION["Hotine_Oblique_Mercator_Azimuth_Center"],PARAMETER["latitude_of_center",46.95240555555556],PARAMETER["longitude_of_center",7.439583333333333],PARAMETER["azimuth",90],PARAMETER["rectified_grid_angle",90],PARAMETER["scale_factor",1],PARAMETER["false_easting",600000],PARAMETER["false_northing",200000],UNIT["metre",1,AUTHORITY["EPSG","9001"]],AXIS["Y",EAST],AXIS["X",NORTH],AUTHORITY["EPSG","21781"]]', + xy: [660013.4882918689, 185172.17110117766], + ll: [8.225, 46.815], + acc: { + xy: 0.1, + ll: 5, + }, + }, + { + code: 'PROJCS["NAD83 / Massachusetts Mainland",GEOGCS["NAD83",DATUM["North_American_Datum_1983",SPHEROID["GRS 1980",6378137,298.257222101,AUTHORITY["EPSG","7019"]],AUTHORITY["EPSG","6269"]],PRIMEM["Greenwich",0,AUTHORITY["EPSG","8901"]],UNIT["degree",0.01745329251994328,AUTHORITY["EPSG","9122"]],AUTHORITY["EPSG","4269"]],UNIT["metre",1,AUTHORITY["EPSG","9001"]],PROJECTION["Lambert_Conformal_Conic_2SP"],PARAMETER["standard_parallel_1",42.68333333333333],PARAMETER["standard_parallel_2",41.71666666666667],PARAMETER["latitude_of_origin",41],PARAMETER["central_meridian",-71.5],PARAMETER["false_easting",200000],PARAMETER["false_northing",750000],AUTHORITY["EPSG","26986"],AXIS["X",EAST],AXIS["Y",NORTH]]', + xy: [231394.84, 902621.11], + ll: [-71.11881762742996, 42.37346263960867], + }, + { + code: 'PROJCS["NAD83 / Massachusetts Mainland",GEOGCS["GCS_North_American_1983",DATUM["D_North_American_1983",SPHEROID["GRS_1980",6378137,298.257222101]],PRIMEM["Greenwich",0],UNIT["Degree",0.017453292519943295]],PROJECTION["Lambert_Conformal_Conic"],PARAMETER["standard_parallel_1",42.68333333333333],PARAMETER["standard_parallel_2",41.71666666666667],PARAMETER["latitude_of_origin",41],PARAMETER["central_meridian",-71.5],PARAMETER["false_easting",200000],PARAMETER["false_northing",750000],UNIT["Meter",1]]', + xy: [231394.84, 902621.11], + ll: [-71.11881762742996, 42.37346263960867], + }, + { + code: 'PROJCS["NAD83 / Massachusetts Mainland", GEOGCS["NAD83", DATUM["North American Datum 1983", SPHEROID["GRS 1980", 6378137.0, 298.257222101, AUTHORITY["EPSG","7019"]], TOWGS84[0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0], AUTHORITY["EPSG","6269"]], PRIMEM["Greenwich", 0.0, AUTHORITY["EPSG","8901"]], UNIT["degree", 0.017453292519943295], AXIS["Geodetic longitude", EAST], AXIS["Geodetic latitude", NORTH], AUTHORITY["EPSG","4269"]], PROJECTION["Lambert_Conformal_Conic_2SP", AUTHORITY["EPSG","9802"]], PARAMETER["central_meridian", -71.5], PARAMETER["latitude_of_origin", 41.0], PARAMETER["standard_parallel_1", 42.68333333333334], PARAMETER["false_easting", 200000.0], PARAMETER["false_northing", 750000.0], PARAMETER["scale_factor", 1.0], PARAMETER["standard_parallel_2", 41.71666666666667], UNIT["m", 1.0], AXIS["Easting", EAST], AXIS["Northing", NORTH], AUTHORITY["EPSG","26986"]]', + xy: [231394.84, 902621.11], + ll: [-71.11881762742996, 42.37346263960867], + }, + { + code: 'PROJCS["Asia_North_Equidistant_Conic",GEOGCS["GCS_WGS_1984",DATUM["D_WGS_1984",SPHEROID["WGS_1984",6378137,298.257223563]],PRIMEM["Greenwich",0],UNIT["Degree",0.017453292519943295]],PROJECTION["Equidistant_Conic"],PARAMETER["False_Easting",0],PARAMETER["False_Northing",0],PARAMETER["Central_Meridian",95],PARAMETER["Standard_Parallel_1",15],PARAMETER["Standard_Parallel_2",65],PARAMETER["Latitude_Of_Origin",30],UNIT["Meter",1]]', + xy: [88280.59904432714, 111340.90165417176], + ll: [96, 31], + }, + { + code: 'PROJCS["Asia_North_Equidistant_Conic",GEOGCS["GCS_WGS_1984",DATUM["WGS_1984",SPHEROID["WGS_1984",6378137,298.257223563]],PRIMEM["Greenwich",0],UNIT["Degree",0.017453292519943295]],PROJECTION["Equidistant_Conic"],PARAMETER["False_Easting",0],PARAMETER["False_Northing",0],PARAMETER["Central_Meridian",95],PARAMETER["Standard_Parallel_1",15],PARAMETER["Standard_Parallel_2",65],PARAMETER["Latitude_Of_Origin",30],UNIT["Meter",1],AUTHORITY["EPSG","102026"]]', + xy: [88280.59904432714, 111340.90165417176], + ll: [96, 31], + }, + { + code: 'PROJCS["World_Sinusoidal",GEOGCS["GCS_WGS_1984",DATUM["WGS_1984",SPHEROID["WGS_1984",6378137,298.257223563]],PRIMEM["Greenwich",0],UNIT["Degree",0.017453292519943295]],PROJECTION["Sinusoidal"],PARAMETER["False_Easting",0],PARAMETER["False_Northing",0],PARAMETER["Central_Meridian",0],UNIT["Meter",1],AUTHORITY["EPSG","54008"]]', + xy: [738509.49, 5874620.38], + ll: [11.0, 53.0], + }, + { + code: 'PROJCS["World_Sinusoidal",GEOGCS["GCS_WGS_1984",DATUM["D_WGS_1984",SPHEROID["WGS_1984",6378137,298.257223563]],PRIMEM["Greenwich",0],UNIT["Degree",0.017453292519943295]],PROJECTION["Sinusoidal"],PARAMETER["False_Easting",0],PARAMETER["False_Northing",0],PARAMETER["Central_Meridian",0],UNIT["Meter",1]]', + xy: [738509.49, 5874620.38], + ll: [11.0, 53.0], + }, + { + code: 'PROJCS["ETRS89 / ETRS-LAEA",GEOGCS["ETRS89",DATUM["D_ETRS_1989",SPHEROID["GRS_1980",6378137,298.257222101]],PRIMEM["Greenwich",0],UNIT["Degree",0.017453292519943295]],PROJECTION["Lambert_Azimuthal_Equal_Area"],PARAMETER["latitude_of_origin",52],PARAMETER["central_meridian",10],PARAMETER["false_easting",4321000],PARAMETER["false_northing",3210000],UNIT["Meter",1]]', + xy: [4388138.6, 3321736.46], + ll: [11.0, 53.0], + }, + { + code: 'PROJCS["ETRS89 / ETRS-LAEA",GEOGCS["ETRS89",DATUM["European_Terrestrial_Reference_System_1989",SPHEROID["GRS 1980",6378137,298.257222101,AUTHORITY["EPSG","7019"]],AUTHORITY["EPSG","6258"]],PRIMEM["Greenwich",0,AUTHORITY["EPSG","8901"]],UNIT["degree",0.01745329251994328,AUTHORITY["EPSG","9122"]],AUTHORITY["EPSG","4258"]],UNIT["metre",1,AUTHORITY["EPSG","9001"]],PROJECTION["Lambert_Azimuthal_Equal_Area"],PARAMETER["latitude_of_center",52],PARAMETER["longitude_of_center",10],PARAMETER["false_easting",4321000],PARAMETER["false_northing",3210000],AUTHORITY["EPSG","3035"],AXIS["X",EAST],AXIS["Y",NORTH]]', + xy: [4388138.6, 3321736.46], + ll: [11.0, 53.0], + }, + { + code: '+proj=gnom +lat_0=90 +lon_0=0 +x_0=6300000 +y_0=6300000 +ellps=WGS84 +datum=WGS84 +units=m +no_defs', + xy: [350577.5930806119, 4705857.070634324], + ll: [-75, 46], + }, + { + code: '+proj=gnom +lat_0=90 +lon_0=0 +x_0=6300000 +y_0=6300000 +ellps=WGS84 +datum=WGS84 +units=m +no_defs', + xy: [350577.5930806119, 4705857.070634324], + ll: [-75, 46], + }, + { + code: 'PROJCS["NAD83(CSRS) / UTM zone 17N",GEOGCS["NAD83(CSRS)",DATUM["D_North_American_1983_CSRS98",SPHEROID["GRS_1980",6378137,298.257222101]],PRIMEM["Greenwich",0],UNIT["Degree",0.017453292519943295]],PROJECTION["Transverse_Mercator"],PARAMETER["latitude_of_origin",0],PARAMETER["central_meridian",-81],PARAMETER["scale_factor",0.9996],PARAMETER["false_easting",500000],PARAMETER["false_northing",0],UNIT["Meter",1]]', + xy: [411461.807497, 4700123.744402], + ll: [-82.07666015625, 42.448388671875], + }, + { + code: 'PROJCS["NAD83(CSRS) / UTM zone 17N",GEOGCS["NAD83(CSRS)",DATUM["NAD83_Canadian_Spatial_Reference_System",SPHEROID["GRS 1980",6378137,298.257222101,AUTHORITY["EPSG","7019"]],AUTHORITY["EPSG","6140"]],PRIMEM["Greenwich",0,AUTHORITY["EPSG","8901"]],UNIT["degree",0.01745329251994328,AUTHORITY["EPSG","9122"]],AUTHORITY["EPSG","4617"]],UNIT["metre",1,AUTHORITY["EPSG","9001"]],PROJECTION["Transverse_Mercator"],PARAMETER["latitude_of_origin",0],PARAMETER["central_meridian",-81],PARAMETER["scale_factor",0.9996],PARAMETER["false_easting",500000],PARAMETER["false_northing",0],AUTHORITY["EPSG","2958"],AXIS["Easting",EAST],AXIS["Northing",NORTH]]', + xy: [411461.807497, 4700123.744402], + ll: [-82.07666015625, 42.448388671875], + }, + { + code: 'PROJCS["ETRS89 / UTM zone 32N",GEOGCS["ETRS89",DATUM["European_Terrestrial_Reference_System_1989",SPHEROID["GRS 1980",6378137,298.257222101,AUTHORITY["EPSG","7019"]],TOWGS84[0,0,0,0,0,0,0],AUTHORITY["EPSG","6258"]],PRIMEM["Greenwich",0,AUTHORITY["EPSG","8901"]],UNIT["degree",0.0174532925199433,AUTHORITY["EPSG","9122"]],AUTHORITY["EPSG","4258"]],PROJECTION["Extended_Transverse_Mercator"],PARAMETER["latitude_of_origin",0],PARAMETER["central_meridian",9],PARAMETER["scale_factor",0.9996],PARAMETER["false_easting",500000],PARAMETER["false_northing",0],UNIT["metre",1,AUTHORITY["EPSG","9001"]],AXIS["Easting",EAST],AXIS["Northing",NORTH],AUTHORITY["EPSG","25832"]]', + xy: [-1877994.66, 3932281.56], + ll: [-16.10000000237, 32.879999998812], + }, + { + code: 'PROJCS["NAD27 / UTM zone 14N",GEOGCS["NAD27 Coordinate System",DATUM["D_North American Datum 1927 (NAD27)",SPHEROID["Clarke_1866",6378206.4,294.97869821391]],PRIMEM["Greenwich",0],UNIT["Degree",0.017453292519943295]],PROJECTION["Extended_Transverse_Mercator"],PARAMETER["false_easting",500000],PARAMETER["false_northing",0],PARAMETER["latitude_of_origin",0],PARAMETER["central_meridian",-99],PARAMETER["scale_factor",0.9996],UNIT["Meter (m)",1]]', + xy: [2026074.9192811155, 12812891.606450122], + ll: [51.517955776474096, 61.56941794249017], + }, + { + code: 'PROJCS["World_Mollweide",GEOGCS["GCS_WGS_1984",DATUM["WGS_1984",SPHEROID["WGS_1984",6378137,298.257223563]],PRIMEM["Greenwich",0],UNIT["Degree",0.017453292519943295]],PROJECTION["Mollweide"],PARAMETER["False_Easting",0],PARAMETER["False_Northing",0],PARAMETER["Central_Meridian",0],UNIT["Meter",1],AUTHORITY["EPSG","54009"]]', + xy: [3891383.58309223, 6876758.9933288], + ll: [60, 60], + }, + { + code: 'PROJCS["World_Mollweide",GEOGCS["GCS_WGS_1984",DATUM["D_WGS_1984",SPHEROID["WGS_1984",6378137,298.257223563]],PRIMEM["Greenwich",0],UNIT["Degree",0.017453292519943295]],PROJECTION["Mollweide"],PARAMETER["False_Easting",0],PARAMETER["False_Northing",0],PARAMETER["Central_Meridian",0],UNIT["Meter",1]]', + xy: [3891383.58309223, 6876758.9933288], + ll: [60, 60], + }, + { + code: 'PROJCS["NAD83 / BC Albers",GEOGCS["NAD83",DATUM["North_American_Datum_1983",SPHEROID["GRS 1980",6378137,298.257222101,AUTHORITY["EPSG","7019"]],AUTHORITY["EPSG","6269"]],PRIMEM["Greenwich",0,AUTHORITY["EPSG","8901"]],UNIT["degree",0.01745329251994328,AUTHORITY["EPSG","9122"]],AUTHORITY["EPSG","4269"]],UNIT["metre",1,AUTHORITY["EPSG","9001"]],PROJECTION["Albers_Conic_Equal_Area"],PARAMETER["standard_parallel_1",50],PARAMETER["standard_parallel_2",58.5],PARAMETER["latitude_of_center",45],PARAMETER["longitude_of_center",-126],PARAMETER["false_easting",1000000],PARAMETER["false_northing",0],AUTHORITY["EPSG","3005"],AXIS["Easting",EAST],AXIS["Northing",NORTH]]', + ll: [-126.54, 54.15], + xy: [964813.103719, 1016486.305862], + }, + { + code: 'PROJCS["NAD83 / BC Albers",GEOGCS["GCS_North_American_1983",DATUM["D_North_American_1983",SPHEROID["GRS_1980",6378137,298.257222101]],PRIMEM["Greenwich",0],UNIT["Degree",0.017453292519943295]],PROJECTION["Albers"],PARAMETER["standard_parallel_1",50],PARAMETER["standard_parallel_2",58.5],PARAMETER["latitude_of_origin",45],PARAMETER["central_meridian",-126],PARAMETER["false_easting",1000000],PARAMETER["false_northing",0],UNIT["Meter",1]]', + ll: [-126.54, 54.15], + xy: [964813.103719, 1016486.305862], + }, + { + code: 'PROJCS["Azimuthal_Equidistant",GEOGCS["GCS_WGS_1984",DATUM["D_WGS_1984",SPHEROID["WGS_1984",6378137,298.257223563]],PRIMEM["Greenwich",0],UNIT["Degree",0.017453292519943295]],PROJECTION["Azimuthal_Equidistant"],PARAMETER["False_Easting",0],PARAMETER["False_Northing",0],PARAMETER["Central_Meridian",0],PARAMETER["Latitude_Of_Origin",0],UNIT["Meter",1]]', + ll: [0, 0], + xy: [0, 0], + }, + { + code: 'PROJCS["Sphere_Azimuthal_Equidistant",GEOGCS["GCS_Sphere",DATUM["Not_specified_based_on_Authalic_Sphere",SPHEROID["Sphere",6371000,0]],PRIMEM["Greenwich",0],UNIT["Degree",0.017453292519943295]],PROJECTION["Azimuthal_Equidistant"],PARAMETER["False_Easting",0],PARAMETER["False_Northing",0],PARAMETER["Central_Meridian",0],PARAMETER["Latitude_Of_Origin",0],UNIT["Meter",1]]', + ll: [0, 0], + xy: [0, 0], + }, + { + code: 'PROJCS["North_Pole_Azimuthal_Equidistant",GEOGCS["GCS_WGS_1984",DATUM["D_WGS_1984",SPHEROID["WGS_1984",6378137,298.257223563]],PRIMEM["Greenwich",0],UNIT["Degree",0.017453292519943295]],PROJECTION["Azimuthal_Equidistant"],PARAMETER["False_Easting",0],PARAMETER["False_Northing",0],PARAMETER["Central_Meridian",0],PARAMETER["Latitude_Of_Origin",90],UNIT["Meter",1]]', + ll: [50.977303830208, 30.915260093747], + xy: [5112279.911077, -4143196.76625], + }, + { + code: 'PROJCS["North_Pole_Azimuthal_Equidistant",GEOGCS["GCS_WGS_1984",DATUM["WGS_1984",SPHEROID["WGS_1984",6378137,298.257223563]],PRIMEM["Greenwich",0],UNIT["Degree",0.017453292519943295]],PROJECTION["Azimuthal_Equidistant"],PARAMETER["False_Easting",0],PARAMETER["False_Northing",0],PARAMETER["Central_Meridian",0],PARAMETER["Latitude_Of_Origin",90],UNIT["Meter",1],AUTHORITY["EPSG","102016"]]', + ll: [50.977303830208, 30.915260093747], + xy: [5112279.911077, -4143196.76625], + }, + { + code: 'PROJCS["Mount Dillon / Tobago Grid",GEOGCS["Mount Dillon",DATUM["Mount_Dillon",SPHEROID["Clarke 1858",6378293.645208759,294.2606763692654,AUTHORITY["EPSG","7007"]],AUTHORITY["EPSG","6157"]],PRIMEM["Greenwich",0,AUTHORITY["EPSG","8901"]],UNIT["degree",0.01745329251994328,AUTHORITY["EPSG","9122"]],AUTHORITY["EPSG","4157"]],UNIT["Clarke\'s link",0.201166195164,AUTHORITY["EPSG","9039"]],PROJECTION["Cassini_Soldner"],PARAMETER["latitude_of_origin",11.25217861111111],PARAMETER["central_meridian",-60.68600888888889],PARAMETER["false_easting",187500],PARAMETER["false_northing",180000],AUTHORITY["EPSG","2066"],AXIS["Easting",EAST],AXIS["Northing",NORTH]]', + ll: [-60.676753018, 11.2487234308], + xy: [192524.3061766178, 178100.2740019509], + acc: { + ll: 1, + xy: -4, + }, + }, + { + code: 'PROJCS["Mount Dillon / Tobago Grid",GEOGCS["Mount Dillon",DATUM["D_Mount_Dillon",SPHEROID["Clarke_1858",6378293.645208759,294.2606763692654]],PRIMEM["Greenwich",0],UNIT["Degree",0.017453292519943295]],PROJECTION["Cassini"],PARAMETER["latitude_of_origin",11.25217861111111],PARAMETER["central_meridian",-60.68600888888889],PARAMETER["false_easting",187500],PARAMETER["false_northing",180000],UNIT["Clarke\'s link",0.201166195164]]', + ll: [-60.676753018, 11.2487234308], + xy: [192524.3061766178, 178100.2740019509], + acc: { + ll: 1, + xy: -4, + }, + }, + // { + // code:'EPSG:3975', + // ll:[-9.764450683, 25.751953], + // xy:[-942135.525095996, 3178441.8667094777] + // }, + { + code: 'PROJCS["World Equidistant Cylindrical (Sphere)",GEOGCS["Unspecified datum based upon the GRS 1980 Authalic Sphere",DATUM["Not_specified_based_on_GRS_1980_Authalic_Sphere",SPHEROID["GRS 1980 Authalic Sphere",6371007,0,AUTHORITY["EPSG","7048"]],AUTHORITY["EPSG","6047"]],PRIMEM["Greenwich",0,AUTHORITY["EPSG","8901"]],UNIT["degree",0.01745329251994328,AUTHORITY["EPSG","9122"]],AUTHORITY["EPSG","4047"]],UNIT["metre",1,AUTHORITY["EPSG","9001"]],PROJECTION["Equirectangular"],PARAMETER["latitude_of_origin",0],PARAMETER["central_meridian",0],PARAMETER["false_easting",0],PARAMETER["false_northing",0],AUTHORITY["EPSG","3786"],AXIS["X",EAST],AXIS["Y",NORTH]]', + ll: [-1.7539371169976, 12.632997701986], + xy: [-195029.12334755991, 1395621.9368162225], + acc: { + ll: 2, + }, + }, + { + code: 'PROJCS["World Equidistant Cylindrical (Sphere)",GEOGCS["Unspecified datum based upon the GRS 1980 Authalic Sphere",DATUM["D_",SPHEROID["GRS_1980_Authalic_Sphere",6371007,0]],PRIMEM["Greenwich",0],UNIT["Degree",0.017453292519943295]],PROJECTION["Equidistant_Cylindrical"],PARAMETER["central_meridian",0],PARAMETER["false_easting",0],PARAMETER["false_northing",0],UNIT["Meter",1]]', + ll: [-1.7539371169976, 12.632997701986], + xy: [-195029.12334755991, 1395621.9368162225], + acc: { + ll: 2, + }, + }, + { + code: 'PROJCS["Segara / NEIEZ",GEOGCS["Segara",DATUM["Gunung_Segara",SPHEROID["Bessel 1841",6377397.155,299.1528128,AUTHORITY["EPSG","7004"]],AUTHORITY["EPSG","6613"]],PRIMEM["Greenwich",0,AUTHORITY["EPSG","8901"]],UNIT["degree",0.01745329251994328,AUTHORITY["EPSG","9122"]],AUTHORITY["EPSG","4613"]],UNIT["metre",1,AUTHORITY["EPSG","9001"]],PROJECTION["Mercator_1SP"],PARAMETER["central_meridian",110],PARAMETER["scale_factor",0.997],PARAMETER["false_easting",3900000],PARAMETER["false_northing",900000],AUTHORITY["EPSG","3000"],AXIS["X",EAST],AXIS["Y",NORTH]]', + ll: [116.65547897884308, -0.6595605286983485], + xy: [4638523.040740433, 827245.2586932715], + }, + { + code: 'PROJCS["Segara / NEIEZ",GEOGCS["Segara",DATUM["D_Gunung_Segara",SPHEROID["Bessel_1841",6377397.155,299.1528128]],PRIMEM["Greenwich",0],UNIT["Degree",0.017453292519943295]],PROJECTION["Mercator"],PARAMETER["central_meridian",110],PARAMETER["scale_factor",0.997],PARAMETER["false_easting",3900000],PARAMETER["false_northing",900000],UNIT["Meter",1]]', + ll: [116.65547897884308, -0.6595605286983485], + xy: [4638523.040740433, 827245.2586932715], + }, + { + code: 'PROJCS["Beduaram / TM 13 NE",GEOGCS["Beduaram",DATUM["Beduaram",SPHEROID["Clarke 1880 (IGN)",6378249.2,293.4660212936269,AUTHORITY["EPSG","7011"]],TOWGS84[-106,-87,188,0,0,0,0],AUTHORITY["EPSG","6213"]],PRIMEM["Greenwich",0,AUTHORITY["EPSG","8901"]],UNIT["degree",0.01745329251994328,AUTHORITY["EPSG","9122"]],AUTHORITY["EPSG","4213"]],UNIT["metre",1,AUTHORITY["EPSG","9001"]],PROJECTION["Transverse_Mercator"],PARAMETER["latitude_of_origin",0],PARAMETER["central_meridian",13],PARAMETER["scale_factor",0.9996],PARAMETER["false_easting",500000],PARAMETER["false_northing",0],AUTHORITY["EPSG","2931"],AXIS["X",EAST],AXIS["Y",NORTH]]', + ll: [5, 25], + xy: [-308919.1234711099, 2788738.255936392], + }, + { + code: 'PROJCS["Beduaram / TM 13 NE",GEOGCS["Beduaram",DATUM["D_Beduaram",SPHEROID["Clarke_1880_IGN",6378249.2,293.4660212936269]],PRIMEM["Greenwich",0],UNIT["Degree",0.017453292519943295]],PROJECTION["Transverse_Mercator"],PARAMETER["latitude_of_origin",0],PARAMETER["central_meridian",13],PARAMETER["scale_factor",0.9996],PARAMETER["false_easting",500000],PARAMETER["false_northing",0],UNIT["Meter",1]]', + ll: [5, 25], + xy: [-308919.1234711099, 2788738.255936392], + }, + { + code: '+proj=lcc +lat_1=49.5 +lat_0=49.5 +lon_0=0 +k_0=0.999877341 +x_0=600000 +y_0=1200000 +ellps=clrk80ign +pm=paris +towgs84=-168,-60,320,0,0,0,0 +units=m +no_defs +type=crs', + ll: [2.294482, 48.859045], + xy: [596916.561147926957, 1128733.073948238511], + }, + { + code: 'PROJCS["S-JTSK (Ferro) / Krovak",GEOGCS["S-JTSK (Ferro)",DATUM["S_JTSK_Ferro",SPHEROID["Bessel 1841",6377397.155,299.1528128,AUTHORITY["EPSG","7004"]],AUTHORITY["EPSG","6818"]],PRIMEM["Ferro",-17.66666666666667,AUTHORITY["EPSG","8909"]],UNIT["degree",0.01745329251994328,AUTHORITY["EPSG","9122"]],AUTHORITY["EPSG","4818"]],UNIT["metre",1,AUTHORITY["EPSG","9001"]],PROJECTION["Krovak"],PARAMETER["latitude_of_center",49.5],PARAMETER["longitude_of_center",42.5],PARAMETER["azimuth",30.28813972222222],PARAMETER["pseudo_standard_parallel_1",78.5],PARAMETER["scale_factor",0.9999],PARAMETER["false_easting",0],PARAMETER["false_northing",0],AUTHORITY["EPSG","2065"],AXIS["Y",WEST],AXIS["X",SOUTH]]', + ll: [17.323583231075897, 49.39440725405376], + xy: [-544115.474379, -1144058.330762], + }, + { + code: 'PROJCS["S-JTSK (Ferro) / Krovak",GEOGCS["S-JTSK (Ferro)",DATUM["D_S_JTSK",SPHEROID["Bessel_1841",6377397.155,299.1528128]],PRIMEM["Ferro",-17.66666666666667],UNIT["Degree",0.017453292519943295]],PROJECTION["Krovak"],PARAMETER["latitude_of_center",49.5],PARAMETER["longitude_of_center",42.5],PARAMETER["azimuth",30.28813972222222],PARAMETER["pseudo_standard_parallel_1",78.5],PARAMETER["scale_factor",0.9999],PARAMETER["false_easting",0],PARAMETER["false_northing",0],UNIT["Meter",1]]', + ll: [17.323583231075897, 49.39440725405376], + xy: [-544115.474379, -1144058.330762], + }, + { + code: 'PROJCS["Sphere_Miller_Cylindrical",GEOGCS["GCS_Sphere",DATUM["D_Sphere",SPHEROID["Sphere",6371000,0]],PRIMEM["Greenwich",0],UNIT["Degree",0.017453292519943295]],PROJECTION["Miller_Cylindrical"],PARAMETER["False_Easting",0],PARAMETER["False_Northing",0],PARAMETER["Central_Meridian",0],UNIT["Meter",1]]', + ll: [-1.3973289073953, 12.649176474268513], + xy: [-155375.88535614178, 1404635.2633403721], + acc: { + ll: 3, + }, + }, + { + code: 'PROJCS["Sphere_Miller_Cylindrical",GEOGCS["GCS_Sphere",DATUM["Not_specified_based_on_Authalic_Sphere",SPHEROID["Sphere",6371000,0]],PRIMEM["Greenwich",0],UNIT["Degree",0.017453292519943295]],PROJECTION["Miller_Cylindrical"],PARAMETER["False_Easting",0],PARAMETER["False_Northing",0],PARAMETER["Central_Meridian",0],UNIT["Meter",1],AUTHORITY["EPSG","53003"]]', + ll: [-1.3973289073953, 12.649176474268513], + xy: [-155375.88535614178, 1404635.2633403721], + acc: { + ll: 3, + }, + }, + { + code: 'PROJCS["NZGD49 / New Zealand Map Grid",GEOGCS["NZGD49",DATUM["D_New_Zealand_1949",SPHEROID["International_1924",6378388,297]],PRIMEM["Greenwich",0],UNIT["Degree",0.017453292519943295]],PROJECTION["New_Zealand_Map_Grid"],PARAMETER["latitude_of_origin",-41],PARAMETER["central_meridian",173],PARAMETER["false_easting",2510000],PARAMETER["false_northing",6023150],UNIT["Meter",1]]', + ll: [172.465, -40.7], + xy: [2464770.343667, 6056137.861919], + }, + { + code: 'PROJCS["NZGD49 / New Zealand Map Grid",GEOGCS["NZGD49",DATUM["New_Zealand_Geodetic_Datum_1949",SPHEROID["International 1924",6378388,297,AUTHORITY["EPSG","7022"]],TOWGS84[59.47,-5.04,187.44,0.47,-0.1,1.024,-4.5993],AUTHORITY["EPSG","6272"]],PRIMEM["Greenwich",0,AUTHORITY["EPSG","8901"]],UNIT["degree",0.01745329251994328,AUTHORITY["EPSG","9122"]],AUTHORITY["EPSG","4272"]],UNIT["metre",1,AUTHORITY["EPSG","9001"]],PROJECTION["New_Zealand_Map_Grid"],PARAMETER["latitude_of_origin",-41],PARAMETER["central_meridian",173],PARAMETER["false_easting",2510000],PARAMETER["false_northing",6023150],AUTHORITY["EPSG","27200"],AXIS["Easting",EAST],AXIS["Northing",NORTH]]', + ll: [172.465, -40.7], + xy: [2464770.343667, 6056137.861919], + }, + { + code: 'PROJCS["Rassadiran / Nakhl e Taqi", GEOGCS["Rassadiran", DATUM["Rassadiran", SPHEROID["International 1924",6378388,297, AUTHORITY["EPSG","7022"]], TOWGS84[-133.63,-157.5,-158.62,0,0,0,0], AUTHORITY["EPSG","6153"]], PRIMEM["Greenwich",0, AUTHORITY["EPSG","8901"]], UNIT["degree",0.0174532925199433, AUTHORITY["EPSG","9122"]], AUTHORITY["EPSG","4153"]], PROJECTION["Hotine_Oblique_Mercator_Azimuth_Center"], PARAMETER["latitude_of_center",27.51882880555555], PARAMETER["longitude_of_center",52.60353916666667], PARAMETER["azimuth",0.5716611944444444], PARAMETER["rectified_grid_angle",0.5716611944444444], PARAMETER["scale_factor",0.999895934], PARAMETER["false_easting",658377.437], PARAMETER["false_northing",3044969.194], UNIT["metre",1, AUTHORITY["EPSG","9001"]], AXIS["Easting",EAST], AXIS["Northing",NORTH], AUTHORITY["EPSG","2057"]]', + ll: [52.605, 27.5], + xy: [658511.261946, 3043003.05468], + acc: { + ll: 8, + xy: 6, + }, + }, + { + code: 'PROJCS["SAD69 / Brazil Polyconic",GEOGCS["SAD69",DATUM["D_South_American_1969",SPHEROID["GRS_1967_SAD69",6378160,298.25]],PRIMEM["Greenwich",0],UNIT["Degree",0.017453292519943295]],PROJECTION["Polyconic"],PARAMETER["latitude_of_origin",0],PARAMETER["central_meridian",-54],PARAMETER["false_easting",5000000],PARAMETER["false_northing",10000000],UNIT["Meter",1]]', + ll: [-49.221772553812, -0.34551739237581], + xy: [5531902.134932, 9961660.779347], + acc: { + ll: 3, + xy: -2, + }, + }, + { + code: 'PROJCS["SAD69 / Brazil Polyconic",GEOGCS["SAD69",DATUM["South_American_Datum_1969",SPHEROID["GRS 1967 (SAD69)",6378160,298.25,AUTHORITY["EPSG","7050"]],AUTHORITY["EPSG","6618"]],PRIMEM["Greenwich",0,AUTHORITY["EPSG","8901"]],UNIT["degree",0.01745329251994328,AUTHORITY["EPSG","9122"]],AUTHORITY["EPSG","4618"]],UNIT["metre",1,AUTHORITY["EPSG","9001"]],PROJECTION["Polyconic"],PARAMETER["latitude_of_origin",0],PARAMETER["central_meridian",-54],PARAMETER["false_easting",5000000],PARAMETER["false_northing",10000000],AUTHORITY["EPSG","29101"],AXIS["X",EAST],AXIS["Y",NORTH]]', + ll: [-49.221772553812, -0.34551739237581], + xy: [5531902.134932, 9961660.779347], + acc: { + ll: 3, + xy: -2, + }, + }, + { + code: 'PROJCS["WGS 84 / UPS North",GEOGCS["WGS 84",DATUM["WGS_1984",SPHEROID["WGS 84",6378137,298.257223563,AUTHORITY["EPSG","7030"]],AUTHORITY["EPSG","6326"]],PRIMEM["Greenwich",0,AUTHORITY["EPSG","8901"]],UNIT["degree",0.01745329251994328,AUTHORITY["EPSG","9122"]],AUTHORITY["EPSG","4326"]],UNIT["metre",1,AUTHORITY["EPSG","9001"]],PROJECTION["Polar_Stereographic"],PARAMETER["latitude_of_origin",90],PARAMETER["central_meridian",0],PARAMETER["scale_factor",0.994],PARAMETER["false_easting",2000000],PARAMETER["false_northing",2000000],AUTHORITY["EPSG","32661"],AXIS["Easting",UNKNOWN],AXIS["Northing",UNKNOWN]]', + ll: [0, 75], + xy: [2000000, 325449.806286], + }, + { + code: 'PROJCS["WGS 84 / UPS North",GEOGCS["GCS_WGS_1984",DATUM["D_WGS_1984",SPHEROID["WGS_1984",6378137,298.257223563]],PRIMEM["Greenwich",0],UNIT["Degree",0.017453292519943295]],PROJECTION["Stereographic_North_Pole"],PARAMETER["standard_parallel_1",90],PARAMETER["central_meridian",0],PARAMETER["scale_factor",0.994],PARAMETER["false_easting",2000000],PARAMETER["false_northing",2000000],UNIT["Meter",1]]', + ll: [0, 75], + xy: [2000000, 325449.806286], + }, + { + code: '+proj=aeqd +lat_0=0 +lon_0=0 +x_0=0 +y_0=0 +datum=WGS84 +units=m +no_defs', + ll: [2, 0], + xy: [222638.98158654713, 0], + }, + { + code: '+proj=aeqd +lat_0=0 +lon_0=0 +x_0=0 +y_0=0 +datum=WGS84 +units=m +no_defs', + ll: [89, 0], + xy: [9907434.680601358, 0], + }, + { + // code:'+proj=aeqd +lat_0=0 +lon_0=0 +x_0=0 +y_0=0 +datum=WGS84 +units=m +no_defs', + // ll:[91, 0], + // xy:[10130073.6622, 0] + // },{ + code: '+proj=aeqd +lat_0=0 +lon_0=0 +x_0=0 +y_0=0 +a=6371000 +b=6371000 +units=m +no_defs', + ll: [91, 0], + xy: [10118738.32, 0.0], + }, + { + code: '+proj=laea +lat_0=2 +lon_0=1 +x_0=0 +y_0=0 +a=6371000 +b=6371000 +units=m +no_defs', + ll: [1, 2], + xy: [0, 0], + }, + { + code: '+proj=laea +lat_0=1 +lon_0=1 +x_0=0 +y_0=0 +a=6371000 +b=6371000 +units=m +no_defs', + ll: [1, 1], + xy: [0, 0], + }, + { + code: '+proj=laea +lat_0=1 +lon_0=1 +x_0=0 +y_0=0 +a=6371000 +b=6371000 +units=m +no_defs', + ll: [2, 1], + xy: [111176.58, 16.93], + }, + { + code: '+proj=laea +lat_0=1 +lon_0=1 +x_0=0 +y_0=0 +a=6371000 +b=6371000 +units=m +no_defs', + ll: [1, 2], + xy: [0.0, 111193.52], + }, + { + code: '+proj=laea +lat_0=0 +lon_0=0 +x_0=0 +y_0=0 +a=6371000 +b=6371000 +units=m +no_defs', + ll: [19, 0], + xy: [2103036.59, 0.0], + }, + { + code: '+proj=stere +lat_0=-90 +lat_ts=-70 +lon_0=0 +k=1 +x_0=0 +y_0=0 +datum=WGS84 +units=m +no_defs"', + ll: [0, -72.5], + xy: [0, 1910008.78441421], + }, + { + code: '+proj=stere +lat_0=-90 +lon_0=0 +x_0=0 +y_0=0 +a=3396000 +b=3396000 +units=m +no_defs', + ll: [0, -72.5], + xy: [0, 1045388.79], + }, + { + code: '+proj=stere', + ll: [0, -72.5], + xy: [0, -9334375.897187851], + }, + { + // Test that lat_ts at a pole is handled correctly in stere projection + code: '+no_defs +units=m +ellps=GRS80 +lon_0=0 +proj=stere +lat_ts=90.0 +lat_0=90 +x_0=0 +y_0=0', + ll: [69.6487, 18.955781], + xy: [8527917.706, -3163255.729], + }, + { + code: 'PROJCS["WGS 84 / NSIDC Sea Ice Polar Stereographic South", GEOGCS["WGS 84", DATUM["World Geodetic System 1984", SPHEROID["WGS 84", 6378137.0, 298.257223563, AUTHORITY["EPSG","7030"]], AUTHORITY["EPSG","6326"]], PRIMEM["Greenwich", 0.0, AUTHORITY["EPSG","8901"]], UNIT["degree", 0.017453292519943295], AXIS["Geodetic longitude", EAST], AXIS["Geodetic latitude", NORTH], AUTHORITY["EPSG","4326"]], PROJECTION["Polar Stereographic (variant B)", AUTHORITY["EPSG","9829"]], PARAMETER["central_meridian", 0.0], PARAMETER["Standard_Parallel_1", -70.0], PARAMETER["false_easting", 0.0], PARAMETER["false_northing", 0.0], UNIT["m", 1.0], AXIS["Easting", "North along 90 deg East"], AXIS["Northing", "North along 0 deg"], AUTHORITY["EPSG","3976"]]', + ll: [0, -72.5], + xy: [0, 1910008.78441421], + }, + { + code: 'PROJCS["NAD83(CSRS98) / New Brunswick Stereo (deprecated)",GEOGCS["NAD83(CSRS98)",DATUM["D_North_American_1983_CSRS98",SPHEROID["GRS_1980",6378137,298.257222101]],PRIMEM["Greenwich",0],UNIT["Degree",0.017453292519943295]],PROJECTION["Stereographic_North_Pole"],PARAMETER["standard_parallel_1",46.5],PARAMETER["central_meridian",-66.5],PARAMETER["scale_factor",0.999912],PARAMETER["false_easting",2500000],PARAMETER["false_northing",7500000],UNIT["Meter",1]]', + ll: [-66.415, 46.34], + xy: [2506543.370459, 7482219.546176], + }, + { + code: 'PROJCS["NAD83(CSRS98) / New Brunswick Stereo (deprecated)",GEOGCS["NAD83(CSRS98)",DATUM["NAD83_Canadian_Spatial_Reference_System",SPHEROID["GRS 1980",6378137,298.257222101,AUTHORITY["EPSG","7019"]],TOWGS84[0,0,0,0,0,0,0],AUTHORITY["EPSG","6140"]],PRIMEM["Greenwich",0,AUTHORITY["EPSG","8901"]],UNIT["degree",0.0174532925199433,AUTHORITY["EPSG","9108"]],AUTHORITY["EPSG","4140"]],UNIT["metre",1,AUTHORITY["EPSG","9001"]],PROJECTION["Oblique_Stereographic"],PARAMETER["latitude_of_origin",46.5],PARAMETER["central_meridian",-66.5],PARAMETER["scale_factor",0.999912],PARAMETER["false_easting",2500000],PARAMETER["false_northing",7500000],AUTHORITY["EPSG","2036"],AXIS["Easting",EAST],AXIS["Northing",NORTH]]', + ll: [-66.415, 46.34], + xy: [2506543.370459, 7482219.546176], + }, + { + code: 'PROJCS["Sphere_Van_der_Grinten_I",GEOGCS["GCS_Sphere",DATUM["D_Sphere",SPHEROID["Sphere",6371000,0]],PRIMEM["Greenwich",0],UNIT["Degree",0.017453292519943295]],PROJECTION["Van_der_Grinten_I"],PARAMETER["False_Easting",0],PARAMETER["False_Northing",0],PARAMETER["Central_Meridian",0],UNIT["Meter",1]]', + ll: [-1.41160801956, 67.40891366748], + xy: [-125108.675828, 9016899.042114], + acc: { + ll: 0, + xy: -5, + }, + }, + { + code: 'PROJCS["Sphere_Van_der_Grinten_I",GEOGCS["GCS_Sphere",DATUM["Not_specified_based_on_Authalic_Sphere",SPHEROID["Sphere",6371000,0]],PRIMEM["Greenwich",0],UNIT["Degree",0.017453292519943295]],PROJECTION["VanDerGrinten"],PARAMETER["False_Easting",0],PARAMETER["False_Northing",0],PARAMETER["Central_Meridian",0],UNIT["Meter",1],AUTHORITY["EPSG","53029"]]', + ll: [-1.41160801956, 67.40891366748], + xy: [-125108.675828, 9016899.042114], + acc: { + ll: 0, + xy: -5, + }, + }, + { + code: 'PROJCS["NAD_1983_StatePlane_New_Jersey_FIPS_2900_Feet",GEOGCS["GCS_North_American_1983",DATUM["D_North_American_1983",SPHEROID["GRS_1980",6378137.0,298.257222101]],PRIMEM["Greenwich",0.0],UNIT["Degree",0.0174532925199433]],PROJECTION["Transverse_Mercator"],PARAMETER["False_Easting",492125.0],PARAMETER["False_Northing",0.0],PARAMETER["Central_Meridian",-74.5],PARAMETER["Scale_Factor",0.9999],PARAMETER["Latitude_Of_Origin",38.83333333333334],UNIT["Foot_US",0.3048006096012192]]', + ll: [-74, 41], + xy: [630128.205, 789591.522], + }, + { + code: 'PROJCS["WGS_1984_Web_Mercator_Auxiliary_Sphere",GEOGCS["GCS_WGS_1984",DATUM["D_WGS_1984",SPHEROID["WGS_1984",6378137.0,298.257223563]],PRIMEM["Greenwich",0.0],UNIT["Degree",0.0174532925199433]],PROJECTION["Mercator_Auxiliary_Sphere"],PARAMETER["False_Easting",0.0],PARAMETER["False_Northing",0.0],PARAMETER["Central_Meridian",0.0],PARAMETER["Standard_Parallel_1",0.0],PARAMETER["Auxiliary_Sphere_Type",0.0],UNIT["Meter",1.0]]', + ll: [-74, 41], + xy: [-8237642.318702244, 5012341.663847514], + }, + { + code: '+proj=sinu +lon_0=0 +x_0=0 +y_0=0 +a=6371000 +b=6371000 +units=m +no_defs', + xy: [736106.55, 5893331.11], + ll: [11.0, 53.0], + }, + { + code: 'PROJCS["Belge 1972 / Belgian Lambert 72",GEOGCS["Belge 1972",DATUM["Reseau_National_Belge_1972",SPHEROID["International 1924",6378388,297,AUTHORITY["EPSG","7022"]],TOWGS84[106.869,-52.2978,103.724,-0.33657,0.456955,-1.84218,1],AUTHORITY["EPSG","6313"]],PRIMEM["Greenwich",0,AUTHORITY["EPSG","8901"]],UNIT["degree",0.01745329251994328,AUTHORITY["EPSG","9122"]],AUTHORITY["EPSG","4313"]],UNIT["metre",1,AUTHORITY["EPSG","9001"]],PROJECTION["Lambert_Conformal_Conic_2SP"],PARAMETER["standard_parallel_1",51.16666723333333],PARAMETER["standard_parallel_2",49.8333339],PARAMETER["latitude_of_origin",90],PARAMETER["central_meridian",4.367486666666666],PARAMETER["false_easting",150000.013],PARAMETER["false_northing",5400088.438],AUTHORITY["EPSG","31370"],AXIS["X",EAST],AXIS["Y",NORTH]]', + xy: [104588.196404, 193175.582367], + ll: [3.7186701465384533, 51.04642936832842], + }, + { + code: 'PROJCS["Belge 1972 / Belgian Lambert 72",GEOGCS["Belge 1972",DATUM["D_Belge_1972",SPHEROID["International_1924",6378388,297]],PRIMEM["Greenwich",0],UNIT["Degree",0.017453292519943295]],PROJECTION["Lambert_Conformal_Conic"],PARAMETER["standard_parallel_1",51.16666723333333],PARAMETER["standard_parallel_2",49.8333339],PARAMETER["latitude_of_origin",90],PARAMETER["central_meridian",4.367486666666666],PARAMETER["false_easting",150000.013],PARAMETER["false_northing",5400088.438],UNIT["Meter",1]]', + xy: [104588.196404, 193175.582367], + ll: [3.7186701465384533, 51.04642936832842], + }, + { + code: 'PROJCS["Belge 1972 / Belgian Lambert 72",GEOGCS["Belge 1972",DATUM["Reseau_National_Belge_1972",SPHEROID["International 1924",6378388,297,AUTHORITY["EPSG","7022"]],TOWGS84[-106.8686,52.2978,-103.7239,-0.3366,0.457,-1.8422,-1.2747],AUTHORITY["EPSG","6313"]],PRIMEM["Greenwich",0,AUTHORITY["EPSG","8901"]],UNIT["degree",0.0174532925199433,AUTHORITY["EPSG","9122"]],AUTHORITY["EPSG","4313"]],PROJECTION["Lambert_Conformal_Conic_2SP"],PARAMETER["standard_parallel_1",51.16666723333333],PARAMETER["standard_parallel_2",49.8333339],PARAMETER["latitude_of_origin",90],PARAMETER["central_meridian",4.367486666666666],PARAMETER["false_easting",150000.013],PARAMETER["false_northing",5400088.438],UNIT["metre",1,AUTHORITY["EPSG","9001"]],AXIS["X",EAST],AXIS["Y",NORTH],AUTHORITY["EPSG","31370"]]', + xy: [104469.69796438649, 193146.39675426576], + ll: [3.7186701465384533, 51.04642936832842], + }, + { + code: 'PROJCS["Belge 1972 / Belgian Lambert 72",GEOGCS["Belge 1972",DATUM["Reseau_National_Belge_1972",SPHEROID["International 1924",6378388,297,AUTHORITY["EPSG","7022"]],TOWGS84[-99.059,53.322,-112.486,-0.419,0.83,-1.885,-1],AUTHORITY["EPSG","6313"]],PRIMEM["Greenwich",0,AUTHORITY["EPSG","8901"]],UNIT["degree",0.0174532925199433,AUTHORITY["EPSG","9122"]],AUTHORITY["EPSG","4313"]],PROJECTION["Lambert_Conformal_Conic_2SP"],PARAMETER["standard_parallel_1",51.16666723333333],PARAMETER["standard_parallel_2",49.8333339],PARAMETER["latitude_of_origin",90],PARAMETER["central_meridian",4.367486666666666],PARAMETER["false_easting",150000.013],PARAMETER["false_northing",5400088.438],UNIT["metre",1,AUTHORITY["EPSG","9001"]],AXIS["X",EAST],AXIS["Y",NORTH],AUTHORITY["EPSG","31370"]]', + xy: [104468.8305227503, 193169.6828284394], + ll: [3.7186701465384533, 51.04642936832842], + }, + { + code: 'PROJCS["Belge 1972 / Belgian Lambert 72",GEOGCS["Belge 1972",DATUM["Reseau_National_Belge_1972",SPHEROID["International 1924",6378388,297,AUTHORITY["EPSG","7022"]],TOWGS84[-125.8,79.9,-100.5,0,0,0,0],AUTHORITY["EPSG","6313"]],PRIMEM["Greenwich",0,AUTHORITY["EPSG","8901"]],UNIT["degree",0.0174532925199433,AUTHORITY["EPSG","9122"]],AUTHORITY["EPSG","4313"]],PROJECTION["Lambert_Conformal_Conic_2SP"],PARAMETER["standard_parallel_1",51.16666723333333],PARAMETER["standard_parallel_2",49.8333339],PARAMETER["latitude_of_origin",90],PARAMETER["central_meridian",4.367486666666666],PARAMETER["false_easting",150000.013],PARAMETER["false_northing",5400088.438],UNIT["metre",1,AUTHORITY["EPSG","9001"]],AXIS["X",EAST],AXIS["Y",NORTH],AUTHORITY["EPSG","31370"]]', + xy: [104412.1099068548, 193116.8535417635], + ll: [3.7186701465384533, 51.04642936832842], + }, + { + code: '+proj=lcc +lat_1=51.16666723333333 +lat_2=49.8333339 +lat_0=90 +lon_0=4.367486666666666 +x_0=150000.013 +y_0=5400088.438 +ellps=intl +towgs84=106.869,-52.2978,103.724,-0.33657,0.456955,-1.84218,1 +units=m +no_defs ', + xy: [104588.196404, 193175.582367], + ll: [3.7186701465384533, 51.04642936832842], + }, + { + code: 'PROJCS["JAD2001 / Jamaica Metric Grid",GEOGCS["JAD2001",DATUM["Jamaica_2001",SPHEROID["WGS 84",6378137,298.257223563,AUTHORITY["EPSG","7030"]],TOWGS84[0,0,0,0,0,0,0],AUTHORITY["EPSG","6758"]],PRIMEM["Greenwich",0,AUTHORITY["EPSG","8901"]],UNIT["degree",0.0174532925199433,AUTHORITY["EPSG","9122"]],AUTHORITY["EPSG","4758"]],PROJECTION["Lambert_Conformal_Conic_1SP"],PARAMETER["latitude_of_origin",18],PARAMETER["central_meridian",-77],PARAMETER["scale_factor",1],PARAMETER["false_easting",750000],PARAMETER["false_northing",650000],UNIT["metre",1,AUTHORITY["EPSG","9001"]],AXIS["Easting",EAST],AXIS["Northing",NORTH],AUTHORITY["EPSG","3448"]]', + xy: [7578825.28673236, 11374595.814939449], + ll: [44.2312, 76.486], + }, + { + code: '+proj=etmerc +lat_0=49 +lon_0=-2 +k=0.9996012717 +x_0=400000 +y_0=-100000 +ellps=airy +datum=OSGB36 +units=m +no_defs', + ll: [-3.20078, 55.96056], + xy: [325132.0089586496, 674822.638235305], + }, + { + code: '+proj=krovak +lat_0=49.5 +lon_0=24.83333333333333 +alpha=30.28813972222222 +k=0.9999 +x_0=0 +y_0=0 +ellps=bessel +pm=greenwich +units=m +no_defs +towgs84=570.8,85.7,462.8,4.998,1.587,5.261,3.56', + ll: [12.806988, 49.452262], + xy: [-868208.61, -1095793.64], + }, + { + code: '+proj=etmerc +lat_0=40.5 +lon_0=-110.0833333333333 +k=0.9999375 +x_0=800000.0000101599 +y_0=99999.99998983997 +ellps=GRS80 +towgs84=0,0,0,0,0,0,0 +units=us-ft +no_defs', + ll: [-110.8, 43.5], + xy: [2434515.87, 1422072.711], + }, + // QSC WGS84 + { + code: '+proj=qsc +lat_0=0 +lon_0=0 +units=m +datum=WGS84', + ll: [2, 1], + xy: [304638.4508447283296846, 164123.8709293559950311], + }, + { + code: '+proj=qsc +lat_0=0 +lon_0=90 +units=m +datum=WGS84', + ll: [2, 1], + xy: [-11576764.4717786349356174, 224687.8649776891397778], + }, + { + code: '+proj=qsc +lat_0=0 +lon_0=180 +units=m +datum=WGS84', + ll: [2, 1], + xy: [-15631296.4526007361710072, 8421356.1168374437838793], + }, + { + code: '+proj=qsc +lat_0=0 +lon_0=-90 +units=m +datum=WGS84', + ll: [2, 1], + xy: [11988027.598701536655426, 232669.8736086514254566], + }, + { + code: '+proj=qsc +lat_0=90 +lon_0=0 +units=m +datum=WGS84', + ll: [2, 1], + xy: [456180.4073964518611319, -11678366.591438926756382], + }, + { + code: '+proj=qsc +lat_0=-90 +lon_0=0 +units=m +datum=WGS84', + ll: [2, 1], + xy: [464158.3228444084525108, 11882603.8180405404418707], + }, + // QSC WGS84 WKT + { + code: 'PROJCS["unnamed",GEOGCS["WGS 84",DATUM["unknown",SPHEROID["WGS84",6378137,298.257223563]],PRIMEM["Greenwich",0],UNIT["degree",0.0174532925199433]],PROJECTION["Quadrilateralized_Spherical_Cube"],PARAMETER["latitude_of_origin",0],PARAMETER["central_meridian",0],UNIT["Meter",1]]', + ll: [2, 1], + xy: [304638.4508447283296846, 164123.8709293559950311], + }, + { + code: 'PROJCS["unnamed",GEOGCS["WGS 84",DATUM["unknown",SPHEROID["WGS84",6378137,298.257223563]],PRIMEM["Greenwich",0],UNIT["degree",0.0174532925199433]],PROJECTION["Quadrilateralized_Spherical_Cube"],PARAMETER["latitude_of_origin",0],PARAMETER["central_meridian",90],UNIT["Meter",1]]', + ll: [2, 1], + xy: [-11576764.4717786349356174, 224687.8649776891397778], + }, + { + code: 'PROJCS["unnamed",GEOGCS["WGS 84",DATUM["unknown",SPHEROID["WGS84",6378137,298.257223563]],PRIMEM["Greenwich",0],UNIT["degree",0.0174532925199433]],PROJECTION["Quadrilateralized_Spherical_Cube"],PARAMETER["latitude_of_origin",0],PARAMETER["central_meridian",180],UNIT["Meter",1]]', + ll: [2, 1], + xy: [-15631296.4526007361710072, 8421356.1168374437838793], + }, + { + code: 'PROJCS["unnamed",GEOGCS["WGS 84",DATUM["unknown",SPHEROID["WGS84",6378137,298.257223563]],PRIMEM["Greenwich",0],UNIT["degree",0.0174532925199433]],PROJECTION["Quadrilateralized_Spherical_Cube"],PARAMETER["latitude_of_origin",0],PARAMETER["central_meridian",-90],UNIT["Meter",1]]', + ll: [2, 1], + xy: [11988027.598701536655426, 232669.8736086514254566], + }, + { + code: 'PROJCS["unnamed",GEOGCS["WGS 84",DATUM["unknown",SPHEROID["WGS84",6378137,298.257223563]],PRIMEM["Greenwich",0],UNIT["degree",0.0174532925199433]],PROJECTION["Quadrilateralized_Spherical_Cube"],PARAMETER["latitude_of_origin",90],PARAMETER["central_meridian",0],UNIT["Meter",1]]', + ll: [2, 1], + xy: [456180.4073964518611319, -11678366.591438926756382], + }, + { + code: 'PROJCS["unnamed",GEOGCS["WGS 84",DATUM["unknown",SPHEROID["WGS84",6378137,298.257223563]],PRIMEM["Greenwich",0],UNIT["degree",0.0174532925199433]],PROJECTION["Quadrilateralized_Spherical_Cube"],PARAMETER["latitude_of_origin",-90],PARAMETER["central_meridian",0],UNIT["Meter",1]]', + ll: [2, 1], + xy: [464158.3228444084525108, 11882603.8180405404418707], + }, + // QSC Mars + { + code: '+proj=qsc +units=m +a=3396190 +b=3376200 +lat_0=0 +lon_0=0', + ll: [2, 1], + xy: [162139.9347801624389831, 86935.6184961361577734], + }, + { + code: '+proj=qsc +units=m +a=3396190 +b=3376200 +lat_0=0 +lon_0=90', + ll: [2, 1], + xy: [-6164327.7345527401193976, 119033.1141843862715177], + }, + { + code: '+proj=qsc +units=m +a=3396190 +b=3376200 +lat_0=0 +lon_0=180', + ll: [2, 1], + xy: [-8327904.7183852149173617, 4465226.5862284321337938], + }, + { + code: '+proj=qsc +units=m +a=3396190 +b=3376200 +lat_0=0 +lon_0=-90', + ll: [2, 1], + xy: [6383315.0547841880470514, 123261.7574065744993277], + }, + { + code: '+proj=qsc +units=m +a=3396190 +b=3376200 +lat_0=90 +lon_0=0', + ll: [2, 1], + xy: [242914.9289354820502922, -6218701.0766915259882808], + }, + { + code: '+proj=qsc +units=m +a=3396190 +b=3376200 +lat_0=-90 +lon_0=0', + ll: [2, 1], + xy: [247141.3965058987669181, 6326900.0192015860229731], + }, + // Robinson + { + code: '+proj=robin +lon_0=0 +x_0=0 +y_0=0 +datum=WGS84 +units=m +no_defs', + ll: [-15, -35], + xy: [-1335949.91, -3743319.07], + acc: { ll: 4, xy: 0 }, + }, + { + code: '+proj=robin +lon_0=0 +x_0=0 +y_0=0 +datum=WGS84 +units=m +no_defs', + ll: [-10, 50], + xy: [-819964.6, 5326895.52], + acc: { ll: 4, xy: 0 }, + }, + { + code: '+proj=robin +a=6400000', + ll: [80, -20], + xy: [7449059.8, -2146370.56], + acc: { ll: 4, xy: 0 }, + }, + { + code: '+proj=robin +lon_0=15 +x_0=100000 +y_0=100000 +datum=WGS84', + ll: [-35, 40], + xy: [-4253493.26, 4376351.58], + acc: { ll: 4, xy: 0 }, + }, + { + code: 'PROJCS["World_Robinson",GEOGCS["GCS_WGS_1984",DATUM["WGS_1984",SPHEROID["WGS_1984",6378137,298.257223563]],PRIMEM["Greenwich",0],UNIT["Degree",0.017453292519943295]],PROJECTION["Robinson"],PARAMETER["False_Easting",0],PARAMETER["False_Northing",0],PARAMETER["Central_Meridian",0],UNIT["Meter",1]]', + ll: [20, 40], + xy: [1741397.3, 4276351.58], + acc: { ll: 4, xy: 0 }, + }, + { + code: 'PROJCS["World_Robinson",GEOGCS["GCS_WGS_1984",DATUM["WGS_1984",SPHEROID["WGS_1984",6378137,298.257223563]],PRIMEM["Greenwich",0],UNIT["Degree",0.017453292519943295]],PROJECTION["Robinson"],PARAMETER["False_Easting",100000],PARAMETER["False_Northing",100000],PARAMETER["Central_Meridian",15],UNIT["Meter",1]]', + ll: [-35, 40], + xy: [-4253493.26, 4376351.58], + acc: { ll: 4, xy: 0 }, + }, + { + code: '+proj=robin +lon_0=162 +x_0=0 +y_0=0 +ellps=WGS84 +datum=WGS84 +units=m +no_defs', + ll: [-90, 22], + xy: [9987057.08, 2352946.55], + acc: { ll: 4, xy: 0 }, + }, + // check that coordinates at 180 and -180 deg. longitude don't wrap around + { + code: '+title=WGS 84 / Pseudo-Mercator +proj=merc +a=6378137 +b=6378137 +lat_ts=0.0 +lon_0=0.0 +x_0=0.0 +y_0=0 +k=1.0 +units=m +nadgrids=@null +no_defs', + ll: [-180, 0], + xy: [-20037508.342789, 0], + }, + { + code: '+title=WGS 84 / Pseudo-Mercator +proj=merc +a=6378137 +b=6378137 +lat_ts=0.0 +lon_0=0.0 +x_0=0.0 +y_0=0 +k=1.0 +units=m +nadgrids=@null +no_defs', + ll: [180, 0], + xy: [20037508.342789, 0], + }, + // these test cases are taken from mapshaper-proj and the test results match + { + code: '+proj=etmerc +ellps=GRS80 +lat_1=0.5 +lat_2=2 +n=0.5', + ll: [2, 1], + xy: [222650.79679577847, 110642.2294119271], + }, + { + code: '+proj=etmerc +approx +a=6400000 +lat_1=0.5 +lat_2=2 +n=0.5', + ll: [2, 1], + xy: [223413.46640632232, 111769.14504059685], + }, + { + code: '+proj=etmerc +zone=30 +ellps=GRS80 +lat_1=0.5 +lat_2=2 +n=0.5', + ll: [2, 1], + xy: [222650.7967975856, 110642.2294119332], + }, + { + code: '+proj=etmerc +k=0.998 +lon_0=-20 +datum=WGS84 +x_0=10000 +y_0=20000', + ll: [2, 1], + xy: [2516532.477709202, 139083.35793371277], + }, + { + code: '+proj=utm +zone=30 +ellps=GRS80 +lat_1=0.5 +lat_2=2 +n=0.5', + ll: [2, 1], + xy: [1057002.405491298, 110955.14117594929], + }, + { + code: '+proj=utm +lon_0=-3 +ellps=GRS80 +lat_1=0.5 +lat_2=2 +n=0.5', + ll: [2, 1], + xy: [1057002.4052152266, 110955.14117382761], + }, + // these test cases are related to the original issue on GitHub + { + code: '+proj=utm +zone=33 +datum=WGS84 +units=m +no_defs', + ll: [2, 1], + xy: [-959006.4926646841, 113457.31956265299], + }, + { + code: '+proj=utm +zone=33 +datum=WGS84 +units=m +no_defs', + ll: [31, 70], + xy: [1104629.4356366363, 7845845.077685604], + }, + // these test cases are for Norway snow flake zones + { + code: '+proj=utm +zone=31 +datum=WGS84 +units=m +no_defs', + ll: [59.121778, 1.508527], + xy: [8089746.634775677, 301230.8618526573], + }, + { + code: '+proj=utm +zone=32 +datum=WGS84 +units=m +no_defs', + ll: [59.121778, 1.508527], + xy: [6969865.865375574, 261237.08330733588], + }, + { + code: '+proj=utm +zone=33 +datum=WGS84 +units=m +no_defs', + ll: [59.121778, 1.508527], + xy: [5984417.050333044, 232959.75386279594], + }, + { + code: '+proj=utm +zone=34 +datum=WGS84 +units=m +no_defs', + ll: [79.070672, 20.520579], + xy: [7421462.108989433, 3922366.25143021], + }, + { + code: '+proj=utm +zone=35 +datum=WGS84 +units=m +no_defs', + ll: [79.070672, 20.520579], + xy: [6548241.281523044, 3478520.1422119136], + }, + // these test cases are for the margin zones 1 and 60 + { + code: '+proj=utm +zone=1 +datum=WGS84 +units=m +no_defs', + ll: [-177, 60], + xy: [500000, 6651411.190362714], + }, + { + code: '+proj=utm +zone=60 +datum=WGS84 +units=m +no_defs', + ll: [177, 60], + xy: [500000.0000000014, 6651411.190362714], + }, + { + code: '+proj=lcc +lat_1=46.8 +lat_0=46.8 +lon_0=0 +k_0=0.99987742 +x_0=600000 +y_0=2200000 +a=6378249.2 +b=6356515 +towgs84=-168,-60,320,0,0,0,0 +pm=paris +units=m +no_defs', + ll: [1.4477496, 46.8692953], + xy: [532247.2835623875, 2208091.8723], + }, + { + code: '+proj=utm +zone=33 +units=m +no_defs', + ll: [2, 1], + xy: [-959006.4926646841, 113457.31956265299], + }, + { + code: '+proj=utm +zone=33 +units=m', + ll: [2, 1], + xy: [-959006.4926646841, 113457.31956265299], + }, + { + code: '+proj=utm +zone=33', + ll: [2, 1], + xy: [-959006.4926646841, 113457.31956265299], + }, + { + code: 'PROJCS["CUSTOM_OBLIQUE_MERCATOR", GEOGCS["WGS 84", DATUM["World Geodetic System 1984", SPHEROID["WGS 84", 6378137.0, 298.257223563]], PRIMEM["Greenwich", 0.0], UNIT["degree", 0.017453292519943295], AXIS["Geodetic latitude", NORTH], AXIS["Geodetic longitude", EAST]], PROJECTION["Hotine_Oblique_Mercator_Azimuth_Center", AUTHORITY["EPSG", "9815"]], PARAMETER["latitude_of_center", 37.50832038], PARAMETER["longitude_of_center", -122.25064809], PARAMETER["azimuth", 45.0], PARAMETER["rectified_grid_angle", -3.99], PARAMETER["scale_factor", 1.0], PARAMETER["false_easting", -361.25], PARAMETER["false_northing", 254.915], UNIT["foot", 0.3048], AXIS["Easting", EAST], AXIS["Northing", NORTH]]', + xy: [-361.2499999983702, 254.91500000283122], + ll: [-122.25064809, 37.50832038], + acc: { + ll: 3, + xy: 8, + }, + }, + // Omerc Type A - #273 + { + code: '+proj=omerc +lat_0=4 +lonc=102.25 +alpha=323.0257964666666 +k=0.99984 +x_0=804671 +y_0=0 +no_uoff +gamma=323.1301023611111 +ellps=GRS80 +units=m +no_defs', + xy: [412597.532715, 338944.957259], + ll: [101.70979078430528, 3.06268465621428], + acc: { + ll: 2, + xy: -3, + }, + }, + { + code: 'PROJCS["GDM2000 / Peninsula RSO", GEOGCS["GDM2000", DATUM["Geodetic_Datum_of_Malaysia_2000", SPHEROID["GRS 1980",6378137,298.257222101, AUTHORITY["EPSG","7019"]], AUTHORITY["EPSG","6742"]], PRIMEM["Greenwich",0, AUTHORITY["EPSG","8901"]], UNIT["degree",0.0174532925199433, AUTHORITY["EPSG","9122"]], AUTHORITY["EPSG","4742"]], PROJECTION["Hotine_Oblique_Mercator"], PARAMETER["latitude_of_center",4], PARAMETER["longitude_of_center",102.25], PARAMETER["azimuth",323.0257964666666], PARAMETER["rectified_grid_angle",323.1301023611111], PARAMETER["scale_factor",0.99984], PARAMETER["false_easting",804671], PARAMETER["false_northing",0], UNIT["metre",1, AUTHORITY["EPSG","9001"]], AXIS["Easting",EAST], AXIS["Northing",NORTH], AUTHORITY["EPSG","3375"]]', + xy: [412597.532715, 338944.957259], + ll: [101.70979078430528, 3.06268465621428], + acc: { + ll: 7, + xy: 6, + }, + }, + // EPSG:3468 + { + code: '+proj=omerc +lat_0=57 +lonc=-133.6666666666667 +alpha=323.1301023611111 +k=0.9999 +x_0=5000000 +y_0=-5000000 +no_uoff +gamma=323.1301023611111 +ellps=GRS80 +towgs84=0,0,0,0,0,0,0 +units=m +no_defs', + xy: [1264314.74, -763162.04], + ll: [-128.115000029, 44.8150000066], + acc: { + ll: 9, + xy: 4, + }, + }, + { + code: 'PROJCS["NAD83(NSRS2007) / Alaska zone 1", GEOGCS["NAD83(NSRS2007)", DATUM["NAD83_National_Spatial_Reference_System_2007", SPHEROID["GRS 1980",6378137,298.257222101, AUTHORITY["EPSG","7019"]], TOWGS84[0,0,0,0,0,0,0], AUTHORITY["EPSG","6759"]], PRIMEM["Greenwich",0, AUTHORITY["EPSG","8901"]], UNIT["degree",0.0174532925199433, AUTHORITY["EPSG","9122"]], AUTHORITY["EPSG","4759"]], PROJECTION["Hotine_Oblique_Mercator"], PARAMETER["latitude_of_center",57], PARAMETER["longitude_of_center",-133.6666666666667], PARAMETER["azimuth",323.1301023611111], PARAMETER["rectified_grid_angle",323.1301023611111], PARAMETER["scale_factor",0.9999], PARAMETER["false_easting",5000000], PARAMETER["false_northing",-5000000], UNIT["metre",1, AUTHORITY["EPSG","9001"]], AXIS["X",EAST], AXIS["Y",NORTH], AUTHORITY["EPSG","3468"]]', + xy: [1264314.74, -763162.04], + ll: [-128.115000029, 44.8150000066], + acc: { + ll: 9, + xy: 4, + }, + }, + // Omerc Type B - #308 + { + code: '+proj=omerc +lat_0=37.4769061 +lonc=141.0039618 +alpha=202.22 +k=1 +x_0=138 +y_0=77.65 +ellps=WGS84 +towgs84=0,0,0,0,0,0,0 +units=m +no_defs', + xy: [168.2438, 64.1736], + ll: [141.003611, 37.476802], + acc: { + ll: 9, + xy: 4, + }, + }, + { + code: 'PROJCS["UNK / Oblique_Mercator",GEOGCS["UNK",DATUM["Unknown datum",SPHEROID["WGS 84", 6378137.0, 298.257223563]],PRIMEM["Greenwich",0],UNIT["degree",0.017453292519943295]],PROJECTION["Oblique_Mercator"],PARAMETER["latitude_of_center",37.4769061],PARAMETER["longitude_of_center",141.0039618],PARAMETER["central_meridian",141.0039618],PARAMETER["azimuth",202.22],PARAMETER["scale_factor",1],PARAMETER["false_easting",138],PARAMETER["false_northing",77.65],UNIT["Meter",1]]', + xy: [168.2438, 64.1736], + ll: [141.003611, 37.476802], + acc: { + ll: 9, + xy: 4, + }, + }, + // Test with Feet + { + code: 'PROJCS["UNK / Oblique_Mercator",GEOGCS["UNK",DATUM["Unknown datum",SPHEROID["WGS 84", 6378137.0, 298.257223563]],PRIMEM["Greenwich",0],UNIT["degree",0.017453292519943295]],PROJECTION["Oblique_Mercator"],PARAMETER["latitude_of_center",37.4769061],PARAMETER["longitude_of_center",141.0039618],PARAMETER["central_meridian",141.0039618],PARAMETER["azimuth",202.22],PARAMETER["scale_factor",1],PARAMETER["false_easting",138],PARAMETER["false_northing",77.65],UNIT["Foot_US",0.3048006096012192]]', + xy: [237.22488871325027, 33.43626458451221], + ll: [141.003611, 37.476802], + }, + // { + // code: 'PROJCS["WGS 84 / Pseudo-Mercator", GEOGCS["WGS 84", DATUM["World Geodetic System 1984", SPHEROID["WGS 84", 6378137.0, 0, AUTHORITY["EPSG","7030"]], AUTHORITY["EPSG","6326"]], PRIMEM["Greenwich", 0.0, AUTHORITY["EPSG","8901"]], UNIT["degree", 0.017453292519943295], AXIS["Geodetic latitude", NORTH], AXIS["Geodetic longitude", EAST], AUTHORITY["EPSG","4326"]], PROJECTION["Popular Visualisation Pseudo Mercator", AUTHORITY["EPSG","1024"]], PARAMETER["semi_minor", 6378137.0], PARAMETER["latitude_of_origin", 0.0], PARAMETER["central_meridian", 0.0], PARAMETER["scale_factor", 1.0], PARAMETER["false_easting", 0.0], PARAMETER["false_northing", 0.0], UNIT["m", 1.0], AXIS["Easting", EAST], AXIS["Northing", NORTH], AUTHORITY["EPSG","3857"]]', + // xy: [-12523490.49256873, 5166512.50707369], + // ll: [-112.50042920000004, 42.036926809999976], + // acc: { + // ll: -2, + // xy: -2, + // }, + // }, + // { + // code: 'PROJCS["WGS 84 / Pseudo-Mercator",GEOGCS["WGS 84",DATUM["WGS_1984",SPHEROID["WGS 84",6378137,298.257223563,AUTHORITY["EPSG","7030"]],AUTHORITY["EPSG","6326"]],PRIMEM["Greenwich",0,AUTHORITY["EPSG","8901"]],UNIT["degree",0.0174532925199433,AUTHORITY["EPSG","9122"]],AUTHORITY["EPSG","4326"]],PROJECTION["Mercator_1SP"],PARAMETER["central_meridian",0],PARAMETER["scale_factor",1],PARAMETER["false_easting",0],PARAMETER["false_northing",0],UNIT["metre",1,AUTHORITY["EPSG","9001"]],AXIS["X",EAST],AXIS["Y",NORTH],EXTENSION["PROJ4","+proj=merc +a=6378137 +b=6378137 +lat_ts=0.0 +lon_0=0.0 +x_0=0.0 +y_0=0 +k=1.0 +units=m +nadgrids=@null +wktext +no_defs"],AUTHORITY["EPSG","9999"]]', + // xy: [-12523490.49256873, 5166512.50707369], + // ll: [-112.50042920000004, 42.036926809999976], + // }, + { + code: '+proj=geocent +datum=WGS84 +units=m +no_defs', + ll: [-7.56234, 38.96618, 0], + xy: [4922499, -653508, 3989398], + acc: { + ll: 0, + xy: -1, + }, + }, + { + code: '+proj=geocent +ellps=GRS80 +units=m +no_defs', + ll: [-7.56234, 38.96618, 1], + xy: [4922499, -653508, 3989399], + acc: { + ll: -1, + xy: -1, + }, + }, + { + code: '+proj=tpers +a=6400000 +h=1000000 +azi=20', + ll: [2, 1], + xy: [170820.288955531, 180460.865555805], + acc: { + ll: 5, + xy: -1, + }, + }, + { + code: '+proj=tpers +a=6400000 +h=1000000 +azi=20', + ll: [2, -1], + xy: [246853.941538942, -28439.878035775], + acc: { + ll: 5, + xy: -1, + }, + }, + { + code: '+proj=tpers +a=6400000 +h=1000000 +azi=20', + ll: [-2, 1], + xy: [-246853.941538942, 28439.878035775], + acc: { + ll: 5, + xy: -1, + }, + }, + { + code: '+proj=tpers +a=6400000 +h=1000000 +azi=20', + ll: [-2, -1], + xy: [-170820.288955531, -180460.865555805], + acc: { + ll: 5, + xy: -1, + }, + }, + { + code: '+proj=tpers +a=6400000 +h=1000000 +tilt=20', + ll: [2, 1], + xy: [213598.340357101, 113687.930830744], + acc: { + ll: 5, + xy: -1, + }, + }, + { + code: '+proj=tpers +a=6400000 +h=1000000 +tilt=20', + ll: [2, -1], + xy: [231609.982792523, -123274.645577324], + acc: { + ll: 5, + xy: -1, + }, + }, + { + code: '+proj=tpers +a=6400000 +h=1000000 +tilt=20', + ll: [-2, 1], + xy: [-213598.340357101, 113687.930830744], + acc: { + ll: 5, + xy: -1, + }, + }, + { + code: '+proj=tpers +a=6400000 +h=1000000 +tilt=20', + ll: [-2, -1], + xy: [-231609.982792523, -123274.645577324], + acc: { + ll: 5, + xy: -1, + }, + }, + // Geostationary - Ellipsoid - X Sweep + // { + // code: '+proj=geos +sweep=x +lon_0=-75 +h=35786023 +a=6378137.0 +b=6356752.314', + // ll: [-95, 25], + // xy: [-1920508.77, 2605680.03], + // }, + // Geostationary - Ellipsoid - Y Sweep + { + code: '+proj=geos +sweep=y +lon_0=-75 +h=35786023 +a=6378137.0 +b=6356752.314', + ll: [-95, 25], + xy: [-1925601.2, 2601922.01], + }, + // Geostationary - Sphere - X Sweep + { + code: '+proj=geos +sweep=x +lon_0=-75 +h=35786023 +a=6378137.0 +b=6378137.0', + ll: [-95, 25], + xy: [-1919131.48, 2621384.15], + }, + // Geostationary - Sphere - Y Sweep + { + code: '+proj=geos +sweep=y +lon_0=-75 +h=35786023 +a=6378137.0 +b=6378137.0', + ll: [-95, 25], + xy: [-1924281.93, 2617608.82], + }, + // WKT - Arctic Polar Stereographic + { + code: 'PROJCS["WGS 84 / Arctic Polar Stereographic",GEOGCS["WGS 84",DATUM["WGS_1984",SPHEROID["WGS 84",6378137,298.257223563,AUTHORITY["EPSG","7030"]],AUTHORITY["EPSG","6326"]],PRIMEM["Greenwich",0,AUTHORITY["EPSG","8901"]],UNIT["degree",0.0174532925199433,AUTHORITY["EPSG","9122"]],AUTHORITY["EPSG","4326"]],PROJECTION["Polar_Stereographic"],PARAMETER["latitude_of_origin",71],PARAMETER["central_meridian",0],PARAMETER["false_easting",0],PARAMETER["false_northing",0],UNIT["metre",1,AUTHORITY["EPSG","9001"]],AUTHORITY["EPSG","3995"]]', + ll: [0, 90], + xy: [0, 0], + }, + { + code: 'PROJCS["WGS 84 / Arctic Polar Stereographic",GEOGCS["WGS 84",DATUM["WGS_1984",SPHEROID["WGS 84",6378137,298.257223563,AUTHORITY["EPSG","7030"]],AUTHORITY["EPSG","6326"]],PRIMEM["Greenwich",0,AUTHORITY["EPSG","8901"]],UNIT["degree",0.0174532925199433,AUTHORITY["EPSG","9122"]],AUTHORITY["EPSG","4326"]],PROJECTION["Polar_Stereographic"],PARAMETER["latitude_of_origin",71],PARAMETER["central_meridian",0],PARAMETER["false_easting",0],PARAMETER["false_northing",0],UNIT["metre",1,AUTHORITY["EPSG","9001"]],AUTHORITY["EPSG","3995"]]', + ll: [0, 0], + xy: [0, -12367396.218459858], + }, + // WKT - Antarctic Polar Stereographic + { + code: 'PROJCS["WGS 84 / Antarctic Polar Stereographic",GEOGCS["WGS 84",DATUM["WGS_1984",SPHEROID["WGS 84",6378137,298.257223563,AUTHORITY["EPSG","7030"]],AUTHORITY["EPSG","6326"]],PRIMEM["Greenwich",0,AUTHORITY["EPSG","8901"]],UNIT["degree",0.01745329251994328,AUTHORITY["EPSG","9122"]],AUTHORITY["EPSG","4326"]],UNIT["metre",1,AUTHORITY["EPSG","9001"]],PROJECTION["Polar_Stereographic"],PARAMETER["latitude_of_origin",-71],PARAMETER["central_meridian",0],PARAMETER["scale_factor",1],PARAMETER["false_easting",0],PARAMETER["false_northing",0],AUTHORITY["EPSG","3031"]]', + ll: [0, -90], + xy: [0, 0], + }, + { + code: 'PROJCS["WGS 84 / Antarctic Polar Stereographic",GEOGCS["WGS 84",DATUM["WGS_1984",SPHEROID["WGS 84",6378137,298.257223563,AUTHORITY["EPSG","7030"]],AUTHORITY["EPSG","6326"]],PRIMEM["Greenwich",0,AUTHORITY["EPSG","8901"]],UNIT["degree",0.01745329251994328,AUTHORITY["EPSG","9122"]],AUTHORITY["EPSG","4326"]],UNIT["metre",1,AUTHORITY["EPSG","9001"]],PROJECTION["Polar_Stereographic"],PARAMETER["latitude_of_origin",-71],PARAMETER["central_meridian",0],PARAMETER["scale_factor",1],PARAMETER["false_easting",0],PARAMETER["false_northing",0],AUTHORITY["EPSG","3031"]]', + ll: [0, 0], + xy: [0, 12367396.218459858], + }, + { + code: '+proj=eqearth +lon_0=0 +x_0=0 +y_0=0 +R=6371008.7714 +units=m +no_defs +type=crs', + ll: [16, 48], + xy: [1284600.7230114893, 5794915.366010354], + }, + { + code: '+proj=eqearth +lon_0=150 +x_0=0 +y_0=0 +R=6371008.7714 +units=m +no_defs +type=crs', + ll: [16, 48], + xy: [-10758531.055221224, 5794915.366010354], + }, + { + code: '+proj=bonne +lat_1=10 +lon_0=10', + ll: [4.9, 52.366667], + xy: [-347381.937958562, 4700204.94589969], + }, + { + code: '+proj=bonne +a=6400000 +lat_1=0.5 +lat_2=2', + ll: [2, 1], + xy: [223368.11557252839, 55884.555246393575], + }, + { + code: '+proj=bonne +ellps=GRS80 +lat_1=0.5 +lat_2=2', + ll: [2, 1], + xy: [222605.29609715697, 55321.139565494814], + }, + { + code: '+proj=cea +lat_ts=30 +lon_0=0 +x_0=0 +y_0=0 +datum=WGS84 +units=m +no_defs +type=crs', + ll: [-112.50042920000004, 42.036926809999976], + xy: [-10854747.940137345, 4904271.142640647], + acc: { + ll: 5, + xy: -1, + }, + }, + { + code: '+proj=cea +lat_ts=30 +lon_0=0 +x_0=0 +y_0=0 +R=6371008.7714 +datum=WGS84 +units=m +no_defs +type=crs', + ll: [-112.50042920000004, 42.036926809999976], + xy: [-10833539.761402687, 4907780.580660692], + acc: { + ll: 3, + xy: -1, + }, + }, + // { + // code: '+proj=gstmerc +lon_0=0 +x_0=0 +y_0=0 +R=6371008.7714 +datum=WGS84 +units=m +no_defs', + // ll: [-112.50042920000004, 42.036926809999976], + // xy: [-5380950.902080743, -7434667.860047833], + // acc: { + // ll: 3, + // xy: -1, + // }, + // }, + { + code: '+proj=ortho +lon_0=30 +lat_0=30 +x_0=0 +y_0=0 +datum=WGS84 +units=m +no_defs', + ll: [-12, 12], + xy: [-4174544.862725822, -1169723.9077304942], + }, + { + code: '+proj=somerc +lat_0=46.95240555555556 +lon_0=7.439583333333333 +k_0=1 +x_0=2600000 +y_0=1200000 +ellps=bessel +towgs84=674.374,15.056,405.346,0,0,0,0 +units=m +no_defs', + ll: [-12, 12], + xy: [85532.74540182156, -2564906.626626396], + }, +]; diff --git a/tests/readers/csv/fixtures/basic.csv b/tests/readers/csv/fixtures/basic.csv new file mode 100644 index 00000000..f7d1941c --- /dev/null +++ b/tests/readers/csv/fixtures/basic.csv @@ -0,0 +1,3 @@ +lat, lon, name +1, 2, 3 +3.2, 1.1, a diff --git a/tests/readers/csv/fixtures/basic3D.csv b/tests/readers/csv/fixtures/basic3D.csv new file mode 100644 index 00000000..651f6feb --- /dev/null +++ b/tests/readers/csv/fixtures/basic3D.csv @@ -0,0 +1,3 @@ +Latitude, Longitude, height, name +1, 2, 55, 3 +3.2, 1.1, -2.2, a diff --git a/tests/readers/csv/fixtures/degrees.csv b/tests/readers/csv/fixtures/degrees.csv new file mode 100644 index 00000000..29b3eee9 --- /dev/null +++ b/tests/readers/csv/fixtures/degrees.csv @@ -0,0 +1,2 @@ +lat,lon,name +23°N,26°W,Chester \ No newline at end of file diff --git a/tests/readers/csv/index.test.ts b/tests/readers/csv/index.test.ts new file mode 100644 index 00000000..c4a0b19a --- /dev/null +++ b/tests/readers/csv/index.test.ts @@ -0,0 +1,71 @@ +import FileReader from '../../../src/readers/file'; +import { BufferReader, CSVReader } from '../../../src/readers'; +import { expect, test } from 'bun:test'; + +test('CSVReader - basic', async () => { + const file = await Bun.file(`${__dirname}/fixtures/basic.csv`).arrayBuffer(); + const buffer = Buffer.from(file); + const reader = new BufferReader(buffer.buffer, 0, buffer.byteLength); + const csvReader = new CSVReader(reader); + const data = await Array.fromAsync(csvReader); + expect(data).toEqual([ + { + geometry: { + coordinates: { x: 2, y: 1 }, + is3D: false, + type: 'Point', + }, + properties: { + name: '3', + }, + type: 'VectorFeature', + }, + { + geometry: { + coordinates: { x: 1.1, y: 3.2 }, + is3D: false, + type: 'Point', + }, + properties: { + name: 'a', + }, + type: 'VectorFeature', + }, + ]); +}); + +test('CSVReader - 3D', async () => { + const fileReader = new FileReader(`${__dirname}/fixtures/basic3D.csv`); + const csvReader = new CSVReader(fileReader, { + delimiter: ',', + lineDelimiter: '\n', + lonKey: 'Longitude', + latKey: 'Latitude', + heightKey: 'height', + }); + const data = await Array.fromAsync(csvReader); + expect(data).toEqual([ + { + geometry: { + coordinates: { x: 2, y: 1, z: 55 }, + is3D: true, + type: 'Point', + }, + properties: { + name: '3', + }, + type: 'VectorFeature', + }, + { + geometry: { + coordinates: { x: 1.1, y: 3.2, z: -2.2 }, + is3D: true, + type: 'Point', + }, + properties: { + name: 'a', + }, + type: 'VectorFeature', + }, + ]); +}); diff --git a/tests/readers/fileReader.test.ts b/tests/readers/fileReader.test.ts index ce461006..d8f4aa21 100644 --- a/tests/readers/fileReader.test.ts +++ b/tests/readers/fileReader.test.ts @@ -1,4 +1,4 @@ -import FileReader from '../../src/readers/fileReader'; +import FileReader from '../../src/readers/file'; import { expect, test } from 'bun:test'; test('FileReader', () => { diff --git a/tests/readers/json/index.test.ts b/tests/readers/json/index.test.ts index 8e625e73..89fabb38 100644 --- a/tests/readers/json/index.test.ts +++ b/tests/readers/json/index.test.ts @@ -1,5 +1,5 @@ import { BufferReader } from '../../../src/readers'; -import FileReader from '../../../src/readers/fileReader'; +import FileReader from '../../../src/readers/file'; import { BufferJSONReader, JSONReader, @@ -11,7 +11,7 @@ test('BufferJSONReader - string', async () => { const file = await Bun.file(`${__dirname}/fixtures/points.geojson`).arrayBuffer(); const buffer = Buffer.from(file); const reader = new BufferJSONReader(buffer.toString('utf-8')); - const data = [...reader.iterate()]; + const data = await Array.fromAsync(reader); expect(data).toEqual([ { geometry: { coordinates: [144.9584, -37.8173], type: 'Point' }, @@ -34,7 +34,7 @@ test('BufferJSONReader - string', async () => { test('BufferJSONReader - object', async () => { const json = await Bun.file(`${__dirname}/fixtures/points.geojson`).json(); const reader = new BufferJSONReader(json); - const data = [...reader.iterate()]; + const data = await Array.fromAsync(reader); expect(data).toEqual([ { geometry: { coordinates: [144.9584, -37.8173], type: 'Point' }, @@ -58,7 +58,7 @@ test('NewLineDelimitedJSONReader - BufferReader', async () => { const fileBuf = await Bun.file(`${__dirname}/fixtures/points.geojsonld`).arrayBuffer(); const bufReader = new BufferReader(fileBuf); const ldReader = new NewLineDelimitedJSONReader(bufReader); - const data = [...ldReader.iterate()]; + const data = await Array.fromAsync(ldReader); expect(data).toEqual([ { geometry: { coordinates: [144.9584, -37.8173], type: 'Point' }, @@ -81,7 +81,7 @@ test('NewLineDelimitedJSONReader - BufferReader', async () => { test('NewLineDelimitedJSONReader - FileReader', async () => { const fileReader = new FileReader(`${__dirname}/fixtures/points.geojsonld`); const ldReader = new NewLineDelimitedJSONReader(fileReader); - const data = [...ldReader.iterate()]; + const data = await Array.fromAsync(ldReader); fileReader.close(); expect(data).toEqual([ { @@ -106,7 +106,7 @@ test('JSONReader - BufferReader', async () => { const fileBuf = await Bun.file(`${__dirname}/fixtures/points.geojson`).arrayBuffer(); const bufReader = new BufferReader(fileBuf); const reader = new JSONReader(bufReader); - const data = [...reader.iterate()]; + const data = await Array.fromAsync(reader); expect(data).toEqual([ { geometry: { coordinates: [144.9584, -37.8173], type: 'Point' }, @@ -130,7 +130,7 @@ test('JSONReader - BufferReader (forced "large" read)', async () => { const fileBuf = await Bun.file(`${__dirname}/fixtures/points.geojson`).arrayBuffer(); const bufReader = new BufferReader(fileBuf); const reader = new JSONReader(bufReader, 1); - const data = [...reader.iterate()]; + const data = await Array.fromAsync(reader); expect(data).toEqual([ { geometry: { coordinates: [144.9584, -37.8173], type: 'Point' }, @@ -153,7 +153,7 @@ test('JSONReader - BufferReader (forced "large" read)', async () => { test('JSONReader - FileReader', async () => { const fileReader = new FileReader(`${__dirname}/fixtures/points.geojson`); const reader = new JSONReader(fileReader); - const data = [...reader.iterate()]; + const data = await Array.fromAsync(reader); fileReader.close(); expect(data).toEqual([ { @@ -178,7 +178,7 @@ test('JSONReader - BufferReader', async () => { const fileBuf = await Bun.file(`${__dirname}/fixtures/point-feature.geojson`).arrayBuffer(); const bufReader = new BufferReader(fileBuf); const reader = new JSONReader(bufReader); - const data = [...reader.iterate()]; + const data = await Array.fromAsync(reader); expect(data).toEqual([ { geometry: { coordinates: [144.9584, -37.8173], type: 'Point' }, diff --git a/tests/readers/mmapReader.test.ts b/tests/readers/mmapReader.test.ts index 7f0640f5..08b3031f 100644 --- a/tests/readers/mmapReader.test.ts +++ b/tests/readers/mmapReader.test.ts @@ -1,4 +1,4 @@ -import MMapReader from '../../src/readers/mmapReader'; +import MMapReader from '../../src/readers/mmap'; import { expect, test } from 'bun:test'; test('MMapReader', () => { diff --git a/tests/readers/osm/index.test.ts b/tests/readers/osm/index.test.ts index e6e936a8..45a7c61d 100644 --- a/tests/readers/osm/index.test.ts +++ b/tests/readers/osm/index.test.ts @@ -1,4 +1,4 @@ -import FileReader from '../../../src/readers/fileReader'; +import FileReader from '../../../src/readers/file'; import { OSMReader } from '../../../src/readers/osm'; import { expect, test } from 'bun:test'; @@ -17,7 +17,7 @@ test('parse basic case', async () => { source: undefined, writingprogram: undefined, }); - const features = await Array.fromAsync(reader.iterate()); + const features = await Array.fromAsync(reader); expect(features.length).toBe(8); expect(features).toEqual([ diff --git a/tests/readers/pmtiles/fixtures/cb_2018_us_zcta510_500k_nolimit.pmtiles b/tests/readers/pmtiles/fixtures/cb_2018_us_zcta510_500k_nolimit.pmtiles new file mode 100644 index 00000000..29201474 Binary files /dev/null and b/tests/readers/pmtiles/fixtures/cb_2018_us_zcta510_500k_nolimit.pmtiles differ diff --git a/tests/readers/pmtiles/fixtures/empty.pmtiles b/tests/readers/pmtiles/fixtures/empty.pmtiles new file mode 100644 index 00000000..e69de29b diff --git a/tests/readers/pmtiles/fixtures/invalid.pmtiles b/tests/readers/pmtiles/fixtures/invalid.pmtiles new file mode 100644 index 00000000..f2b720ba --- /dev/null +++ b/tests/readers/pmtiles/fixtures/invalid.pmtiles @@ -0,0 +1 @@ +This is an invalid tile archive, a test case to make sure that the code throws an error, but it needs to be the minimum size to pass the first test diff --git a/tests/readers/pmtiles/fixtures/s2.s2pmtiles b/tests/readers/pmtiles/fixtures/s2.s2pmtiles new file mode 100644 index 00000000..71dd84eb Binary files /dev/null and b/tests/readers/pmtiles/fixtures/s2.s2pmtiles differ diff --git a/tests/readers/pmtiles/fixtures/test_fixture_1.pmtiles b/tests/readers/pmtiles/fixtures/test_fixture_1.pmtiles new file mode 100644 index 00000000..c86db1f2 Binary files /dev/null and b/tests/readers/pmtiles/fixtures/test_fixture_1.pmtiles differ diff --git a/tests/readers/pmtiles/fixtures/test_fixture_2.pmtiles b/tests/readers/pmtiles/fixtures/test_fixture_2.pmtiles new file mode 100644 index 00000000..cb19dd5f Binary files /dev/null and b/tests/readers/pmtiles/fixtures/test_fixture_2.pmtiles differ diff --git a/tests/readers/pmtiles/index.test.ts b/tests/readers/pmtiles/index.test.ts new file mode 100644 index 00000000..e69de29b diff --git a/tests/readers/pmtiles/pmtiles.test.ts b/tests/readers/pmtiles/pmtiles.test.ts new file mode 100644 index 00000000..bd067e68 --- /dev/null +++ b/tests/readers/pmtiles/pmtiles.test.ts @@ -0,0 +1,130 @@ +import { + Compression, + HEADER_SIZE_BYTES, + ROOT_SIZE, + TileType, + bytesToHeader, + deserializeDir, + findTile, + getUint64, + tileIDToZxy, + zxyToTileID, +} from '../../../src/readers/pmtiles'; +import { headerToBytes, serializeDir, setUint64 } from '../../../src/writers/pmtiles'; + +import { describe, expect, test } from 'bun:test'; + +import type { Entry, Header } from '../../../src/readers/pmtiles'; + +test('HEADER_SIZE_BYTES', () => { + expect(HEADER_SIZE_BYTES).toBe(127); +}); + +test('ROOT_SIZE', () => { + expect(ROOT_SIZE).toBe(16_384); +}); + +describe('zxyToTileID & tileIDToZxy', () => { + test('zxyToTileID', () => { + expect(zxyToTileID(0, 0, 0)).toBe(0); + expect(zxyToTileID(1, 0, 1)).toBe(2); + expect(zxyToTileID(1, 1, 0)).toBe(4); + expect(zxyToTileID(1, 1, 1)).toBe(3); + expect(zxyToTileID(2, 0, 0)).toBe(5); + expect(zxyToTileID(2, 0, 1)).toBe(8); + expect(zxyToTileID(2, 1, 0)).toBe(6); + expect(zxyToTileID(2, 1, 1)).toBe(7); + expect(zxyToTileID(20, 1_002, 6_969)).toBe(366567509724); + expect(() => zxyToTileID(30, 0, 0)).toThrowError( + 'Tile zoom level exceeds max safe number limit (26)', + ); + expect(() => zxyToTileID(0, 10, 0)).toThrowError('tile x/y outside zoom level bounds'); + }); + + test('tileIDToZxy', () => { + expect(tileIDToZxy(0)).toEqual([0, 0, 0]); + expect(tileIDToZxy(2)).toEqual([1, 0, 1]); + expect(tileIDToZxy(4)).toEqual([1, 1, 0]); + expect(tileIDToZxy(3)).toEqual([1, 1, 1]); + expect(tileIDToZxy(5)).toEqual([2, 0, 0]); + expect(tileIDToZxy(8)).toEqual([2, 0, 1]); + expect(tileIDToZxy(6)).toEqual([2, 1, 0]); + expect(tileIDToZxy(7)).toEqual([2, 1, 1]); + expect(tileIDToZxy(366567509724)).toEqual([20, 1_002, 6_969]); + expect(() => tileIDToZxy(8501199875890165)).toThrowError( + 'Tile zoom level exceeds max safe number limit (26)', + ); + }); +}); + +// bytesToHeader, +// headerToBytes, + +test('bytesToHeader & headerToBytes', () => { + const header: Header = { + specVersion: 1, + rootDirectoryOffset: 20, + rootDirectoryLength: 634, + jsonMetadataOffset: 720, + jsonMetadataLength: 7, + leafDirectoryOffset: 6, + leafDirectoryLength: 100, + tileDataOffset: 5, + tileDataLength: 4, + numAddressedTiles: 3, + numTileEntries: 2, + numTileContents: 1, + clustered: true, + internalCompression: Compression.None, + tileCompression: Compression.Zstd, + tileType: TileType.Pbf, + minZoom: 1, + maxZoom: 10, + }; + const bytes = headerToBytes(header); + expect(bytes).toEqual( + new Uint8Array([ + 80, 77, 0, 0, 0, 0, 0, 1, 20, 0, 0, 0, 0, 0, 0, 0, 122, 2, 0, 0, 0, 0, 0, 0, 208, 2, 0, 0, 0, + 0, 0, 0, 7, 0, 0, 0, 0, 0, 0, 0, 6, 0, 0, 0, 0, 0, 0, 0, 100, 0, 0, 0, 0, 0, 0, 0, 5, 0, 0, 0, + 0, 0, 0, 0, 4, 0, 0, 0, 0, 0, 0, 0, 3, 0, 0, 0, 0, 0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, + 0, 0, 0, 0, 0, 1, 1, 4, 1, 1, 10, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, + ]), + ); + const header2 = bytesToHeader(bytes); + expect(header2).toEqual(header); +}); + +test('getUint64 and setUint64', () => { + const dv = new DataView(new ArrayBuffer(8)); + setUint64(dv, 0, 1234567890); + expect(getUint64(dv, 0)).toBe(1234567890); +}); + +test('serializeDir, deserializeDir, & findTile', () => { + const entries: Entry[] = []; + for (let i = 0; i < 10; i++) { + entries.push({ + tileID: i, + offset: i * 2, + length: 100, + runLength: 1, + }); + } + const bytes = serializeDir(entries); + const entries2 = deserializeDir(bytes); + expect(entries2).toEqual(entries); + + // findTile + let tile = findTile(entries, 0); + expect(tile).toEqual({ tileID: 0, offset: 0, length: 100, runLength: 1 }); + tile = findTile(entries, 1); + expect(tile).toEqual({ tileID: 1, offset: 2, length: 100, runLength: 1 }); + tile = findTile(entries, 2); + expect(tile).toEqual({ tileID: 2, offset: 4, length: 100, runLength: 1 }); + + expect(findTile(entries, 11)).toBeNull(); +}); + +// let tile = Tile { x: 1_002, y: 6_969, zoom: 20 }; +// let id = tile.to_id(); diff --git a/tests/readers/pmtiles/reader.test.ts b/tests/readers/pmtiles/reader.test.ts new file mode 100644 index 00000000..1dfdf8ff --- /dev/null +++ b/tests/readers/pmtiles/reader.test.ts @@ -0,0 +1,320 @@ +import { Compression } from '../../../src/readers/pmtiles'; +import FileReader from '../../../src/readers/file'; +import MMapReader from '../../../src/readers/mmap'; +import { buildServer } from '../../server'; +import { BufferReader, S2PMTilesReader } from '../../../src/readers'; +import { describe, expect, test } from 'bun:test'; + +import type { Metadata } from 's2-tilejson'; +import type { S2Header } from '../../../src/readers/pmtiles'; + +/** External old metadata spec */ +interface MetaExternal { + name: string; + description: string; + version: string; + type: string; + generator: string; + generator_options: string; + vector_layers: Array<{ + id: string; + description?: string; + minzoom: number; + maxzoom: number; + fields: Record; + }>; + tilestats: { + layerCount: number; + layers: Array<{ + layer: string; + count: number; + geometry: string; + attributeCount: number; + attributes: Array; + }>; + }; +} + +describe('File Reader', async () => { + test('test_fixture_1', async () => { + const bufferReader = new BufferReader( + await Bun.file(`${__dirname}/fixtures/test_fixture_1.pmtiles`).arrayBuffer(), + ); + const testFixture1 = new S2PMTilesReader(bufferReader); + expect(testFixture1).toBeInstanceOf(S2PMTilesReader); + const header = await testFixture1.getHeader(); + // header + expect(header).toEqual({ + clustered: false, + internalCompression: Compression.Gzip, + jsonMetadataLength: 247, + jsonMetadataOffset: 152, + leafDirectoryLength: 0, + leafDirectoryOffset: 0, + maxZoom: 0, + minZoom: 0, + numAddressedTiles: 1, + numTileContents: 1, + numTileEntries: 1, + rootDirectoryLength: 25, + rootDirectoryOffset: 127, + specVersion: 3, + tileCompression: Compression.Gzip, + tileDataLength: 69, + tileDataOffset: 399, + tileType: 1, + }); + // metadata + expect((await testFixture1.getMetadata()) as unknown as MetaExternal).toEqual({ + name: 'test_fixture_1.pmtiles', + description: 'test_fixture_1.pmtiles', + version: '2', + type: 'overlay', + generator: 'tippecanoe v2.5.0', + generator_options: './tippecanoe -zg -o test_fixture_1.pmtiles --force', + vector_layers: [ + { + id: 'test_fixture_1pmtiles', + description: '', + minzoom: 0, + maxzoom: 0, + fields: {}, + }, + ], + tilestats: { + layerCount: 1, + layers: [ + { + layer: 'test_fixture_1pmtiles', + count: 1, + geometry: 'Polygon', + attributeCount: 0, + attributes: [], + }, + ], + }, + }); + // TILE + const tile = await testFixture1.getTile(0, 0, 0); + expect(tile).toBeInstanceOf(Uint8Array); + expect(new Uint8Array(tile as Uint8Array)).toEqual( + new Uint8Array([ + 26, 47, 120, 2, 10, 21, 116, 101, 115, 116, 95, 102, 105, 120, 116, 117, 114, 101, 95, 49, + 112, 109, 116, 105, 108, 101, 115, 40, 128, 32, 18, 17, 24, 3, 34, 13, 9, 150, 32, 232, 31, + 26, 0, 24, 21, 0, 0, 23, 15, + ]), + ); + }); + + test('test_fixture_2', async () => { + const fileReader = new FileReader(`${__dirname}/fixtures/test_fixture_2.pmtiles`); + const testFixture2 = new S2PMTilesReader(fileReader); + expect(testFixture2).toBeInstanceOf(S2PMTilesReader); + const header = await testFixture2.getHeader(); + // header + expect(header).toEqual({ + clustered: false, + internalCompression: Compression.Gzip, + jsonMetadataLength: 247, + jsonMetadataOffset: 152, + leafDirectoryLength: 0, + leafDirectoryOffset: 0, + maxZoom: 0, + minZoom: 0, + numAddressedTiles: 1, + numTileContents: 1, + numTileEntries: 1, + rootDirectoryLength: 25, + rootDirectoryOffset: 127, + specVersion: 3, + tileCompression: Compression.Gzip, + tileDataLength: 67, + tileDataOffset: 399, + tileType: 1, + }); + // metadata + expect((await testFixture2.getMetadata()) as unknown as MetaExternal).toEqual({ + name: 'test_fixture_2.pmtiles', + description: 'test_fixture_2.pmtiles', + version: '2', + type: 'overlay', + generator: 'tippecanoe v2.5.0', + generator_options: './tippecanoe -zg -o test_fixture_2.pmtiles --force', + vector_layers: [ + { + id: 'test_fixture_2pmtiles', + description: '', + minzoom: 0, + maxzoom: 0, + fields: {}, + }, + ], + tilestats: { + layerCount: 1, + layers: [ + { + layer: 'test_fixture_2pmtiles', + count: 1, + geometry: 'Polygon', + attributeCount: 0, + attributes: [], + }, + ], + }, + }); + // TILE + const tile = await testFixture2.getTile(0, 0, 0); + expect(tile).toBeInstanceOf(Uint8Array); + expect(new Uint8Array(tile as Uint8Array)).toEqual( + new Uint8Array([ + 26, 45, 120, 2, 10, 21, 116, 101, 115, 116, 95, 102, 105, 120, 116, 117, 114, 101, 95, 50, + 112, 109, 116, 105, 108, 101, 115, 40, 128, 32, 18, 15, 24, 3, 34, 11, 9, 128, 32, 232, 31, + 18, 22, 24, 21, 0, 15, + ]), + ); + }); +}); + +test('mmap test_fixture_2', async () => { + try { + const mmapReader = new MMapReader(`${__dirname}/fixtures/test_fixture_2.pmtiles`); + + const testFixture2 = new S2PMTilesReader(mmapReader); + const header = await testFixture2.getHeader(); + // header + expect(header).toEqual({ + clustered: false, + internalCompression: Compression.Gzip, + jsonMetadataLength: 247, + jsonMetadataOffset: 152, + leafDirectoryLength: 0, + leafDirectoryOffset: 0, + maxZoom: 0, + minZoom: 0, + numAddressedTiles: 1, + numTileContents: 1, + numTileEntries: 1, + rootDirectoryLength: 25, + rootDirectoryOffset: 127, + specVersion: 3, + tileCompression: Compression.Gzip, + tileDataLength: 67, + tileDataOffset: 399, + tileType: 1, + }); + // metadata + expect((await testFixture2.getMetadata()) as unknown as MetaExternal).toEqual({ + name: 'test_fixture_2.pmtiles', + description: 'test_fixture_2.pmtiles', + version: '2', + type: 'overlay', + generator: 'tippecanoe v2.5.0', + generator_options: './tippecanoe -zg -o test_fixture_2.pmtiles --force', + vector_layers: [ + { + id: 'test_fixture_2pmtiles', + description: '', + minzoom: 0, + maxzoom: 0, + fields: {}, + }, + ], + tilestats: { + layerCount: 1, + layers: [ + { + layer: 'test_fixture_2pmtiles', + count: 1, + geometry: 'Polygon', + attributeCount: 0, + attributes: [], + }, + ], + }, + }); + // TILE + const tile = await testFixture2.getTile(0, 0, 0); + expect(tile).toBeInstanceOf(Uint8Array); + expect(new Uint8Array(tile as Uint8Array)).toEqual( + new Uint8Array([ + 26, 45, 120, 2, 10, 21, 116, 101, 115, 116, 95, 102, 105, 120, 116, 117, 114, 101, 95, 50, + 112, 109, 116, 105, 108, 101, 115, 40, 128, 32, 18, 15, 24, 3, 34, 11, 9, 128, 32, 232, 31, + 18, 22, 24, 21, 0, 15, + ]), + ); + } catch (error) { + console.error(error); + } +}); + +test('server - s2 example', async () => { + const server = buildServer(); + + const reader = new S2PMTilesReader( + `http://localhost:${server.port}/readers/pmtiles/fixtures/s2.s2pmtiles`, + true, + ); + + // setup data + const str = 'hello world'; + const buf = Buffer.from(str, 'utf8'); + const uint8 = new Uint8Array(buf.buffer, buf.byteOffset, buf.byteLength); + // const str2 = 'hello world 2'; + // const buf2 = Buffer.from(str2, 'utf8'); + // const uint8_2 = new Uint8Array(buf2.buffer, buf2.byteOffset, buf2.byteLength); + + const metadata = await reader.getMetadata(); + const header = await reader.getHeader(); + expect(header).toEqual({ + clustered: true, + internalCompression: 1, + jsonMetadataLength: 17, + jsonMetadataOffset: 280, + leafDirectoryLength: 0, + leafDirectoryLength1: 0, + leafDirectoryLength2: 0, + leafDirectoryLength3: 0, + leafDirectoryLength4: 0, + leafDirectoryLength5: 0, + leafDirectoryOffset: 98_339, + leafDirectoryOffset1: 98_339, + leafDirectoryOffset2: 98_339, + leafDirectoryOffset3: 98_339, + leafDirectoryOffset4: 98_339, + leafDirectoryOffset5: 98_339, + maxZoom: 0, + minZoom: 0, + numAddressedTiles: 3, + numTileContents: 1, + numTileEntries: 1, + rootDirectoryLength: 5, + rootDirectoryLength1: 5, + rootDirectoryLength2: 1, + rootDirectoryLength3: 5, + rootDirectoryLength4: 1, + rootDirectoryLength5: 1, + rootDirectoryOffset: 262, + rootDirectoryOffset1: 267, + rootDirectoryOffset2: 272, + rootDirectoryOffset3: 273, + rootDirectoryOffset4: 278, + rootDirectoryOffset5: 279, + specVersion: 1, + tileCompression: 1, + tileDataLength: 35, + tileDataOffset: 98_304, + tileType: 1, + } as S2Header); + expect(metadata).toEqual({ metadata: true } as unknown as Metadata); + + const tile = await reader.getTileS2(0, 0, 0, 0); + expect(tile).toEqual(uint8); + + const tile2 = await reader.getTileS2(1, 0, 0, 0); + expect(tile2).toEqual(uint8); + + // const tile3 = await reader.getTileS2(3, 2, 1, 1); + // expect(tile3).toEqual(uint8_2); + + server.stop(); +}); diff --git a/tests/readers/pmtiles/varint.test.ts b/tests/readers/pmtiles/varint.test.ts new file mode 100644 index 00000000..9eb87ac0 --- /dev/null +++ b/tests/readers/pmtiles/varint.test.ts @@ -0,0 +1,42 @@ +import { readVarint } from '../../../src/readers/pmtiles'; +import { writeVarint } from '../../../src/writers/pmtiles'; +import { describe, expect, test } from 'bun:test'; + +describe('varint', () => { + const buffer = { buf: new Uint8Array(0), pos: 0 }; + writeVarint(0, buffer); + writeVarint(1, buffer); + writeVarint(127, buffer); + writeVarint(128, buffer); + writeVarint(16383, buffer); + writeVarint(16384, buffer); + writeVarint(839483929049384, buffer); + writeVarint(-1, buffer); + writeVarint(-1938339320, buffer); + + test('writeVarint', () => { + expect(buffer).toEqual({ + buf: new Uint8Array([ + 0, 1, 127, 128, 1, 255, 127, 128, 128, 1, 168, 242, 138, 171, 153, 240, 190, 1, 255, 255, + 255, 255, 255, 255, 255, 255, 255, 1, 136, 148, 221, 227, 248, 255, 255, 255, 255, 1, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + ]), + pos: 38, + }); + }); + + const resBuffer = { buf: new Uint8Array(buffer.buf.buffer, 0, buffer.pos), pos: 0 }; + + test('readVarint', () => { + expect(readVarint(resBuffer)).toEqual(0); + expect(readVarint(resBuffer)).toEqual(1); + expect(readVarint(resBuffer)).toEqual(127); + expect(readVarint(resBuffer)).toEqual(128); + expect(readVarint(resBuffer)).toEqual(16383); + expect(readVarint(resBuffer)).toEqual(16384); + expect(readVarint(resBuffer)).toEqual(839483929049384); + // the next two numbers are not supported + readVarint(resBuffer); + readVarint(resBuffer); + }); +}); diff --git a/tests/readers/shapefile/dbf.test.ts b/tests/readers/shapefile/dbf.test.ts index 78c54559..c2778e0f 100644 --- a/tests/readers/shapefile/dbf.test.ts +++ b/tests/readers/shapefile/dbf.test.ts @@ -1,7 +1,7 @@ import { BufferReader } from '../../../src/readers'; import DataBaseFile from '../../../src/readers/shapefile/dbf'; -import FileReader from '../../../src/readers/fileReader'; -import MMapReader from '../../../src/readers/mmapReader'; +import FileReader from '../../../src/readers/file'; +import MMapReader from '../../../src/readers/mmap'; import { expect, test } from 'bun:test'; test('empty dbf', async () => { diff --git a/tests/readers/shapefile/shp.test.ts b/tests/readers/shapefile/shp.test.ts index 745124b2..0127f70e 100644 --- a/tests/readers/shapefile/shp.test.ts +++ b/tests/readers/shapefile/shp.test.ts @@ -1,7 +1,7 @@ import { BufferReader } from '../../../src/readers'; import DataBaseFile from '../../../src/readers/shapefile/dbf'; -import FileReader from '../../../src/readers/fileReader'; -import MMapReader from '../../../src/readers/mmapReader'; +import FileReader from '../../../src/readers/file'; +import MMapReader from '../../../src/readers/mmap'; import { buildServer } from '../../server'; // import { fromPath } from '../../../src/readers/shapefile/file'; import { ShapeFile, fromGzip, fromURL } from '../../../src/readers/shapefile'; @@ -18,7 +18,7 @@ test('utf shp', async () => { version: 1000, }); - const featureCollection = shp.getFeatureCollection(); + const featureCollection = await shp.getFeatureCollection(); expect(featureCollection).toEqual({ bbox: [-108.97956848144531, 41.244772343082076, -108.6328125, 41.253032440653186, 0, 0], features: [ @@ -60,7 +60,7 @@ test('utf shp with dbf', async () => { version: 1000, }); - const featureCollection = shp.getFeatureCollection(); + const featureCollection = await shp.getFeatureCollection(); expect(featureCollection).toEqual({ bbox: [-108.97956848144531, 41.244772343082076, -108.6328125, 41.253032440653186, 0, 0], features: [ @@ -113,7 +113,7 @@ test('multipointz shp', async () => { version: 1000, }); - const featureCollection = shp.getFeatureCollection(); + const featureCollection = await shp.getFeatureCollection(); expect(featureCollection).toEqual({ type: 'FeatureCollection', @@ -151,7 +151,7 @@ test('polylinez shp', async () => { version: 1000, }); - const featureCollection = shp.getFeatureCollection(); + const featureCollection = await shp.getFeatureCollection(); expect(featureCollection).toEqual({ bbox: [-120, 38, -113, 45, 0, 0], @@ -194,7 +194,7 @@ test('fromGzip', async () => { version: 1000, }); - const featureCollection = shp.getFeatureCollection(); + const featureCollection = await shp.getFeatureCollection(); expect(featureCollection).toEqual({ bbox: [-108.97956848144531, 41.244772343082076, -108.6328125, 41.253032440653186, 0, 0], features: [ @@ -242,7 +242,7 @@ test('fromURL', async () => { version: 1000, }); - const featureCollection = shp.getFeatureCollection(); + const featureCollection = await shp.getFeatureCollection(); expect(featureCollection).toEqual({ bbox: [-108.97956848144531, 41.244772343082076, -108.6328125, 41.253032440653186, 0, 0], features: [ @@ -270,7 +270,7 @@ test('fromURL', async () => { type: 'FeatureCollection', }); - const featureCollection2 = shp2.getFeatureCollection(); + const featureCollection2 = await shp2.getFeatureCollection(); expect(featureCollection2).toEqual({ bbox: [-108.97956848144531, 41.244772343082076, -108.6328125, 41.253032440653186, 0, 0], features: [ diff --git a/tests/readers/wkt/index.test.ts b/tests/readers/wkt/index.test.ts index 3c4ea291..b03fb9c0 100644 --- a/tests/readers/wkt/index.test.ts +++ b/tests/readers/wkt/index.test.ts @@ -568,8 +568,10 @@ test('parse fixture 0', () => { projName: 'New_Zealand_Map_Grid', units: 'meter', to_meter: 1, + toMeter: 1, datumCode: 'nzgd49', datum_params: [59.47, -5.04, 187.44, 0.47, -0.1, 1.024, -4.5993], + datumParams: [59.47, -5.04, 187.44, 0.47, -0.1, 1.024, -4.5993], ellps: 'intl', a: 6378388, rf: 297, @@ -648,6 +650,7 @@ test('parse fixture 1', () => { projName: 'Lambert_Conformal_Conic_2SP', units: 'meter', to_meter: 1, + toMeter: 1, datumCode: 'north_american_datum_1983', ellps: 'GRS 1980', a: 6378137, @@ -728,6 +731,7 @@ test('parse fixture 2', () => { axis: 'enu', units: 'meter', to_meter: 1, + toMeter: 1, datumCode: 'european_terrestrial_reference_system_1989', ellps: 'GRS 1980', a: 6378137, @@ -787,6 +791,7 @@ test('parse fixture 3', () => { projName: 'longlat', units: 'degree', to_meter: 111319.4907932736, + toMeter: 111319.4907932736, datumCode: 'european terrestrial reference system 1989 ensemble', ellps: 'GRS 1980', a: 6378137, @@ -941,6 +946,7 @@ test('parse fixture 5', () => { latitude_of_origin: -71, central_meridian: 0, scale_factor: 1, + scaleFactor: 1, false_easting: 0, false_northing: 0, AUTHORITY: { @@ -954,6 +960,7 @@ test('parse fixture 5', () => { axis: 'enu', units: 'meter', to_meter: 1, + toMeter: 1, datumCode: 'wgs84', ellps: 'WGS 84', a: 6378137, @@ -965,5 +972,6 @@ test('parse fixture 5', () => { lat0: -1.5707963267948966, srsCode: 'WGS 84 / Antarctic Polar Stereographic', lat_ts: -1.239183768915974, + latTs: -1.239183768915974, }); }); diff --git a/tests/readers/xml/fixtures/example.svg b/tests/readers/xml/fixtures/example.svg new file mode 100644 index 00000000..f736366d --- /dev/null +++ b/tests/readers/xml/fixtures/example.svg @@ -0,0 +1,11 @@ + + + + + + + + + + + \ No newline at end of file diff --git a/tests/readers/xml/fixtures/gadas-export.png.aux.xml b/tests/readers/xml/fixtures/gadas-export.png.aux.xml new file mode 100644 index 00000000..0429b089 --- /dev/null +++ b/tests/readers/xml/fixtures/gadas-export.png.aux.xml @@ -0,0 +1,9 @@ + + PROJCS["WGS_1984_Web_Mercator_Auxiliary_Sphere",GEOGCS["GCS_WGS_1984",DATUM["WGS_1984",SPHEROID["WGS_1984",6378137.0,298.257223563]],PRIMEM["Greenwich",0.0],UNIT["Degree",0.0174532925199433]],PROJECTION["Mercator_Auxiliary_Sphere"],PARAMETER["False_Easting",0.0],PARAMETER["False_Northing",0.0],PARAMETER["Central_Meridian",0.0],PARAMETER["Standard_Parallel_1",0.0],PARAMETER["Auxiliary_Sphere_Type",0.0],UNIT["Meter",1.0]] + + PIXEL + + + NEAREST + + \ No newline at end of file diff --git a/tests/readers/xml/fixtures/iso.xml b/tests/readers/xml/fixtures/iso.xml new file mode 100644 index 00000000..ecfcdf0b --- /dev/null +++ b/tests/readers/xml/fixtures/iso.xml @@ -0,0 +1,747 @@ + + + + + 84576d7e-139b-11e7-b551-0200c0a8050e + + + ita + + + utf8 + + + dataset + + + + + + + + Servizio Osservatorio Agenti Fisici - ARPAV + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + soaf@arpa.veneto.it + + + + + + + + pointOfContact + + + + + 2017-03-28T07:30:26Z + + + ISO 19115:2003 - Geographic information - Metadata + + + ISO 19115:2003 + + + + + + + + 4326 + + + EPSG + + + 6.11 + + + + + + + + + + + Veneto Atlas of artificial night sky brightness - GRID + + + + + 2017-04-03T04:47:00Z + + + publication + + + + + + + + mapDigital + + + + + Valori dell'Atlante Mondiale della brillanza del cielo, per l'area della regione veneto, in mcd/m² (GRID). + + + I dati costituiscono una stima modellistica dei valori di brillanza del cielo notturno, utilizzabile in particolare per l'individuazione delle aree su cui effettuare i controlli in campo. + + + completed + + + + + + http://geomap.arpa.veneto.it/uploaded/thumbs/layer-84576d7e-139b-11e7-b551-0200c0a8050e-thumb.png + + + Thumbnail for 'Veneto Atlas of artificial night sky brightness - GRID' + + + image/png + + + + + + + + ESRI Shapefile + + + + 1.0 + + + + + + + + + Italy + + + place + + + + + + + + + + license + + + Creative Commons Attribution-NonCommercial (CC BY-NC 4.0): Creative Commons Attribution-NonCommercial 4.0 International (https://creativecommons.org/licenses/by-nc/4.0/) + + + + + + + + + + + + + + + + grid + + + ita + + + utf8 + + + + agentifisici + + + + + + + + 10.2822923743907 + + + 13.3486486092171 + + + 44.418521542726054 + + + 47.15260827566466 + + + + + + + + http://advances.sciencemag.org/content/2/6/e1600377 + + + + + + + + + + + http://geomap.arpa.veneto.it/layers/geonode%3Aatlanteil + + + WWW:LINK-1.0-http--link + + + Online link to the 'Veneto Atlas of artificial night sky brightness - GRID' description on GeoNode + + + + + + + + http://geomap.arpa.veneto.it/geoserver/wms?layers=geonode%3Aatlanteil&width=616&bbox=10.2822923743907%2C44.418521542726054%2C13.3486486092171%2C47.15260827566466&service=WMS&format=image%2Fjpeg&srs=EPSG%3A4326&request=GetMap&height=550 + + + WWW:DOWNLOAD-1.0-http--download + + + atlanteil.jpg + + + Veneto Atlas of artificial night sky brightness - GRID (JPEG Format) + + + + + + + + http://geomap.arpa.veneto.it/geoserver/wms?layers=geonode%3Aatlanteil&width=616&bbox=10.2822923743907%2C44.418521542726054%2C13.3486486092171%2C47.15260827566466&service=WMS&format=application%2Fpdf&srs=EPSG%3A4326&request=GetMap&height=550 + + + WWW:DOWNLOAD-1.0-http--download + + + atlanteil.pdf + + + Veneto Atlas of artificial night sky brightness - GRID (PDF Format) + + + + + + + + http://geomap.arpa.veneto.it/geoserver/wms?layers=geonode%3Aatlanteil&width=616&bbox=10.2822923743907%2C44.418521542726054%2C13.3486486092171%2C47.15260827566466&service=WMS&format=image%2Fpng&srs=EPSG%3A4326&request=GetMap&height=550 + + + WWW:DOWNLOAD-1.0-http--download + + + atlanteil.png + + + Veneto Atlas of artificial night sky brightness - GRID (PNG Format) + + + + + + + + http://geomap.arpa.veneto.it/geoserver/wms/kml?layers=geonode%3Aatlanteil&mode=download + + + WWW:DOWNLOAD-1.0-http--download + + + atlanteil.kml + + + Veneto Atlas of artificial night sky brightness - GRID (KML Format) + + + + + + + + http://geomap.arpa.veneto.it/geoserver/wms/kml?layers=geonode%3Aatlanteil&mode=refresh + + + WWW:DOWNLOAD-1.0-http--download + + + atlanteil.kml + + + Veneto Atlas of artificial night sky brightness - GRID (View in Google Earth Format) + + + + + + + + http://geomap.arpa.veneto.it/geoserver/gwc/service/gmaps?layers=geonode:atlanteil&zoom={z}&x={x}&y={y}&format=image/png8 + + + WWW:DOWNLOAD-1.0-http--download + + + atlanteil.tiles + + + Veneto Atlas of artificial night sky brightness - GRID (Tiles Format) + + + + + + + + http://geomap.arpa.veneto.it/geoserver/wms/reflect?layers=geonode:atlanteil&width=200&height=150&format=image/png8 + + + WWW:DOWNLOAD-1.0-http--download + + + atlanteil.png + + + Veneto Atlas of artificial night sky brightness - GRID (Remote Thumbnail Format) + + + + + + + + http://geomap.arpa.veneto.it/uploaded/thumbs/layer-84576d7e-139b-11e7-b551-0200c0a8050e-thumb.png + + + WWW:DOWNLOAD-1.0-http--download + + + atlanteil.png + + + Veneto Atlas of artificial night sky brightness - GRID (Thumbnail Format) + + + + + + + + http://geomap.arpa.veneto.it/geoserver/wms?request=GetLegendGraphic&format=image/png&WIDTH=20&HEIGHT=20&LAYER=geonode:atlanteil&legend_options=fontAntiAliasing:true;fontSize:12;forceLabels:on + + + WWW:DOWNLOAD-1.0-http--download + + + atlanteil.png + + + Veneto Atlas of artificial night sky brightness - GRID (Legend Format) + + + + + + + + http://geomap.arpa.veneto.it/geoserver/wcs?crs=EPSG%3A4326&service=WCS&format=ArcGrid&request=GetCoverage&height=329&width=368&version=1.0.0&BBox=9.956291513405647%2C13.674649470202153%2C44.39118067539667%2C47.17994914299405&Coverage=geonode%3Aatlanteil + + + WWW:DOWNLOAD-1.0-http--download + + + atlanteil.ArcGrid + + + Veneto Atlas of artificial night sky brightness - GRID (ArcGrid Format) + + + + + + + + http://geomap.arpa.veneto.it/geoserver/wcs?crs=EPSG%3A4326&service=WCS&format=GeoTIFF&request=GetCoverage&height=329&width=368&version=1.0.0&BBox=9.956291513405647%2C13.674649470202153%2C44.39118067539667%2C47.17994914299405&Coverage=geonode%3Aatlanteil + + + WWW:DOWNLOAD-1.0-http--download + + + atlanteil.GeoTIFF + + + Veneto Atlas of artificial night sky brightness - GRID (GeoTIFF Format) + + + + + + + + http://geomap.arpa.veneto.it/geoserver/wcs?crs=EPSG%3A4326&service=WCS&format=GeoPackage+%28mosaic%29&request=GetCoverage&height=329&width=368&version=1.0.0&BBox=9.956291513405647%2C13.674649470202153%2C44.39118067539667%2C47.17994914299405&Coverage=geonode%3Aatlanteil + + + WWW:DOWNLOAD-1.0-http--download + + + atlanteil.GeoPackage (mosaic) + + + Veneto Atlas of artificial night sky brightness - GRID (GeoPackage (mosaic) Format) + + + + + + + + http://geomap.arpa.veneto.it/geoserver/wcs?crs=EPSG%3A4326&service=WCS&format=Gtopo30&request=GetCoverage&height=329&width=368&version=1.0.0&BBox=9.956291513405647%2C13.674649470202153%2C44.39118067539667%2C47.17994914299405&Coverage=geonode%3Aatlanteil + + + WWW:DOWNLOAD-1.0-http--download + + + atlanteil.Gtopo30 + + + Veneto Atlas of artificial night sky brightness - GRID (Gtopo30 Format) + + + + + + + + http://geomap.arpa.veneto.it/geoserver/wcs?crs=EPSG%3A4326&service=WCS&format=ImageMosaic&request=GetCoverage&height=329&width=368&version=1.0.0&BBox=9.956291513405647%2C13.674649470202153%2C44.39118067539667%2C47.17994914299405&Coverage=geonode%3Aatlanteil + + + WWW:DOWNLOAD-1.0-http--download + + + atlanteil.ImageMosaic + + + Veneto Atlas of artificial night sky brightness - GRID (ImageMosaic Format) + + + + + + + + http://geomap.arpa.veneto.it/geoserver/wcs?crs=EPSG%3A4326&service=WCS&format=ArcGrid&request=GetCoverage&height=329&width=368&version=1.0.0&BBox=9.919107933837681%2C13.711833049770119%2C44.363292990720694%2C47.20783682767002&Coverage=geonode%3Aatlanteil + + + WWW:DOWNLOAD-1.0-http--download + + + atlanteil.ArcGrid + + + Veneto Atlas of artificial night sky brightness - GRID (ArcGrid Format) + + + + + + + + http://geomap.arpa.veneto.it/geoserver/wcs?crs=EPSG%3A4326&service=WCS&format=GeoTIFF&request=GetCoverage&height=329&width=368&version=1.0.0&BBox=9.919107933837681%2C13.711833049770119%2C44.363292990720694%2C47.20783682767002&Coverage=geonode%3Aatlanteil + + + WWW:DOWNLOAD-1.0-http--download + + + atlanteil.GeoTIFF + + + Veneto Atlas of artificial night sky brightness - GRID (GeoTIFF Format) + + + + + + + + http://geomap.arpa.veneto.it/geoserver/wcs?crs=EPSG%3A4326&service=WCS&format=GeoPackage+%28mosaic%29&request=GetCoverage&height=329&width=368&version=1.0.0&BBox=9.919107933837681%2C13.711833049770119%2C44.363292990720694%2C47.20783682767002&Coverage=geonode%3Aatlanteil + + + WWW:DOWNLOAD-1.0-http--download + + + atlanteil.GeoPackage (mosaic) + + + Veneto Atlas of artificial night sky brightness - GRID (GeoPackage (mosaic) Format) + + + + + + + + http://geomap.arpa.veneto.it/geoserver/wcs?crs=EPSG%3A4326&service=WCS&format=Gtopo30&request=GetCoverage&height=329&width=368&version=1.0.0&BBox=9.919107933837681%2C13.711833049770119%2C44.363292990720694%2C47.20783682767002&Coverage=geonode%3Aatlanteil + + + WWW:DOWNLOAD-1.0-http--download + + + atlanteil.Gtopo30 + + + Veneto Atlas of artificial night sky brightness - GRID (Gtopo30 Format) + + + + + + + + http://geomap.arpa.veneto.it/geoserver/wcs?crs=EPSG%3A4326&service=WCS&format=ImageMosaic&request=GetCoverage&height=329&width=368&version=1.0.0&BBox=9.919107933837681%2C13.711833049770119%2C44.363292990720694%2C47.20783682767002&Coverage=geonode%3Aatlanteil + + + WWW:DOWNLOAD-1.0-http--download + + + atlanteil.ImageMosaic + + + Veneto Atlas of artificial night sky brightness - GRID (ImageMosaic Format) + + + + + + + + http://geomap.arpa.veneto.it/geoserver/wcs?crs=EPSG%3A4326&service=WCS&format=GeoTIFF&request=GetCoverage&height=329&width=368&version=1.0.0&BBox=10.2822923743907%2C13.3486486092171%2C44.418521542726054%2C47.15260827566466&Coverage=geonode%3Aatlanteil + + + WWW:DOWNLOAD-1.0-http--download + + + atlanteil.GeoTIFF + + + Veneto Atlas of artificial night sky brightness - GRID (GeoTIFF Format) + + + + + + + + http://geomap.arpa.veneto.it/geoserver/wcs?crs=EPSG%3A4326&service=WCS&format=GeoTIFF&request=GetCoverage&height=329&width=368&version=1.0.0&BBox=9.881180682678357%2C13.749760300929443%2C44.3348475523512%2C47.23628226603952&Coverage=geonode%3Aatlanteil + + + WWW:DOWNLOAD-1.0-http--download + + + atlanteil.GeoTIFF + + + Veneto Atlas of artificial night sky brightness - GRID (GeoTIFF Format) + + + + + + + + http://geomap.arpa.veneto.it/geoserver/wcs?crs=EPSG%3A4326&service=WCS&format=GeoTIFF&request=GetCoverage&height=329&width=368&version=1.0.0&BBox=9.842494886495844%2C13.788446097111956%2C44.30583320521431%2C47.2652966131764&Coverage=geonode%3Aatlanteil + + + WWW:DOWNLOAD-1.0-http--download + + + atlanteil.GeoTIFF + + + Veneto Atlas of artificial night sky brightness - GRID (GeoTIFF Format) + + + + + + + + http://geomap.arpa.veneto.it/geoserver/wcs?crs=EPSG%3A4326&service=WCS&format=GeoTIFF&request=GetCoverage&height=329&width=368&version=1.0.0&BBox=9.80303537438968%2C13.82790560921812%2C44.27623857113469%2C47.294891247256025&Coverage=geonode%3Aatlanteil + + + WWW:DOWNLOAD-1.0-http--download + + + atlanteil.GeoTIFF + + + Veneto Atlas of artificial night sky brightness - GRID (GeoTIFF Format) + + + + + + + + http://geomap.arpa.veneto.it/geoserver/wcs?crs=EPSG%3A4326&service=WCS&format=GeoTIFF&request=GetCoverage&height=329&width=368&version=1.0.0&BBox=9.762786672041393%2C13.868154311566407%2C44.24605204437348%2C47.32507777401724&Coverage=geonode%3Aatlanteil + + + WWW:DOWNLOAD-1.0-http--download + + + atlanteil.GeoTIFF + + + Veneto Atlas of artificial night sky brightness - GRID (GeoTIFF Format) + + + + + + + + http://geomap.arpa.veneto.it/geoserver/wcs?crs=EPSG%3A4326&service=WCS&format=GeoTIFF&request=GetCoverage&height=329&width=368&version=1.0.0&BBox=9.721732995646143%2C13.909207987961658%2C44.21526178707704%2C47.355868031313676&Coverage=geonode%3Aatlanteil + + + WWW:DOWNLOAD-1.0-http--download + + + atlanteil.GeoTIFF + + + Veneto Atlas of artificial night sky brightness - GRID (GeoTIFF Format) + + + + + + + + http://geomap.arpa.veneto.it/geoserver/wcs?crs=EPSG%3A4326&service=WCS&format=GeoTIFF&request=GetCoverage&height=329&width=368&version=1.0.0&BBox=9.679858245722988%2C13.951082737884812%2C44.183855724634675%2C47.38727409375604&Coverage=geonode%3Aatlanteil + + + WWW:DOWNLOAD-1.0-http--download + + + atlanteil.GeoTIFF + + + Veneto Atlas of artificial night sky brightness - GRID (GeoTIFF Format) + + + + + + + + + http://geomap.arpa.veneto.it/geoserver/geonode/wms + + + OGC:WMS + + + OGC:WMS geonode Service - Provides Layer: Veneto Atlas of artificial night sky brightness - GRID + + + OGC WMS: geonode Service Root Endpoint. This service contains the Veneto Atlas of artificial night sky brightness - GRID layer. + + + + + + + + http://geomap.arpa.veneto.it/geoserver/geonode/wcs + + + OGC:WCS + + + OGC:WCS geonode Service - Provides Layer: Veneto Atlas of artificial night sky brightness - GRID + + + OGC WCS: geonode Service Root Endpoint. This service contains the Veneto Atlas of artificial night sky brightness - GRID layer. + + + + + + + + + + + + + + dataset + + + + + + I dati utilizzati per l'elaborazione provengono dal Falchi et al: "The new world atlas of artificial night sky brightness" (2016) + + + + + \ No newline at end of file diff --git a/tests/readers/xml/fixtures/m_3008501_ne_16_1_20171018.mrf b/tests/readers/xml/fixtures/m_3008501_ne_16_1_20171018.mrf new file mode 100644 index 00000000..e3166574 --- /dev/null +++ b/tests/readers/xml/fixtures/m_3008501_ne_16_1_20171018.mrf @@ -0,0 +1,13 @@ + + + + + LERC + + + + + PROJCS["NAD83 / UTM zone 16N",GEOGCS["NAD83",DATUM["North_American_1983",SPHEROID["GRS 1980",6378137,298.2572221010042,AUTHORITY["EPSG","7019"]],AUTHORITY["EPSG","6269"]],PRIMEM["Greenwich",0],UNIT["degree",0.0174532925199433],AUTHORITY["EPSG","4269"]],PROJECTION["Transverse_Mercator"],PARAMETER["latitude_of_origin",0],PARAMETER["central_meridian",-87],PARAMETER["scale_factor",0.9996],PARAMETER["false_easting",500000],PARAMETER["false_northing",0],UNIT["metre",1,AUTHORITY["EPSG","9001"]],AUTHORITY["EPSG","26916"]] + + LERC_PREC=0.5 V2=ON + diff --git a/tests/readers/xml/fixtures/rgb_raster.tif.aux.xml b/tests/readers/xml/fixtures/rgb_raster.tif.aux.xml new file mode 100644 index 00000000..fef22d05 --- /dev/null +++ b/tests/readers/xml/fixtures/rgb_raster.tif.aux.xml @@ -0,0 +1,60 @@ + + + + + + -0.5 + 255.5 + 256 + 1 + 0 + 13605|15268|17732|22219|29846|42673|63896|146992|1066269|1192831|639440|551646|389300|336105|303464|302365|276326|250514|240822|234205|233507|233505|233914|237711|241594|245957|250064|256675|262138|269005|281280|291947|303497|313528|319988|329772|343584|358873|374236|392257|412327|434393|457282|480423|506514|530276|549584|567926|577711|580881|578342|570340|557416|541840|524627|506925|488193|470175|450703|430338|409764|389021|366293|343475|322799|300766|280669|259917|242487|225490|208976|194844|180842|169369|157162|146663|138162|128513|120865|112636|106381|99653|94074|88341|84044|79690|76109|72917|69746|66902|64288|61930|59329|57001|54942|52696|50731|48972|47316|45490|43628|41997|40622|39115|37212|35682|33993|32667|31408|30238|28432|27539|26254|25001|23706|22465|21261|20203|19029|18310|17642|16509|15869|15148|14160|13712|12910|12390|11590|11215|10547|9848|9440|8924|8438|8072|7512|7208|6937|6619|6307|5855|5616|5248|4891|4542|4093|3856|3471|3241|2881|2586|2406|2090|1870|1737|1512|1275|1184|1000|804|694|610|521|402|341|301|212|152|110|113|62|45|44|24|17|8|8|3|5|2|4|2|0|0|0|0|0|0|0|0|0|0|0|0|0|0|0|0|0|0|0|0|0|0|0|0|0|0|0|0|0|0|0|0|0|0|0|0|0|0|0|0|0|0|0|0|0|0|0|0|0|0|0|0|0|0|0|0|0|0|0|0|0|0|0|0|0|0|0|0|0|0|0|0|0 + + + + 0 + 182 + 44.407563908646 + 0 + 25.392374521083 + + + + + + -0.5 + 255.5 + 256 + 1 + 0 + 1053|0|0|0|0|0|0|0|0|2|7|12|16|53|153|409|1209|3126|8684|27309|695675|1019649|653799|501385|350694|239955|168809|100744|100212|88513|83101|81943|76851|78547|81125|84040|85688|84943|84876|82440|81234|82090|84149|82205|81970|83113|85739|89807|90637|92206|91517|92536|94834|94592|95777|98291|99546|102096|104903|109636|113199|116447|121080|125148|130581|135139|140678|147631|154442|161543|168906|177479|186031|195412|205834|217115|228507|240221|254167|266445|279183|293242|306499|319870|333394|345009|355487|363189|370959|373062|374939|374902|368745|364314|358495|351803|344776|338048|329649|321242|311780|303240|295218|284449|276254|266367|255221|244490|235866|225785|217316|207236|199249|189270|181393|173404|165648|158504|151875|146474|139726|134442|128332|123742|119668|115869|111756|108572|105652|102364|99299|97223|93904|92536|89985|88244|87200|85352|84181|82094|81627|80547|79902|79037|78286|78021|77172|76770|75830|75059|75032|75112|74528|74354|74370|74493|74710|73895|74444|74061|74891|74798|75937|76598|77248|77907|78814|79090|79550|81110|82579|84253|84932|86422|87178|87435|88669|86409|85392|84760|84830|83732|82824|80892|79277|76166|74152|72509|70694|69798|67083|65269|63696|61937|60775|59597|58297|56869|55576|54719|53612|52443|51062|50156|49551|48377|48224|47646|46583|45806|44416|42628|41419|39954|37762|36020|33692|31799|29162|26406|23715|20723|17977|15605|13666|11468|9384|7454|6001|4806|3807|3075|2268|1899|1413|1005|750|529|398|276|196|157|131|98|47|39|32|27|24|27|22|12|9|17|11|36 + + + + 1 + 255 + 96.372431147996 + 0 + 50.057898474622 + + + + + + -0.5 + 255.5 + 256 + 1 + 0 + 1070|63|124|196|265|394|575|802|1096|1516|1995|2756|3842|5137|6775|8725|11284|14901|19449|24363|30772|38803|48297|61333|75812|94867|117610|147403|186663|238046|470538|709014|1401997|1466894|717911|670116|675789|677861|682893|644746|590403|557789|525571|490889|462019|433083|407333|394474|372731|352917|333976|314475|296937|279320|263764|249802|237742|225816|214033|200821|190867|180711|172200|165026|157169|150687|144793|139147|134161|128013|124640|120714|117827|114280|111201|109118|107801|106200|104720|103103|101742|99729|98581|97422|95547|95301|93714|91985|90997|89464|88124|87005|85369|84366|82799|82223|80371|79827|79008|78151|77364|75851|75637|74607|73929|72713|72201|72160|72119|71355|71473|70891|71444|70538|70410|70133|70581|70269|70531|70257|69879|70246|70351|70022|69121|69122|68955|68679|68654|69163|68982|68814|68638|68451|68946|68128|68135|68018|67570|67687|67560|66601|67029|67076|66621|67056|66948|66536|66298|66492|66077|66030|66325|66565|66288|67280|67530|68017|68326|68746|68673|68927|68864|69349|70699|71454|72072|73561|74534|74972|76667|77300|79056|80346|81205|83546|82922|83191|81942|81890|81640|80616|80372|77846|76541|75142|73032|71421|68678|65913|64331|62138|60297|59008|57041|57029|55056|54314|53400|52690|51526|51185|50498|49858|49225|48517|47884|47792|47152|46563|45685|44951|44159|43285|42662|41462|40568|38624|37491|35701|33383|31609|29280|26233|23497|20642|17612|15294|12685|10567|8399|6494|4864|3671|2533|1765|1194|875|593|407|283|207|159|126|91|78|73|61|52|51|27|51|40|40|42|71 + + + + 2 + 255 + 78.467562373125 + 0 + 55.153168095348 + + + diff --git a/tests/readers/xml/fixtures/tmx.xml b/tests/readers/xml/fixtures/tmx.xml new file mode 100644 index 00000000..f6bdf3c4 --- /dev/null +++ b/tests/readers/xml/fixtures/tmx.xml @@ -0,0 +1,17 @@ + +
+ + + + Hello world! + + + Bonjour tout le monde! + + + + diff --git a/tests/readers/xml/parsering.test.ts b/tests/readers/xml/parsering.test.ts new file mode 100644 index 00000000..d84907c0 --- /dev/null +++ b/tests/readers/xml/parsering.test.ts @@ -0,0 +1,281 @@ +import { + countSubstring, + findTagByName, + findTagByPath, + findTagsByName, + findTagsByPath, + getAttribute, + indexOfMatch, + indexOfMatchEnd, + removeComments, + removeTagsByName, +} from '../../../src/readers/xml/parsing'; + +import { beforeAll, expect, test } from 'bun:test'; + +import type { Tag } from '../../../src/readers/xml/parsing'; + +let iso: string; +let mrf: string; +let tiffAux: string; +let tmx: string; +let svg: string; + +beforeAll(async () => { + iso = await Bun.file(`${__dirname}/fixtures/iso.xml`).text(); + mrf = await Bun.file(`${__dirname}/fixtures/m_3008501_ne_16_1_20171018.mrf`).text(); + tiffAux = await Bun.file(`${__dirname}/fixtures/rgb_raster.tif.aux.xml`).text(); + // tmx example from https://en.wikipedia.org/wiki/Translation_Memory_eXchange + tmx = await Bun.file(`${__dirname}/fixtures/tmx.xml`).text(); + // svg example from https://en.wikipedia.org/wiki/SVG + svg = await Bun.file(`${__dirname}/fixtures/example.svg`).text(); +}); + +const nested = ''; + +const commented = ` + + +`; + +const multiline = `
+
+
+`; + +test('tmx', () => { + expect(getAttribute(tmx, 'version')).toEqual('1.4'); + const header: Tag = findTagByName(tmx, 'header') ?? { + inner: '', + outer: '', + start: 0, + end: 0, + }; + expect(getAttribute(header, 'srclang')).toEqual('en'); + expect(getAttribute(header, 'o-tmf')).toEqual('ABCTransMem'); + const tu = findTagByName(tmx, 'tu', { debug: false }); + expect((tu?.inner ?? '').trim()).toEqual( + '\n Hello world!\n \n \n Bonjour tout le monde!\n ', + ); + const tuvs = findTagsByName(tmx, 'tuv'); + expect(tuvs.length).toEqual(2); +}); + +test('svg', () => { + const tag: Tag = findTagByName(svg, 'svg') ?? { inner: '', outer: '', start: 0, end: 0 }; + expect(getAttribute(tag, 'height')).toEqual('391'); + expect(getAttribute(tag, 'width')).toEqual('391'); + expect(getAttribute(tag, 'viewBox')).toEqual('-70.5 -70.5 391 391'); + expect(getAttribute(tag, 'xmlns:xlink')).toEqual('http://www.w3.org/1999/xlink'); + expect(getAttribute(findTagByPath(svg, ['g']) ?? '', 'opacity')).toEqual('0.8'); + expect(getAttribute(findTagByName(svg, 'rect') ?? '', 'fill')).toEqual('#fff'); + const rect = findTagByPath(svg, ['g', 'rect'])?.outer; + expect(getAttribute(rect ?? '', 'x')).toEqual('25'); + expect(getAttribute(rect ?? '', 'stroke-width')).toEqual('4'); +}); + +test('support multi-line tags', () => { + const container = findTagByName(multiline, 'div'); + expect(container?.outer).toEqual( + `\n \n
`, + ); + expect(container?.inner).toEqual(`\n \n`); + expect(getAttribute(container?.outer ?? '', 'id')).toEqual('container'); + expect(getAttribute(container?.outer ?? '', 'data-foo')).toBeUndefined(); + expect(getAttribute((container?.inner ?? '').trim(), 'data-foo', { debug: false })).toEqual( + 'bar', + ); +}); + +test('should get gmd:code and avoid gmd:codeSpace', () => { + const index = indexOfMatch(iso, `]`, 0); + expect(iso.slice(index).startsWith(' { + const xml = ``; + const index = indexOfMatchEnd(xml, '[ /]items>', 0); + expect(index).toEqual(xml.length - 1); +}); + +test('removing comments', () => { + expect(removeComments(commented)).toEqual('\n\n\n'); + expect(removeComments('')).toEqual(''); +}); + +test('count substring', () => { + expect(countSubstring(nested, '')).toEqual(3); +}); + +test('should find all the urls in iso.xml', () => { + const urls = findTagsByName(iso, 'gmd:URL'); + expect(urls[0].inner).toEqual('http://geomap.arpa.veneto.it/layers/geonode%3Aatlanteil'); + expect(urls.length).toEqual(29); +}); + +test('should get only tags with full string match on tag name', () => { + const urls = findTagsByName(iso, 'gmd:code'); + expect(urls.length).toEqual(1); +}); + +test('should get info from iso.xml file', () => { + const tag = findTagByPath(iso, ['gmd:RS_Identifier', 'gmd:code', 'gco:CharacterString']); + const projection = parseInt(tag!.inner!); + expect(projection).toEqual(4326); + + const longitude = Number(findTagByPath(iso, ['gmd:westBoundLongitude', 'gco:Decimal'])!.inner); + expect(longitude).toEqual(10.2822923743907); +}); + +test('should get raster size from a .mrf file', () => { + const rasterSize = findTagByPath(mrf, ['MRF_META', 'Raster', 'Size'], { + debug: false, + })!; + expect(rasterSize?.outer).toEqual(''); + expect(rasterSize?.inner).toEqual(null); +}); + +test('should get all character strings', () => { + const tags = findTagsByPath(iso, ['gmd:RS_Identifier', 'gmd:code']); + expect(tags.length).toEqual(1); + expect(tags[0]?.inner === '').toEqual(false); +}); + +test('should get all metadata for bands from .tif.aux.xml', () => { + const debug = false; + const mdis = findTagsByPath(tiffAux, ['Metadata', 'MDI'], { debug }); + expect(mdis.length).toEqual(15); +}); + +test('should get attributes from metadata', () => { + const mdi = findTagByPath(tiffAux, ['Metadata', 'MDI'], { debug: false }); + const key = getAttribute(mdi!, 'key', { debug: false }); + expect(key).toEqual('SourceBandIndex'); +}); + +test('should get raster width from a .mrf file', () => { + const rasterSize = ''; + expect(getAttribute(rasterSize, 'x')).toEqual('6638'); + expect(getAttribute(rasterSize, 'y')).toEqual('7587'); + expect(getAttribute(rasterSize, 'c')).toEqual('4'); +}); + +test('should get first tag', () => { + const xml = ` `; + const tag = findTagByName(xml, 'field', { debug: false })!; + expect(tag.outer).toEqual(``); + + const tag2 = findTagByName(xml, 'field', { debug: false, nested: false })!; + expect(tag2.outer).toEqual(``); +}); + +test('should get all tags (self-closing and not)', () => { + const xml = ` `; + const tags = findTagsByName(xml, 'field', { debug: false }); + expect(tags.length).toEqual(5); +}); + +test('should get self-closing with immediate close and without interior space', () => { + const xml = ``; + const tag = findTagByName(xml, 'Kitchen')!; + expect(tag.outer).toEqual(''); + expect(tag.inner).toEqual(null); +}); + +test('should handle nested tags', () => { + const xml = `AB`; + + expect(findTagByName(xml, 'Thing')!.outer).toEqual(xml); + expect(findTagByName(xml, 'Thing')!.outer).toEqual(xml); + + expect(findTagsByName(xml, 'Thing').length).toEqual(3); + expect(findTagsByName(xml, 'Thing')[0].outer).toEqual(xml); + expect(findTagsByName(xml, 'Thing', { nested: true }).length).toEqual(3); + expect(findTagsByName(xml, 'Thing', { nested: true })[0].outer).toEqual(xml); + expect(findTagsByName(xml, 'Thing', { nested: false }).length).toEqual(1); + expect(findTagsByName(xml, 'Thing', { nested: false })[0].outer).toEqual(xml); + + expect(findTagsByPath(xml, ['Thing']).length).toEqual(1); + expect(findTagsByPath(xml, ['Thing'])[0].outer).toEqual(xml); + expect(findTagsByPath(xml, ['Thing', 'Thing'])).toEqual([ + { inner: 'A', outer: 'A', start: 7, end: 28 }, + { inner: 'B', outer: 'B', start: 28, end: 49 }, + ]); + expect(findTagByPath(xml, ['Thing'])!.outer).toEqual(xml); +}); + +test('removeTagsByName', () => { + expect(removeTagsByName('
  • A
  • B
', 'li')).toEqual('
    '); +}); + +test('check immutability of findTagsByPath', () => { + const path = ['gmd:RS_Identifier', 'gmd:code'] as const; + const tags = findTagsByPath(iso, path); + expect(tags.length).toEqual(1); + expect(tags[0].inner === '').toEqual(false); + expect(path.length).toEqual(2); +}); + +test('simple check findTagsByPath with index', () => { + const xml = 'ABCD'; + const tags = findTagsByPath(xml, [{ name: 'tag', index: 2 }]); + expect(tags).toEqual([{ outer: 'C', inner: 'C', start: 24, end: 36 }]); +}); + +test('findTagsByPath with larger source', () => { + const tags = findTagsByPath(iso, [ + 'gmd:MD_DigitalTransferOptions', + { name: 'gmd:onLine', index: 10 }, + 'gmd:CI_OnlineResource', + 'gmd:description', + 'gco:CharacterString', + ]); + expect(tags).toEqual([ + { + outer: + 'Veneto Atlas of artificial night sky brightness - GRID (ArcGrid Format)', + inner: 'Veneto Atlas of artificial night sky brightness - GRID (ArcGrid Format)', + start: 20934, + end: 21048, + }, + ]); +}); + +test('check findTagsByPath with index (multi-level)', () => { + const xml = ` + + + A + B + + + C + D + + `; + + expect(findTagsByPath(xml, [{ name: 'tag', index: 2 }])).toEqual([ + { inner: 'C', outer: 'C', start: 89, end: 101 }, + ]); + + expect( + findTagsByPath(xml, ['outer', { name: 'pair', index: 1 }, { name: 'tag', index: 0 }]), + ).toEqual([{ inner: 'C', outer: 'C', start: 89, end: 101 }]); + + expect( + findTagsByPath(xml, ['outer', { name: 'pair', index: 1 }, { name: 'tag', index: 1 }]), + ).toEqual([{ inner: 'D', outer: 'D', start: 108, end: 120 }]); +}); diff --git a/tests/server.ts b/tests/server.ts index 8b561b57..042012c4 100644 --- a/tests/server.ts +++ b/tests/server.ts @@ -9,13 +9,38 @@ export function buildServer() { * @returns - a response of the file to the user */ async fetch(req) { - // Extract the pathname from the request URL const { pathname } = new URL(req.url); - // Build the absolute file path based on the request - const file = Bun.file(`${__dirname}${pathname}`); - // If the file does not exist or is empty, return 404 + const filePath = `${__dirname}${pathname}`; + const file = Bun.file(filePath); + if (!file || file.size === 0) return new Response(null, { status: 404 }); - // Return the file as the response + + // Handle range request + const rangeHeader = req.headers.get('Range'); + if (rangeHeader) { + const [unit, range] = rangeHeader.split('='); + if (unit === 'bytes') { + const [start, end] = range.split('-').map(Number); + + const fileSize = file.size; + const endByte = end !== undefined ? Math.min(end, fileSize - 1) : fileSize - 1; + const rangeStart = Math.max(start, 0); + + // Read the specified byte range from the file + const chunk = file.slice(rangeStart, endByte + 1); + + return new Response(chunk, { + status: 206, + headers: { + 'Content-Range': `bytes ${rangeStart}-${endByte}/${fileSize}`, + 'Content-Length': String(endByte - rangeStart + 1), + 'Accept-Ranges': 'bytes', + }, + }); + } + } + + // If no range is requested, serve the whole file return new Response(file); }, }); diff --git a/tests/dataStructures/delaunator.test.ts b/tests/tools/delaunator.test.ts similarity index 92% rename from tests/dataStructures/delaunator.test.ts rename to tests/tools/delaunator.test.ts index 3baf0994..0f1cfaf3 100644 --- a/tests/dataStructures/delaunator.test.ts +++ b/tests/tools/delaunator.test.ts @@ -1,16 +1,8 @@ -import Delaunator from '../../src/dataStructures/delaunator'; +import Delaunator from '../../src/tools/delaunator'; import { beforeAll, expect, test } from 'bun:test'; import { Point } from '../../src/geometry'; -// const issue13 = loadJSON('./fixtures/issue13.json'); -// const issue43 = loadJSON('./fixtures/issue43.json'); -// const issue44 = loadJSON('./fixtures/issue44.json'); -// const robustness1 = loadJSON('./fixtures/robustness1.json'); -// const robustness2 = loadJSON('./fixtures/robustness2.json'); -// const robustness3 = loadJSON('./fixtures/robustness3.json'); -// const robustness4 = loadJSON('./fixtures/robustness4.json'); - let points: Point[]; beforeAll(async () => { diff --git a/tests/dataStructures/fixtures/issue13.json b/tests/tools/fixtures/issue13.json similarity index 100% rename from tests/dataStructures/fixtures/issue13.json rename to tests/tools/fixtures/issue13.json diff --git a/tests/dataStructures/fixtures/issue43.json b/tests/tools/fixtures/issue43.json similarity index 100% rename from tests/dataStructures/fixtures/issue43.json rename to tests/tools/fixtures/issue43.json diff --git a/tests/dataStructures/fixtures/issue44.json b/tests/tools/fixtures/issue44.json similarity index 100% rename from tests/dataStructures/fixtures/issue44.json rename to tests/tools/fixtures/issue44.json diff --git a/tests/dataStructures/fixtures/robustness1.json b/tests/tools/fixtures/robustness1.json similarity index 100% rename from tests/dataStructures/fixtures/robustness1.json rename to tests/tools/fixtures/robustness1.json diff --git a/tests/dataStructures/fixtures/robustness2.json b/tests/tools/fixtures/robustness2.json similarity index 100% rename from tests/dataStructures/fixtures/robustness2.json rename to tests/tools/fixtures/robustness2.json diff --git a/tests/dataStructures/fixtures/robustness3.json b/tests/tools/fixtures/robustness3.json similarity index 100% rename from tests/dataStructures/fixtures/robustness3.json rename to tests/tools/fixtures/robustness3.json diff --git a/tests/dataStructures/fixtures/robustness4.json b/tests/tools/fixtures/robustness4.json similarity index 100% rename from tests/dataStructures/fixtures/robustness4.json rename to tests/tools/fixtures/robustness4.json diff --git a/tests/dataStructures/fixtures/ukraine.json b/tests/tools/fixtures/ukraine.json similarity index 100% rename from tests/dataStructures/fixtures/ukraine.json rename to tests/tools/fixtures/ukraine.json diff --git a/tests/dataStructures/fixtures/water1.json b/tests/tools/fixtures/water1.json similarity index 100% rename from tests/dataStructures/fixtures/water1.json rename to tests/tools/fixtures/water1.json diff --git a/tests/dataStructures/fixtures/water2.json b/tests/tools/fixtures/water2.json similarity index 100% rename from tests/dataStructures/fixtures/water2.json rename to tests/tools/fixtures/water2.json diff --git a/tests/dataStructures/orthodrome.test.ts b/tests/tools/orthodrome.test.ts similarity index 95% rename from tests/dataStructures/orthodrome.test.ts rename to tests/tools/orthodrome.test.ts index 86728b78..3ba7b320 100644 --- a/tests/dataStructures/orthodrome.test.ts +++ b/tests/tools/orthodrome.test.ts @@ -1,4 +1,4 @@ -import Orthodrome from '../../src/dataStructures/orthodrome'; +import Orthodrome from '../../src/tools/orthodrome'; import { expect, test } from 'bun:test'; test('orthodrome', () => { diff --git a/tests/dataStructures/polylabel.test.ts b/tests/tools/polylabel.test.ts similarity index 92% rename from tests/dataStructures/polylabel.test.ts rename to tests/tools/polylabel.test.ts index 9cd91125..193df574 100644 --- a/tests/dataStructures/polylabel.test.ts +++ b/tests/tools/polylabel.test.ts @@ -1,4 +1,4 @@ -import polylabel from '../../src/dataStructures/polylabel'; +import polylabel from '../../src/tools/polylabel'; import { expect, test } from 'bun:test'; import type { Polygon, VectorPolygon } from '../../src/geometry'; @@ -60,7 +60,8 @@ test('works on degenerate polygons', () => { }); /** - * @param polygon + * @param polygon - the polygon to convert + * @returns - the vector polygon */ function convertGeometry(polygon: Polygon): VectorPolygon { return polygon.map((line) => line.map((point) => ({ x: point[0], y: point[1] }))); diff --git a/tests/writers/pmtiles/varint.test.ts b/tests/writers/pmtiles/varint.test.ts new file mode 100644 index 00000000..9eb87ac0 --- /dev/null +++ b/tests/writers/pmtiles/varint.test.ts @@ -0,0 +1,42 @@ +import { readVarint } from '../../../src/readers/pmtiles'; +import { writeVarint } from '../../../src/writers/pmtiles'; +import { describe, expect, test } from 'bun:test'; + +describe('varint', () => { + const buffer = { buf: new Uint8Array(0), pos: 0 }; + writeVarint(0, buffer); + writeVarint(1, buffer); + writeVarint(127, buffer); + writeVarint(128, buffer); + writeVarint(16383, buffer); + writeVarint(16384, buffer); + writeVarint(839483929049384, buffer); + writeVarint(-1, buffer); + writeVarint(-1938339320, buffer); + + test('writeVarint', () => { + expect(buffer).toEqual({ + buf: new Uint8Array([ + 0, 1, 127, 128, 1, 255, 127, 128, 128, 1, 168, 242, 138, 171, 153, 240, 190, 1, 255, 255, + 255, 255, 255, 255, 255, 255, 255, 1, 136, 148, 221, 227, 248, 255, 255, 255, 255, 1, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + ]), + pos: 38, + }); + }); + + const resBuffer = { buf: new Uint8Array(buffer.buf.buffer, 0, buffer.pos), pos: 0 }; + + test('readVarint', () => { + expect(readVarint(resBuffer)).toEqual(0); + expect(readVarint(resBuffer)).toEqual(1); + expect(readVarint(resBuffer)).toEqual(127); + expect(readVarint(resBuffer)).toEqual(128); + expect(readVarint(resBuffer)).toEqual(16383); + expect(readVarint(resBuffer)).toEqual(16384); + expect(readVarint(resBuffer)).toEqual(839483929049384); + // the next two numbers are not supported + readVarint(resBuffer); + readVarint(resBuffer); + }); +}); diff --git a/tests/writers/pmtiles/writer.test.ts b/tests/writers/pmtiles/writer.test.ts new file mode 100644 index 00000000..860eeb2e --- /dev/null +++ b/tests/writers/pmtiles/writer.test.ts @@ -0,0 +1,226 @@ +import FileReader from '../../../src/readers/file'; +import FileWriter from '../../../src/writers/file'; +import { TileType } from '../../../src/writers/pmtiles'; +import tmp from 'tmp'; +import { unlink } from 'node:fs/promises'; +import { BufferReader, S2PMTilesReader } from '../../../src/readers'; +import { BufferWriter, S2PMTilesWriter } from '../../../src/writers'; +import { afterAll, expect, test } from 'bun:test'; + +import { stat } from 'node:fs/promises'; + +import type { Metadata } from 's2-tilejson'; +import type { S2Header } from '../../../src/readers/pmtiles'; + +let tmpFile1: string; +let tmpFile2: string; + +test('File Writer WM', async () => { + const bufWriter = new BufferWriter(); + const writer = new S2PMTilesWriter(bufWriter, TileType.Pbf); + // setup data + const str = 'hello world'; + const buf = Buffer.from(str, 'utf8'); + const uint8 = new Uint8Array(buf.buffer, buf.byteOffset, buf.byteLength); + const str2 = 'hello world 2'; + const buf2 = Buffer.from(str2, 'utf8'); + const uint8_2 = new Uint8Array(buf2.buffer, buf2.byteOffset, buf2.byteLength); + // write data in tile + await writer.writeTileXYZ(0, 0, 0, uint8); + await writer.writeTileXYZ(1, 0, 1, uint8); + await writer.writeTileXYZ(5, 2, 9, uint8_2); + // finish + await writer.commit({ metadata: true } as unknown as Metadata); + + const bufReader = new BufferReader(bufWriter.commit().buffer); + const reader = new S2PMTilesReader(bufReader); + const metadata = await reader.getMetadata(); + const header = await reader.getHeader(); + expect(bufReader.buffer.byteLength).toEqual(98_399); + expect(header).toEqual({ + clustered: true, + internalCompression: 2, + jsonMetadataLength: 37, + jsonMetadataOffset: 296, + leafDirectoryLength: 0, + leafDirectoryOffset: 98399, + maxZoom: 5, + minZoom: 0, + numAddressedTiles: 3, + numTileContents: 3, + numTileEntries: 3, + rootDirectoryLength: 34, + rootDirectoryOffset: 262, + specVersion: 3, + tileCompression: 2, + tileDataLength: 95, + tileDataOffset: 98304, + tileType: 1, + }); + expect(metadata).toEqual({ metadata: true } as unknown as Metadata); + + const tile = await reader.getTile(0, 0, 0); + expect(tile).toEqual(uint8); + + const tile2 = await reader.getTile(1, 0, 1); + expect(tile2).toEqual(uint8); + + const tile3 = await reader.getTile(5, 2, 9); + expect(tile3).toEqual(uint8_2); +}); + +test('File Writer S2', async () => { + tmpFile1 = tmp.tmpNameSync({ prefix: 'S2' }); + const writer = new S2PMTilesWriter(new FileWriter(tmpFile1), TileType.Pbf); + // setup data + const str = 'hello world'; + const buf = Buffer.from(str, 'utf8'); + const uint8 = new Uint8Array(buf.buffer, buf.byteOffset, buf.byteLength); + const str2 = 'hello world 2'; + const buf2 = Buffer.from(str2, 'utf8'); + const uint8_2 = new Uint8Array(buf2.buffer, buf2.byteOffset, buf2.byteLength); + // write data in tile + await writer.writeTileS2(0, 0, 0, 0, uint8); + await writer.writeTileS2(1, 0, 0, 0, uint8); + await writer.writeTileS2(2, 8, 1, 1, uint8_2); + await writer.writeTileS2(3, 2, 1, 1, uint8_2); + await writer.writeTileS2(4, 5, 5, 5, uint8_2); + await writer.writeTileS2(5, 5, 5, 5, uint8); + // finish + await writer.commit({ metadata: true } as unknown as Metadata); + + const reader = new S2PMTilesReader(new FileReader(tmpFile1)); + const metadata = await reader.getMetadata(); + const header = await reader.getHeader(); + + expect((await stat(tmpFile1)).size).toEqual(98_496); + expect(header).toEqual({ + clustered: true, + internalCompression: 2, + jsonMetadataLength: 37, + jsonMetadataOffset: 418, + leafDirectoryLength: 0, + leafDirectoryLength1: 0, + leafDirectoryLength2: 0, + leafDirectoryLength3: 0, + leafDirectoryLength4: 0, + leafDirectoryLength5: 0, + leafDirectoryOffset: 98496, + leafDirectoryOffset1: 98496, + leafDirectoryOffset2: 98496, + leafDirectoryOffset3: 98496, + leafDirectoryOffset4: 98496, + leafDirectoryOffset5: 98496, + maxZoom: 8, + minZoom: 0, + numAddressedTiles: 6, + numTileContents: 1, + numTileEntries: 1, + rootDirectoryLength: 25, + rootDirectoryLength1: 25, + rootDirectoryLength2: 27, + rootDirectoryLength3: 25, + rootDirectoryLength4: 27, + rootDirectoryLength5: 27, + rootDirectoryOffset: 262, + rootDirectoryOffset1: 287, + rootDirectoryOffset2: 312, + rootDirectoryOffset3: 339, + rootDirectoryOffset4: 364, + rootDirectoryOffset5: 391, + specVersion: 1, + tileCompression: 2, + tileDataLength: 192, + tileDataOffset: 98304, + tileType: 1, + } as S2Header); + expect(metadata).toEqual({ metadata: true } as unknown as Metadata); + + const tile = await reader.getTileS2(0, 0, 0, 0); + expect(tile).toEqual(uint8); + + const tile2 = await reader.getTileS2(1, 0, 0, 0); + expect(tile2).toEqual(uint8); + + const tile3 = await reader.getTileS2(3, 2, 1, 1); + expect(tile3).toEqual(uint8_2); + + const tile4 = await reader.getTileS2(4, 5, 5, 5); + expect(tile4).toEqual(uint8_2); + + const tile5 = await reader.getTileS2(5, 5, 5, 5); + expect(tile5).toEqual(uint8); + + const tile6 = await reader.getTileS2(2, 8, 1, 1); + expect(tile6).toEqual(uint8_2); +}); + +test( + 'File Writer WM Large', + async () => { + tmpFile2 = tmp.tmpNameSync({ prefix: 'S2-big-2' }); + const writer = new S2PMTilesWriter(new FileWriter(tmpFile2), TileType.Pbf); + // write lots of tiles + for (let zoom = 0; zoom < 8; zoom++) { + const size = 1 << zoom; + for (let x = 0; x < size; x++) { + for (let y = 0; y < size; y++) { + const str = `${zoom}-${x}-${y}`; + const buf = Buffer.from(str, 'utf8'); + const uint8 = new Uint8Array(buf.buffer, buf.byteOffset, buf.byteLength); + await writer.writeTileXYZ(zoom, x, y, uint8); + } + } + } + // finish + await writer.commit({ metadata: true } as unknown as Metadata); + + const reader = new S2PMTilesReader(new FileReader(tmpFile2)); + // const header = await reader.getHeader(); + // expect((await stat(tmpFile2)).size).toEqual(736_752); + // expect(header).toEqual({ + // clustered: false, + // internalCompression: 2, + // jsonMetadataLength: 37, + // jsonMetadataOffset: 305, + // leafDirectoryLength: 46_519, + // leafDirectoryOffset: 690_233, + // maxZoom: 7, + // minZoom: 0, + // numAddressedTiles: 21845, + // numTileContents: 21_845, + // numTileEntries: 21_845, + // rootDirectoryLength: 43, + // rootDirectoryOffset: 262, + // specVersion: 3, + // tileCompression: 2, + // tileDataLength: 638_448, + // tileDataOffset: 98_304, + // tileType: 1, + // }); + const metadata = await reader.getMetadata(); + expect(metadata).toEqual({ metadata: true } as unknown as Metadata); + + // get a random tile + const tile = await reader.getTile(6, 22, 45); + const str = `6-22-45`; + const buf = Buffer.from(str, 'utf8'); + const uint8 = new Uint8Array(buf.buffer, buf.byteOffset, buf.byteLength); + expect(tile).toEqual(uint8); + }, + { timeout: 10_000 }, +); + +// cleanup +afterAll(async () => { + try { + await unlink(tmpFile1); + } catch (_) { + // ignore + } + try { + await unlink(tmpFile2); + } catch (_) { + // ignore + } +}); diff --git a/tmp.ts b/tmp.ts deleted file mode 100644 index 2d2f3942..00000000 --- a/tmp.ts +++ /dev/null @@ -1,65 +0,0 @@ -import proj4 from './proj4js-master/lib/index.js'; -import { Transformer } from './src/proj4/index.js' - -proj4.defs([ - // ["EPSG:102018", "+proj=gnom +lat_0=90 +lon_0=0 +x_0=6300000 +y_0=6300000 +ellps=WGS84 +datum=WGS84 +units=m +no_defs"], - ["testmerc", "+proj=merc +lon_0=5.937 +lat_ts=45.027 +ellps=sphere"], - // ["testmerc2", "+proj=merc +a=6378137 +b=6378137 +lat_ts=0.0 +lon_0=0.0 +x_0=0.0 +y_0=0 +units=m +k=1.0 +nadgrids=@null +no_defs"] -]); -// proj4.defs('esriOnline', 'PROJCS["WGS_1984_Web_Mercator_Auxiliary_Sphere",GEOGCS["GCS_WGS_1984",DATUM["D_WGS_1984",SPHEROID["WGS_1984",6378137.0,298.257223563]],PRIMEM["Greenwich",0.0],UNIT["Degree",0.0174532925199433]],PROJECTION["Mercator_Auxiliary_Sphere"],PARAMETER["False_Easting",0.0],PARAMETER["False_Northing",0.0],PARAMETER["Central_Meridian",0.0],PARAMETER["Standard_Parallel_1",0.0],PARAMETER["Auxiliary_Sphere_Type",0.0],UNIT["Meter",1.0]]'); - -const testPoint = { - code: 'testmerc', - xy: [-45007.0787624, 4151725.59875], - ll: [5.364315,46.623154] -} -const proj = new proj4.Proj(testPoint.code); -console.log('proj', proj) -const xy = proj4.transform(proj4.WGS84, proj, proj4.toPoint(testPoint.ll)); -const ll = proj4.transform(proj, proj4.WGS84, proj4.toPoint(testPoint.xy)); -// console.log('xy', xy) -// console.log('ll', ll) - - - - -console.log('\n\n\n\n\n\n\n'); - - - -const transform = new Transformer(); -transform.setSource('+proj=merc +lon_0=5.937 +lat_ts=45.027 +ellps=sphere') - -const result = transform.forward({ x: -45007.0787624, y: 4151725.59875 }) -console.log('result', result) -const backwards = transform.inverse(result) -console.log('backwards', backwards) - - - - - - - - - -// transform.insertDefinition() - -// const defData = '+proj=gnom +lat_0=90 +lon_0=0 +x_0=6300000 +y_0=6300000 +ellps=WGS84 +datum=WGS84 +units=m +no_defs' -// const defData = '+proj=merc +lon_0=5.937 +lat_ts=45.027 +ellps=sphere' - -// interface Obj { -// [key: string]: string | true -// } - -// const paramObj: Obj = defData -// .split('+') -// .map((v) => v.trim()) -// .filter((a) => a.length > 0) -// .reduce((res, a) => { -// const [key, value] = a.split('='); -// res[key.toLowerCase()] = value !== undefined && value.length > 0 ? value : true; -// return res; -// }, {} as Obj); - -// console.log('paramObj', paramObj) diff --git a/tmpTests.ts b/tmpTests.ts new file mode 100644 index 00000000..ec257c11 --- /dev/null +++ b/tmpTests.ts @@ -0,0 +1,199 @@ +import proj4 from './proj4js-master/lib/index'; +// import proj4 from './proj4js-master/dist/proj4-src.js'; +import { Transformer, injectAllDefinitions } from './src/proj4' + +// proj4.defs([ +// ["EPSG:102018", "+proj=gnom +lat_0=90 +lon_0=0 +x_0=6300000 +y_0=6300000 +ellps=WGS84 +datum=WGS84 +units=m +no_defs"], +// ["testmerc", "+proj=merc +lon_0=5.937 +lat_ts=45.027 +ellps=sphere"], +// ["testmerc2", "+proj=merc +a=6378137 +b=6378137 +lat_ts=0.0 +lon_0=0.0 +x_0=0.0 +y_0=0 +units=m +k=1.0 +nadgrids=@null +no_defs"] +// ]); +// proj4.defs('esriOnline', 'PROJCS["WGS_1984_Web_Mercator_Auxiliary_Sphere",GEOGCS["GCS_WGS_1984",DATUM["D_WGS_1984",SPHEROID["WGS_1984",6378137.0,298.257223563]],PRIMEM["Greenwich",0.0],UNIT["Degree",0.0174532925199433]],PROJECTION["Mercator_Auxiliary_Sphere"],PARAMETER["False_Easting",0.0],PARAMETER["False_Northing",0.0],PARAMETER["Central_Meridian",0.0],PARAMETER["Standard_Parallel_1",0.0],PARAMETER["Auxiliary_Sphere_Type",0.0],UNIT["Meter",1.0]]'); +// proj4.defs('testings', 'GEOGCS["GCS_WGS_1984",DATUM["D_WGS_1984",SPHEROID["WGS_1984",6378137,298.257223563]],PRIMEM["Greenwich",0],UNIT["Degree",0.017453292519943295]]') + + + + + + +// const PROJECTION_STRING = '+proj=gstmerc +lon_0=0 +x_0=0 +y_0=0 +R=6371008.7714 +datum=WGS84 +units=m +no_defs' + +// proj4.defs('testings', PROJECTION_STRING) +// const testCase = { +// code: 'testings', +// ll: [-112.50042920000004, 42.036926809999976], +// xy: [-5380950.902080743, -7434667.860047833], +// } +// // const proj = new proj4.Proj(testCase.code); +// // const rsltA = proj4.transform(proj4.WGS84, proj, testCase.ll); +// // const rsltA = proj4.transform(proj, proj4.WGS84, testCase.xy); +// // console.log('OLD: ', rsltA) + +// console.log('\n\n') +// console.log('-----------------------------------------------------------------') +// console.log('\n\n') + +// const transform = new Transformer(); +// injectAllDefinitions(transform); +// transform.setSource(PROJECTION_STRING) +// const rsltB = transform.inverse({ x: testCase.ll[0], y: testCase.ll[1], z: testCase.ll[2] }); +// // const rsltB = transform.forward({ x: testCase.xy[0], y: testCase.xy[1], z: testCase.xy[2] }); +// console.log('NEW: ', rsltB) + +// console.log('\n\n\n\n\n\n') + +// console.log('OLD: ', rsltA) +// console.log('NEW: ', rsltB) + +const enu = '+proj=longlat +axis=enu'; +const esu = '+proj=longlat +axis=esu'; +const wnu = '+proj=longlat +axis=wnu'; +const transform = new Transformer(enu, esu); +injectAllDefinitions(transform); +// var result = proj4(enu, esu).forward({ x: 40, y: 50 }, true); +const result = transform.forward({ x: 40, y: 50 }, true); +console.log(result) + +// { +// projName: "cea", +// datumCode: "WGS84", +// units: "m", +// no_defs: true, +// type: "crs", +// datum_params: [ "0", "0", "0" ], +// ellps: "WGS84", +// datumName: "WGS84", +// k0: 0.8667510025721987, +// axis: "enu", +// init: [Function: init], +// forward: [Function: forward], +// inverse: [Function: inverse], +// names: [ "cea" ], +// a: 6378137, +// b: 6356752.314245179, +// rf: 298.257223563, +// sphere: undefined, +// es: 0.006694379990141316, +// e: 0.08181919084262149, +// ep2: 0.006739496742276434, +// datum: { +// datum_type: 4, +// datum_params: [ 0, 0, 0 ], +// a: 6378137, +// b: 6356752.314245179, +// es: 0.006694379990141316, +// ep2: 0.006739496742276434, +// }, +// } + +// // ------------------------------------ + +// { +// name: "Equal_Area_Cylindrical", +// projName: undefined, +// datumCode: "WGS84", +// datumType: 4, +// datumParams: [ 0, 0, 0, 0, 0, 0, 0 ], +// srsCode: "", +// lon0: 0, +// lon1: 0, +// lon2: 0, +// long0: 0, +// long1: 0, +// longc: 0, +// lat0: 0, +// lat1: 0, +// lat2: 0, +// latTs: 0.5235987755982988, +// a: 6378137, +// b: 6356752.314245179, +// e: 0.08181919084262149, +// x0: 0, +// y0: 0, +// k: undefined, +// k0: 1, +// rf: 298.257223563, +// rA: false, +// rc: undefined, +// es: 0.006694379990141316, +// ep2: 0.006739496742276434, +// alpha: undefined, +// gamma: undefined, +// zone: undefined, +// rectifiedGridAngle: undefined, +// utmSouth: false, +// toMeter: undefined, +// units: "m", +// fromGreenwich: 0, +// approx: false, +// axis: "enu", +// nadgrids: "@null", +// sphere: false, +// ellps: "WGS84", +// noDefs: true, +// type: "crs", +// forward: [Function: forward], +// inverse: [Function: inverse], +// } + + + + + + +// const Rn = 6378137 / (Math.sqrt(1.0e0 - es * Sin2_Lat)); +// console.log('SUB A', a, Sin_Lat, Cos_Lat, Sin2_Lat, Rn) +// // 6378137 0.8453038511165344 0.5342858778664806 0.7145386007124441 6393446.513163418 + +// es, f 0.006694380022900686 0.003352810681182268 +// N NP K0 0.0016792203946287192 0.0000028197811337370315 0.9996 0.997924968703673 +// es, f 0.006674372231802045 0.0033427731821747556 +// N NP K0 0.0016741848011149636 0.0000028028947482843505 1 0.998329312961542 + +// es, f 0.006694380022900686 0.003352810681182268 +// N NP K0 0.0016792203946287192 0.0000028197811337370315 1 0.9983242984230422 + +// es, f 0.006694380022900686 +// N NP K0 1 0.9983242984230422 + +// console.log('\n\n\n\n\n\n\n'); + + + +// const transform = new Transformer(); +// transform.setSource('+proj=merc +lon_0=5.937 +lat_ts=45.027 +ellps=sphere') + +// const result = transform.forward({ x: -45007.0787624, y: 4151725.59875 }) +// console.log('result', result) +// const backwards = transform.inverse(result) +// console.log('backwards', backwards) + + +// Benchmarking: + +// create n number of points as both an array and vectorpoint + +// const n = 50_000_000 + +// const vectorPoints = Array.from({ length: n }, (_, i) => { +// // all the x-y values have to be between -180 and 180 and -80 and 80 +// const x = Math.random() * 360 - 180 +// const y = Math.min(Math.max(Math.random() * 180 - 90, -80), 80) +// return { x, y } +// }) +// const arrayPoints = vectorPoints.map((p) => [p.x, p.y]) + +// let start = Bun.nanoseconds() +// for (const point of arrayPoints) { +// proj4.transform(proj4.WGS84, proj, proj4.toPoint(point)); +// } +// let end = Bun.nanoseconds() +// let seconds = (end - start) / 1_000_000_000 +// console.log('Proj4JS', seconds) + +// start = Bun.nanoseconds() +// for (const point of vectorPoints) { +// transform.inverse(point) +// } +// end = Bun.nanoseconds() +// seconds = (end - start) / 1_000_000_000 +// console.log('Transformer', seconds)