├── js ├── tests │ ├── nodejs │ │ ├── run_tests.js │ │ ├── proj4rs.js │ │ └── test_core.js │ ├── test_index.js │ ├── test_core.js │ └── test_defs.js ├── ol-proj4rs-demo-app │ ├── assets │ │ ├── pkg │ │ └── js │ │ │ └── proj4.js │ ├── .gitignore │ ├── favicon.ico │ ├── vite.docker.js │ ├── package.json │ ├── vite.config.js │ ├── README.md │ ├── codeStyle.css │ ├── reprojection-image.js │ ├── sphere-mollweide.js │ ├── index.html │ ├── wms-image-custom-proj.js │ ├── sphere-mollweide.html │ ├── vector-projections.js │ ├── wms-image-custom-proj.html │ ├── style.css │ ├── reprojection-image.html │ └── vector-projections.html ├── .gitignore ├── .docker │ ├── Dockerfile │ ├── ol-proj4rs-test.sh │ └── ol-run.sh ├── index.html └── README.md ├── proj4rs-clib ├── examples │ ├── .gitignore │ ├── Makefile │ └── test_epsg3035.c ├── python │ ├── .gitignore │ └── tests │ │ └── test_all.py ├── proj4rs_c.pc.in ├── cbindgen.toml ├── Cargo.toml ├── pyproject.toml └── Makefile.toml ├── .gitignore ├── proj4rs ├── fixtures │ ├── .gitignore │ ├── cnhpgn.gsb │ └── 100800401.gsb ├── PROJ4_General_Parameters.pdf ├── src │ ├── math │ │ ├── msfn.rs │ │ ├── tsfn.rs │ │ ├── adjlon.rs │ │ ├── qsfn.rs │ │ ├── auth.rs │ │ ├── aasincos.rs │ │ ├── phi2.rs │ │ ├── mlfn.rs │ │ ├── mod.rs │ │ └── gauss.rs │ ├── bin │ │ ├── projdbg.rs │ │ └── nadinfos.rs │ ├── prime_meridians.rs │ ├── parse.rs │ ├── projections │ │ ├── mill.rs │ │ ├── geocent.rs │ │ ├── latlong.rs │ │ ├── eqc.rs │ │ ├── sterea.rs │ │ ├── tmerc.rs │ │ ├── somerc.rs │ │ ├── cea.rs │ │ ├── moll.rs │ │ └── merc.rs │ ├── nadgrids │ │ ├── files │ │ │ ├── mod.rs │ │ │ └── ntv2.rs │ │ ├── header.rs │ │ └── mod.rs │ ├── units.rs │ ├── datum_params.rs │ ├── errors.rs │ ├── adaptors.rs │ ├── wasm │ │ ├── mod.rs │ │ └── nadgrids.rs │ ├── lib.rs │ ├── datum_transform.rs │ ├── datums.rs │ └── geocent.rs ├── .gitignore ├── README_WASM.md ├── Cargo.toml ├── Makefile.toml ├── examples │ ├── rsproj.rs │ └── rsproj_bench.rs ├── CHANGELOG.md └── tests │ └── proj4js_tests.rs ├── proj4rs-geodesic ├── build.rs ├── Cargo.toml └── README.md ├── proj4rs-php ├── README.md ├── examples │ └── test_epsg3035.php ├── Cargo.toml └── src │ └── lib.rs ├── CONTRIBUTING.md ├── Cargo.toml ├── projections.md ├── .github └── workflows │ └── docs.yml └── README.md /js/tests/nodejs/run_tests.js: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /js/ol-proj4rs-demo-app/assets/pkg: -------------------------------------------------------------------------------- 1 | ../../pkg -------------------------------------------------------------------------------- /proj4rs-clib/examples/.gitignore: -------------------------------------------------------------------------------- 1 | *.out 2 | -------------------------------------------------------------------------------- /proj4rs-clib/python/.gitignore: -------------------------------------------------------------------------------- 1 | proj4rs/_proj4rs 2 | -------------------------------------------------------------------------------- /js/ol-proj4rs-demo-app/assets/js/proj4.js: -------------------------------------------------------------------------------- 1 | ../../../proj4.js -------------------------------------------------------------------------------- /js/.gitignore: -------------------------------------------------------------------------------- 1 | /.npm 2 | .idea/ 3 | 4 | ol-proj4rs-demo-app/dist 5 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | Cargo.lock 2 | 3 | /target 4 | .idea/ 5 | 6 | __pycache__ 7 | -------------------------------------------------------------------------------- /js/ol-proj4rs-demo-app/.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | dist 3 | package-lock.json 4 | -------------------------------------------------------------------------------- /js/.docker/Dockerfile: -------------------------------------------------------------------------------- 1 | # syntax=docker/dockerfile:1 2 | FROM node:22-bookworm-slim 3 | 4 | -------------------------------------------------------------------------------- /proj4rs/fixtures/.gitignore: -------------------------------------------------------------------------------- 1 | BWTA2017.gsb 2 | NTv2_SN.gsb 3 | GDA94_GDA2020_conformal.gsb 4 | -------------------------------------------------------------------------------- /proj4rs/fixtures/cnhpgn.gsb: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/3liz/proj4rs/HEAD/proj4rs/fixtures/cnhpgn.gsb -------------------------------------------------------------------------------- /proj4rs/fixtures/100800401.gsb: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/3liz/proj4rs/HEAD/proj4rs/fixtures/100800401.gsb -------------------------------------------------------------------------------- /js/ol-proj4rs-demo-app/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/3liz/proj4rs/HEAD/js/ol-proj4rs-demo-app/favicon.ico -------------------------------------------------------------------------------- /proj4rs/PROJ4_General_Parameters.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/3liz/proj4rs/HEAD/proj4rs/PROJ4_General_Parameters.pdf -------------------------------------------------------------------------------- /proj4rs-geodesic/build.rs: -------------------------------------------------------------------------------- 1 | 2 | fn main() { 3 | cc::Build::new() 4 | .file("src/C/geodesic.c") 5 | .compile("geodesic"); 6 | } 7 | -------------------------------------------------------------------------------- /proj4rs/src/math/msfn.rs: -------------------------------------------------------------------------------- 1 | #[inline] 2 | pub(crate) fn msfn(sinphi: f64, cosphi: f64, es: f64) -> f64 { 3 | cosphi / (1. - es * sinphi * sinphi).sqrt() 4 | } 5 | -------------------------------------------------------------------------------- /js/tests/nodejs/proj4rs.js: -------------------------------------------------------------------------------- 1 | // Statements to load wasm module in nodejs REPL 2 | let Proj 3 | import("../../pkg-node/proj4rs.js").then(module => { Proj = module }); 4 | -------------------------------------------------------------------------------- /proj4rs/.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | /Cargo.lock 3 | /.npm 4 | 5 | # IDE 6 | .idea/ 7 | 8 | # Build 9 | ol-proj4rs-demo-app/dist 10 | pipeline*.sh 11 | toolchain.txt 12 | -------------------------------------------------------------------------------- /js/ol-proj4rs-demo-app/vite.docker.js: -------------------------------------------------------------------------------- 1 | export default { 2 | build: { 3 | sourcemap: true, 4 | }, 5 | server: { 6 | fs: { 7 | allow: ['/src'] 8 | } 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /js/.docker/ol-proj4rs-test.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | export npm_config_cache=$(pwd)/.npm 4 | 5 | cd ol-proj4rs-demo-app 6 | npm --loglevel=verbose update 7 | echo "Starting ol-proj4rs-demo-app" 8 | npm --loglevel=verbose start -c vite.docker.js 9 | 10 | 11 | -------------------------------------------------------------------------------- /proj4rs-clib/proj4rs_c.pc.in: -------------------------------------------------------------------------------- 1 | prefix=@PREFIX@ 2 | exec_prefix=${prefix} 3 | libdir=@LIBDIR@ 4 | includedir=@INCLUDE_DIR@/ 5 | 6 | Name: proj4rs_c 7 | Version: @VERSION@ 8 | Description: Proj4rs binding library 9 | Requires: 10 | Libs: -L${libdir} -lproj4rs_c 11 | Cflags: -I${includedir} 12 | -------------------------------------------------------------------------------- /proj4rs/src/math/tsfn.rs: -------------------------------------------------------------------------------- 1 | use super::consts::FRAC_PI_2; 2 | 3 | #[inline] 4 | pub(crate) fn tsfn(phi: f64, sinphi: f64, e: f64) -> f64 { 5 | // XXX Avoid division by zero, check denominator 6 | (0.5 * (FRAC_PI_2 - phi)).tan() / ((1. - sinphi * e) / (1. + sinphi * e)).powf(0.5 * e) 7 | } 8 | -------------------------------------------------------------------------------- /proj4rs-clib/cbindgen.toml: -------------------------------------------------------------------------------- 1 | # 2 | # See 3 | # * https://github.com/mozilla/cbindgen/blob/master/template.toml 4 | # * https://github.com/mozilla/cbindgen/blob/master/docs.md 5 | # 6 | language = "C" 7 | 8 | include_guard =" _PROJ4RS_H_" 9 | 10 | cpp_compat = true 11 | documentation_style = "c99" 12 | -------------------------------------------------------------------------------- /js/ol-proj4rs-demo-app/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ol-test-app", 3 | "version": "1.0.0", 4 | "scripts": { 5 | "start": "vite", 6 | "build": "vite build", 7 | "serve": "vite preview" 8 | }, 9 | "devDependencies": { 10 | "vite": "^4.0.4" 11 | }, 12 | "dependencies": { 13 | "ol": "latest" 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /js/.docker/ol-run.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | ( 4 | set -e; 5 | cd .docker; 6 | docker build -t ol-proj4rs-test .; 7 | ) 8 | 9 | docker run \ 10 | --entrypoint=bash \ 11 | --name ol-proj4rs-test \ 12 | --network host \ 13 | --rm \ 14 | -it \ 15 | -w /src \ 16 | -v $(pwd):/src -u $UID:$UID \ 17 | ol-proj4rs-test .docker/ol-proj4rs-test.sh 18 | 19 | -------------------------------------------------------------------------------- /js/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | proj4rs-wasm 6 | 7 | 8 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /proj4rs-php/README.md: -------------------------------------------------------------------------------- 1 | # Minimal binding for php 2 | 3 | This crate implement a minimal binding for php. 4 | 5 | Do not expect good performances for batch processing because of the nature 6 | of php arrays and constante marshalling for points. 7 | 8 | If you are seeking for performance, you would rather go with the C bindings 9 | and php [FFI module](https://www.php.net/manual/en/book.ffi.php). 10 | -------------------------------------------------------------------------------- /proj4rs-php/examples/test_epsg3035.php: -------------------------------------------------------------------------------- 1 | projName); 7 | var_dump($dst->projName); 8 | 9 | $point = new Point(15.4213696, 47.0766716, 0.); 10 | 11 | var_dump($point); 12 | 13 | print "==> Transform <==\n"; 14 | transform_point($src, $dst, $point, true); 15 | 16 | var_dump($point); 17 | -------------------------------------------------------------------------------- /proj4rs-geodesic/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "proj4rs-geodesic" 3 | version = "0.1.0" 4 | edition = "2021" 5 | description = "Binding to Geodesic calculation routines" 6 | readme="README.md" 7 | keywords.workspace = true 8 | authors.workspace = true 9 | license.workspace = true 10 | homepage.workspace = true 11 | repository.workspace = true 12 | categories.workspace = true 13 | 14 | [dependencies] 15 | 16 | [build-dependencies] 17 | cc = "1.0" 18 | 19 | -------------------------------------------------------------------------------- /proj4rs-geodesic/README.md: -------------------------------------------------------------------------------- 1 | Binding to geodesic routines 2 | ============================ 3 | 4 | This is a static binding of the C implementation of geodesic 5 | algorithms as implemented in the [Proj library](https://proj.org/en/stable/). 6 | 7 | This crate is used as an optional dependency of the [proj4rs](https://lib.rs/crates/proj4rs) 8 | and was created initially for the support of the [Azimuthal Equidistant (aeqd)](https://proj.org/en/stable/operations/projections/aeqd.html) projection. 9 | 10 | 11 | -------------------------------------------------------------------------------- /proj4rs/src/math/adjlon.rs: -------------------------------------------------------------------------------- 1 | use super::consts::{EPS_12, PI, TAU}; 2 | 3 | pub(crate) fn adjlon(mut lon: f64) -> f64 { 4 | // Let lon slightly overshoot, 5 | // to avoid spurious sign switching at the date line 6 | if lon.abs() >= PI + EPS_12 { 7 | // adjust to 0..2pi rad 8 | lon += PI; 9 | 10 | // remove integral # of 'revolutions' 11 | lon -= TAU * (lon / TAU).floor(); 12 | 13 | // adjust back to -pi..pi rad 14 | lon -= PI; 15 | } 16 | lon 17 | } 18 | -------------------------------------------------------------------------------- /proj4rs-clib/examples/Makefile: -------------------------------------------------------------------------------- 1 | # 2 | # Compile examples 3 | # 4 | 5 | SRC=$(wildcard *.c) 6 | OUT=$(patsubst %.c,%.out,$(SRC)) 7 | 8 | NAMES=$(patsubst %.c,%,$(SRC)) 9 | 10 | .PHONY: test build $(NAMES) 11 | 12 | build: $(OUT) 13 | 14 | %.out : %.c 15 | @echo "Compiling $<" 16 | @gcc -Wall -g -O0 $< -I../../target/cbindgen/ -L../../target/release -lproj4rs_c -o $@ 17 | 18 | clean: 19 | rm *.out 20 | 21 | export LD_LIBRARY_PATH=$(shell realpath ../../target/release) 22 | 23 | test: $(NAMES) 24 | 25 | $(NAMES): 26 | @echo "==== $@ ====" 27 | @./$@.out 28 | 29 | -------------------------------------------------------------------------------- /proj4rs-php/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "proj4rs-php" 3 | version = "0.1.0" 4 | edition = "2021" 5 | keywords.workspace = true 6 | authors.workspace = true 7 | license.workspace = true 8 | homepage.workspace = true 9 | repository.workspace = true 10 | categories.workspace = true 11 | 12 | [lib] 13 | name = "proj4rs_php" 14 | crate-type = ["cdylib"] 15 | 16 | [dependencies] 17 | proj4rs = "~0.1" 18 | ext-php-rs = "0.13" 19 | log = { version = "0.4", optional = true } 20 | 21 | [features] 22 | crs-definitions = ["proj4rs/crs-definitions"] 23 | logging = ["log", "proj4rs/logging" ] 24 | -------------------------------------------------------------------------------- /proj4rs/src/math/qsfn.rs: -------------------------------------------------------------------------------- 1 | use super::consts::EPS_7; 2 | 3 | pub(crate) fn qsfn(sinphi: f64, e: f64, one_es: f64) -> f64 { 4 | if e >= EPS_7 { 5 | let con = e * sinphi; 6 | let div1 = 1.0 - con * con; 7 | let div2 = 1.0 + con; 8 | // avoid zero division, fail gracefully 9 | if div1 == 0.0 || div2 == 0.0 { 10 | f64::INFINITY 11 | } else { 12 | one_es * (sinphi / div1 - (0.5 / e) * ((1. - con) / div2).ln()) 13 | } 14 | } else { 15 | // XXX why not 2.*sinphi ? 16 | sinphi + sinphi 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /proj4rs/src/bin/projdbg.rs: -------------------------------------------------------------------------------- 1 | //! 2 | //! Display debugging projection infos 3 | //! 4 | use proj4rs::{ 5 | errors::{Error, Result}, 6 | proj, 7 | }; 8 | use std::env; 9 | 10 | fn main() -> Result<()> { 11 | let args: Vec = env::args().collect(); 12 | if args.len() <= 1 { 13 | println!("Usage: projdbg "); 14 | return Err(Error::InvalidParameterValue("Missing proj string")); 15 | } 16 | 17 | let projstr = args[1..].join(" "); 18 | let projection = proj::Proj::from_user_string(&projstr)?; 19 | 20 | println!("{projection:#?}"); 21 | Ok(()) 22 | } 23 | -------------------------------------------------------------------------------- /js/ol-proj4rs-demo-app/vite.config.js: -------------------------------------------------------------------------------- 1 | export default { 2 | base: './', 3 | build: { 4 | sourcemap: true, 5 | target: 'esnext', 6 | rollupOptions: { 7 | input: { 8 | index: 'index.html', 9 | 'reprojection-image': 'reprojection-image.html', 10 | 'reprojection': 'reprojection.html', 11 | 'sphere-mollweide': 'sphere-mollweide.html', 12 | 'wms-image-custom-proj': 'wms-image-custom-proj.html', 13 | 'vector-projections': 'vector-projections.html', 14 | }, 15 | }, 16 | }, 17 | server: { 18 | fs: { 19 | allow: ['/..'] 20 | } 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /js/README.md: -------------------------------------------------------------------------------- 1 | # Running the WASM examples 2 | 3 | ## Running the js tests 4 | 5 | There is a `index.html` file for testing the WASM module in a navigator. 6 | 7 | For security reasons, you need to run it from a server. 8 | You can start a Python server with the following command: 9 | 10 | ```bash 11 | python3 -m http.server 12 | ``` 13 | 14 | The server will automatically serve the `index.html` file in the current directory 15 | 16 | ## Running the OpenLayers demo from Docker container 17 | 18 | ```bash 19 | .docker/ol-run.sh 20 | ``` 21 | 22 | This will build the Node.js image and run the application. Once the application 23 | is started, navigate to http://localhost:5173/. 24 | -------------------------------------------------------------------------------- /proj4rs/src/math/auth.rs: -------------------------------------------------------------------------------- 1 | //! 2 | //! Determine latitude from authalic latitude 3 | //! 4 | 5 | pub(crate) fn authset(es: f64) -> (f64, f64, f64) { 6 | const P00: f64 = 1. / 3.; 7 | const P01: f64 = 31. / 180.; 8 | const P02: f64 = 517. / 5040.; 9 | const P10: f64 = 23. / 360.; 10 | const P11: f64 = 251. / 3780.; 11 | const P20: f64 = 761. / 45360.; 12 | let t = es * es; 13 | ( 14 | es * P00 + t * P01 + t * es * P02, 15 | t * P10 + t * es * P11, 16 | t * es * P20, 17 | ) 18 | } 19 | 20 | pub(crate) fn authlat(beta: f64, apa: (f64, f64, f64)) -> f64 { 21 | let t = beta + beta; 22 | beta + apa.0 * t.sin() + apa.1 * (t + t).sin() + apa.2 * (t + t + t).sin() 23 | } 24 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | 2 | The documentation is available on [docs.rs](https://docs.rs/proj4rs/) and the demo on [docs.3liz.org](https://docs.3liz.org/proj4rs/). 3 | 4 | # Contributing 5 | 6 | ### Modify 7 | 8 | Any Rust file you wish to modify can be found in `src/` with some tests in `tests/proj4js_tests.rs`. 9 | In order to test your new code, you need to build it thanks to WASM. More information on how to build it can be found in [README_WASM.md](./proj4rs/README_WASM.md). 10 | 11 | ### Test 12 | 13 | If you want to test your code, you can run locally some demos by reading [this README](./proj4rs/ol-proj4rs-demo-app/README.md) in `ol-proj4rs-demo-app/`. 14 | You can create another demo page with a HTML file + JavaScript file combo. 15 | -------------------------------------------------------------------------------- /js/ol-proj4rs-demo-app/README.md: -------------------------------------------------------------------------------- 1 | # OpenLayers + Proj4rs with Vite 2 | 3 | This example demonstrates how `OpenLayers` can be used with `Proj4rs`. It is based on `OpenLayers` + [Vite](https://vitejs.dev/). 4 | 5 | To get started, run the following (requires Node 14+): 6 | 7 | ```bash 8 | cd ol-proj4rs-demo-app 9 | npm update 10 | npm start 11 | ``` 12 | 13 | Then go to http://localhost:5173 with your browser, you must have a development server started. 14 | 15 | To generate a build ready for production: 16 | 17 | ```bash 18 | npm run build 19 | ``` 20 | 21 | Then deploy the contents of the `dist` directory to your server. 22 | You can also run `npm run serve` to serve the results of the `dist` directory for preview. 23 | 24 | 25 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [workspace] 2 | members = [ 3 | "proj4rs-geodesic", 4 | "proj4rs", 5 | "proj4rs-clib", 6 | "proj4rs-php", 7 | ] 8 | resolver = "2" 9 | 10 | [workspace.package] 11 | keywords = ["gis", "proj", "projection", "geography", "geospatial"] 12 | authors = ["David Marteau "] 13 | license = "MIT OR Apache-2.0" 14 | homepage = "https://github.com/3liz/proj4rs/" 15 | repository = "https://github.com/3liz/proj4rs/" 16 | categories = ["science::geo"] 17 | 18 | [profile.release] 19 | lto = true 20 | codegen-units = 1 21 | strip = "debuginfo" 22 | incremental = false 23 | 24 | [patch.crates-io] 25 | # Use local crates 26 | proj4rs = { path = "./proj4rs" } 27 | proj4rs-geodesic = { path = "./proj4rs-geodesic" } 28 | 29 | 30 | -------------------------------------------------------------------------------- /proj4rs/src/bin/nadinfos.rs: -------------------------------------------------------------------------------- 1 | //! 2 | //! Display infos about NAD grid 3 | //! 4 | use proj4rs::errors::{Error, Result}; 5 | use proj4rs::nadgrids::{files::read_from_file, Catalog}; 6 | use std::env; 7 | 8 | fn main() -> Result<()> { 9 | let args: Vec = env::args().collect(); 10 | if args.len() <= 1 { 11 | println!("Usage: nadinfos "); 12 | return Err(Error::InvalidParameterValue("Missing filename")); 13 | } 14 | 15 | let key = &args[1]; 16 | 17 | let catalog = Catalog::default(); 18 | read_from_file(&catalog, key)?; 19 | 20 | match catalog.find(key) { 21 | Some(iter) => { 22 | iter.for_each(|g| println!("{g}#")); 23 | Ok(()) 24 | } 25 | None => Err(Error::NadGridNotAvailable), 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /proj4rs/README_WASM.md: -------------------------------------------------------------------------------- 1 | 2 | The documentation is available on [docs.rs](https://docs.rs/proj4rs/) and the demo on [docs.3liz.org](https://docs.3liz.org/proj4rs/). 3 | 4 | # Build locally a package 5 | 6 | ## Compiling for WASM 7 | 8 | Install [wasm-pack](https://rustwasm.github.io/wasm-pack/book/) 9 | 10 | ```bash 11 | wasm-pack build --target web --no-default-features 12 | ``` 13 | 14 | Or if you have installed [cargo-make](https://sagiegurari.github.io/cargo-make/), use the following 15 | command: 16 | 17 | ```bash 18 | cargo make wasm 19 | ``` 20 | 21 | This will create a web package in js/pkg 22 | 23 | ## Build for npm 24 | 25 | ```bash 26 | cargo make wasm_bundle 27 | ``` 28 | 29 | This will create a npm bundler package in js/pkg-bundler 30 | 31 | 32 | Packages are created in the *js/* directory at the root of the repository. 33 | -------------------------------------------------------------------------------- /projections.md: -------------------------------------------------------------------------------- 1 | ## Projections defined in proj4rs 2 | 3 | ``` 4 | [ 5 | (latlong, longlat), 6 | (lcc), 7 | (etmerc, utm), 8 | (tmerc), 9 | (aea, leac), 10 | (stere, ups), 11 | (sterea), 12 | (merc, webmerc), 13 | (geocent, cart), 14 | (somerc), 15 | (laea), 16 | (moll, wag4, wag5), 17 | (geos), 18 | (eqc), 19 | (aeqd), 20 | (krovak), 21 | (mill), 22 | (cea), 23 | ] 24 | ``` 25 | 26 | ## Projections defined in proj4js 27 | 28 | - [+] aea 29 | - [+] aeqd 30 | - [-] bonne 31 | - [-] cass 32 | - [+] cea 33 | - [+] eqc 34 | - [-] eqdc 35 | - [-] eqearth 36 | - [-] equi 37 | - [+] etmerc 38 | - [-] gauss 39 | - [+] geocent 40 | - [+] geos 41 | - [-] gnom 42 | - [-] gstmerc 43 | - [+] krovac 44 | - [+] laea 45 | - [+] lcc 46 | - [+] longlat 47 | - [+] merc 48 | - [+] mill 49 | - [+] moll 50 | - [-] nzmg 51 | - [-] omerc 52 | - [-] ortho 53 | - [-] poly 54 | - [-] qsc 55 | - [-] robin 56 | - [-] sinu 57 | - [+] somerc 58 | - [+] stere 59 | - [+] sterea 60 | - [+] tmerc 61 | - [-] tpers 62 | - [+] utm 63 | - [-] vandg 64 | 65 | -------------------------------------------------------------------------------- /js/tests/test_index.js: -------------------------------------------------------------------------------- 1 | import { proj4 } from '../proj4.js'; 2 | 3 | function test_index() { 4 | proj4.defs('EPSG:3006', '+proj=utm +zone=33 +ellps=GRS80 +towgs84=0,0,0,0,0,0,0 +units=m +no_defs'); 5 | proj4.defs('EPSG:3021', '+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'); 6 | console.log(proj4(proj4.defs('EPSG:3006'), proj4.defs('EPSG:3021'), [319180, 6399862])); 7 | console.log(proj4(proj4.defs('EPSG:3006'), proj4.defs('EPSG:3021')).forward([319180, 6399862])); 8 | 9 | proj4.defs('EPSG:2154', '+proj=lcc +lat_0=46.5 +lon_0=3 +lat_1=49 +lat_2=44 +x_0=700000 +y_0=6600000 +ellps=GRS80 +towgs84=0,0,0,0,0,0,0 +units=m +no_defs +type=crs'); 10 | proj4.defs('EPSG:3857', '+proj=merc +a=6378137 +b=6378137 +lat_ts=0 +lon_0=0 +x_0=0 +y_0=0 +k=1 +units=m +nadgrids=@null +wktext +no_defs +type=crs'); 11 | console.log(proj4(proj4.defs('EPSG:2154'), proj4.defs('EPSG:3857')).forward([489353.59, 6587552.2])); 12 | } 13 | 14 | test_index(); 15 | -------------------------------------------------------------------------------- /proj4rs-clib/examples/test_epsg3035.c: -------------------------------------------------------------------------------- 1 | 2 | #include 3 | #include 4 | 5 | // Sample library usage 6 | 7 | #include "proj4rs.h" 8 | 9 | 10 | int main(void) { 11 | 12 | printf("Initializing\n"); 13 | Proj4rs* src = proj4rs_proj_new("WGS84"); 14 | Proj4rs* dst = proj4rs_proj_new("+proj=laea +lat_0=52 +lon_0=10 +x_0=4321000 +y_0=3210000 +ellps=GRS80"); 15 | 16 | printf("Src: %s\n", proj4rs_proj_projname(src)); 17 | printf("Dst: %s\n", proj4rs_proj_projname(dst)); 18 | 19 | double x = 15.4213696; 20 | double y = 47.0766716; 21 | 22 | printf("Transform\n"); 23 | int res = proj4rs_transform(src, dst, &x , &y, NULL, 1, sizeof(double), true); 24 | if ( res != 1 ) { 25 | printf("Error:\n"); 26 | printf("%s\n", proj4rs_last_error()); 27 | return 1; 28 | } else { 29 | printf("x = %f\n", x); // Should be 4732659.007426 30 | printf("y = %f\n", y); // Should be 2677630.726961 31 | } 32 | 33 | printf("Deleting\n"); 34 | proj4rs_proj_delete(src); 35 | proj4rs_proj_delete(dst); 36 | return 0; 37 | } 38 | -------------------------------------------------------------------------------- /proj4rs-clib/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "proj4rs-clib" 3 | version = "0.1.0" 4 | edition = "2021" 5 | keywords = ["gis", "proj", "projection", "geography", "geospatial"] 6 | authors = ["David Marteau "] 7 | license = "MIT OR Apache-2.0" 8 | homepage = "https://github.com/3liz/proj4rs/" 9 | repository = "https://github.com/3liz/proj4rs/" 10 | description = "C bindings for proj4rs" 11 | exclude=[ 12 | "Makefile.toml", 13 | ] 14 | 15 | [lib] 16 | name = "proj4rs_c" 17 | crate-type = ["cdylib"] 18 | 19 | [dependencies] 20 | proj4rs = "~0.1" 21 | libc = { version = "0.2" } 22 | 23 | [features] 24 | crs-definitions = ["proj4rs/crs-definitions"] 25 | 26 | # XXX Defined in workspace 27 | #[profile.release] 28 | #lto = true 29 | #codegen-units = 1 30 | #strip = "debuginfo" 31 | 32 | [package.metadata.deb] 33 | maintainer = "David Marteau " 34 | copyright = "2024, 3liz" 35 | extended-description = """Projection library inspired from proj""" 36 | depends = "$auto" 37 | section = "development" 38 | priority = "optional" 39 | assets = [ 40 | ["../target/release/libproj4rs_c.so", "usr/lib/", "755"], 41 | ["../target/cbindgen/proj4rs.h", "usr/include/", "644"] 42 | ] 43 | 44 | -------------------------------------------------------------------------------- /js/ol-proj4rs-demo-app/codeStyle.css: -------------------------------------------------------------------------------- 1 | .code-section { 2 | display: grid; 3 | margin-bottom: 25px; 4 | } 5 | 6 | pre { 7 | background-color: #ffffff; 8 | width: 90%; 9 | min-width: 600px; 10 | text-align: left; 11 | margin: auto; 12 | margin-bottom: 15px; 13 | border: solid rgba(128, 128, 128, 0.47) 1px; 14 | border-radius: 5px; 15 | padding-left: 20px; 16 | font-size: 15px; 17 | } 18 | 19 | pre > h3 { 20 | margin: 0 0 -20px; 21 | color: rgba(58, 58, 58, 0.89); 22 | } 23 | 24 | .token.keyword { 25 | color: #0000ff; 26 | } 27 | 28 | .token.string { 29 | color: #A31515; 30 | } 31 | 32 | .token.constant { 33 | color: #36acaa; 34 | } 35 | 36 | .change { 37 | background-color: rgba(255, 217, 217, 0.59); 38 | } 39 | 40 | @media screen and (min-width: 1271px) { 41 | .left { 42 | grid-column: 1; 43 | grid-row: 1; 44 | } 45 | 46 | .right { 47 | grid-column: 2; 48 | grid-row: 1; 49 | } 50 | } 51 | 52 | @media screen and (max-width: 1270px) { 53 | .left { 54 | grid-column: 1; 55 | grid-row: 1; 56 | } 57 | 58 | .right { 59 | grid-column: 1; 60 | grid-row: 2; 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /proj4rs/src/prime_meridians.rs: -------------------------------------------------------------------------------- 1 | /// 2 | /// Static prime meridians definitions 3 | /// 4 | #[rustfmt::skip] 5 | pub(crate) const PRIME_MERIDIANS: [(&str, &str, f64); 14] = [ 6 | ("greenwich", "0dE", 0.0), 7 | ("lisbon", "9d07'54.862\"W", -9.131906111111), 8 | ("paris", "2d20'14.025\"E", 2.337229166667), 9 | ("bogota", "74d04'51.3\"W", -74.080916666667), 10 | ("madrid", "3d41'16.58\"W", -3.687938888889), 11 | ("rome", "12d27'8.4\"E", 12.452333333333), 12 | ("bern", "7d26'22.5\"E", 7.439583333333), 13 | ("jakarta", "106d48'27.79\"E", 106.807719444444), 14 | ("ferro", "17d40'W", -17.666666666667), 15 | ("brussels", "4d22'4.71\"E", 4.367975), 16 | ("stockholm", "18d3'29.8\"E", 18.058277777778), 17 | ("athens", "23d42'58.815\"E", 23.7163375), 18 | ("oslo", "10d43'22.5\"E", 10.722916666667), 19 | ("copenhagen", "12d34'40.35\"E", 12.57788), 20 | ]; 21 | 22 | /// Return the datum definition 23 | pub fn find_prime_meridian(name: &str) -> Option { 24 | PRIME_MERIDIANS 25 | .iter() 26 | .find(|d| d.0.eq_ignore_ascii_case(name)) 27 | .map(|d| d.2) 28 | } 29 | -------------------------------------------------------------------------------- /js/tests/nodejs/test_core.js: -------------------------------------------------------------------------------- 1 | let Proj = await import("../../pkg-node/proj4rs.js"); 2 | 3 | function test_core() { 4 | //EPSG:3006 5 | var sweref99tm = '+proj=utm +zone=33 +ellps=GRS80 +towgs84=0,0,0,0,0,0,0 +units=m +no_defs'; 6 | // EPSG:3021 7 | var 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'; 8 | let from = new Proj.Projection(sweref99tm); 9 | let to = new Proj.Projection(rt90); 10 | let point = new Proj.Point(319180, 6399862, 0.0); 11 | Proj.transform(from, to, point); 12 | console.log(`=> ${point.x} ${point.y}`); 13 | 14 | var epsg2154 = '+proj=lcc +lat_0=46.5 +lon_0=3 +lat_1=49 +lat_2=44 +x_0=700000 +y_0=6600000 +ellps=GRS80 +towgs84=0,0,0,0,0,0,0 +units=m +no_defs +type=crs'; 15 | var epsg3857 = '+proj=merc +a=6378137 +b=6378137 +lat_ts=0 +lon_0=0 +x_0=0 +y_0=0 +k=1 +units=m +nadgrids=@null +wktext +no_defs +type=crs'; 16 | from = new Proj.Projection(epsg2154); 17 | to = new Proj.Projection(epsg3857); 18 | point = new Proj.Point(489353.59, 6587552.2, 0.0); 19 | Proj.transform(from, to, point); 20 | console.log(`=> ${point.x} ${point.y}`); 21 | } 22 | 23 | test_core(); 24 | -------------------------------------------------------------------------------- /proj4rs/src/math/aasincos.rs: -------------------------------------------------------------------------------- 1 | //! arc sin, cosine, tan2 and sqrt that will NOT fail 2 | //! 3 | #![allow(dead_code)] 4 | use crate::errors::{Error, Result}; 5 | use crate::math::consts::{FRAC_PI_2, PI}; 6 | 7 | const ONE_TOL: f64 = 1.000_000_000_000_01; 8 | const ATOL: f64 = 1.0e-50; 9 | 10 | pub(crate) fn aasin(v: f64) -> Result { 11 | let av = v.abs(); 12 | if av >= 1. { 13 | if av > ONE_TOL { 14 | Err(Error::ArgumentTooLarge) 15 | } else { 16 | Ok(FRAC_PI_2 * v.signum()) 17 | } 18 | } else { 19 | Ok(v.asin()) 20 | } 21 | } 22 | 23 | pub(crate) fn aacos(v: f64) -> Result { 24 | let av = v.abs(); 25 | if av >= 1. { 26 | if av > ONE_TOL { 27 | Err(Error::ArgumentTooLarge) 28 | } else if v < 0. { 29 | Ok(PI) 30 | } else { 31 | Ok(0.) 32 | } 33 | } else { 34 | Ok(v.acos()) 35 | } 36 | } 37 | 38 | pub(crate) fn asqrt(v: f64) -> f64 { 39 | if v <= 0. { 40 | 0. 41 | } else { 42 | v.sqrt() 43 | } 44 | } 45 | 46 | pub(crate) fn aatan2(n: f64, d: f64) -> f64 { 47 | if n.abs() < ATOL && d.abs() < ATOL { 48 | 0. 49 | } else { 50 | n.atan2(d) 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /proj4rs/src/math/phi2.rs: -------------------------------------------------------------------------------- 1 | //! Determine latitude angle phi-2. 2 | //! 3 | //! Inputs: 4 | //! ts = exp(-psi) where psi is the isometric latitude (dimensionless) 5 | //! e = eccentricity of the ellipsoid (dimensionless) 6 | //! Output: 7 | //! phi = geographic latitude (radians) 8 | //! Here isometric latitude is defined by 9 | //! psi = log( tan(pi/4 + phi/2) * 10 | //! ( (1 - e*sin(phi)) / (1 + e*sin(phi)) )^(e/2) ) 11 | //! = asinh(tan(phi)) - e * atanh(e * sin(phi)) 12 | //! This routine inverts this relation using the iterative scheme given 13 | //! by Snyder (1987), Eqs. (7-9) - (7-11) 14 | //! 15 | use super::consts::{EPS_10, FRAC_PI_2}; 16 | use crate::errors::{Error, Result}; 17 | 18 | const PHI2_NITER: i32 = 15; 19 | 20 | pub(crate) fn phi2(ts: f64, e: f64) -> Result { 21 | let eccnth = 0.5 * e; 22 | let mut phi = FRAC_PI_2 - 2. * ts.atan(); 23 | let mut i = PHI2_NITER; 24 | while i > 0 { 25 | let con = e * phi.sin(); 26 | let dphi = FRAC_PI_2 - 2. * (ts * ((1. - con) / (1. + con)).powf(eccnth)).atan() - phi; 27 | 28 | phi += dphi; 29 | 30 | if dphi.abs() <= EPS_10 { 31 | break; 32 | } 33 | 34 | i -= 1; 35 | } 36 | 37 | if i <= 0 { 38 | Err(Error::NonInvPhi2Convergence) 39 | } else { 40 | Ok(phi) 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /proj4rs-clib/pyproject.toml: -------------------------------------------------------------------------------- 1 | # Use Maturin https://www.maturin.rs/ 2 | [build-system] 3 | requires = ["maturin>=1.7,<2.0"] 4 | build-backend = "maturin" 5 | 6 | [project] 7 | name = "proj4rs" 8 | requires-python = ">=3.12" 9 | classifiers = [ 10 | "Programming Language :: Rust", 11 | "Programming Language :: Python :: Implementation :: CPython", 12 | "Programming Language :: Python :: Implementation :: PyPy", 13 | ] 14 | dependencies = ["cffi"] 15 | dynamic = ["version"] 16 | 17 | [tool.maturin] 18 | bindings = "cffi" 19 | python-source = "python" 20 | module-name = "proj4rs._proj4rs" 21 | 22 | [tool.ruff] 23 | # Ruff configuration 24 | # See https://docs.astral.sh/ruff/configuration/ 25 | line-length = 120 26 | target-version = "py312" 27 | extend-exclude = ["python/proj4rs/_proj4rs"] 28 | 29 | [tool.ruff.format] 30 | indent-style = "space" 31 | 32 | [tool.ruff.lint] 33 | extend-select = ["E", "F", "I", "ANN", "W", "T", "COM", "RUF"] 34 | ignore = ["ANN002", "ANN003"] 35 | 36 | [tool.ruff.lint.per-file-ignores] 37 | "python/tests/*" = ["T201"] 38 | 39 | [tool.ruff.lint.isort] 40 | lines-between-types = 1 41 | 42 | [tool.ruff.lint.flake8-annotations] 43 | ignore-fully-untyped = true 44 | suppress-none-returning = true 45 | suppress-dummy-args = true 46 | 47 | [tool.mypy] 48 | python_version = "3.12" 49 | allow_redefinition = true 50 | 51 | [[tool.mypy.overrides]] 52 | module = "_cffi_backend" 53 | ignore_missing_imports = true 54 | -------------------------------------------------------------------------------- /proj4rs-clib/Makefile.toml: -------------------------------------------------------------------------------- 1 | [tasks.default] 2 | alias = "defaults" 3 | 4 | [tasks.defaults] 5 | dependencies = [ 6 | "build" 7 | ] 8 | 9 | [tasks.build] 10 | command = "cargo" 11 | args = ["build"] 12 | 13 | [tasks."build-release"] 14 | command = "cargo" 15 | args = ["build", "--release"] 16 | 17 | [tasks.cbindgen] 18 | command = "cbindgen" 19 | args = [ 20 | "--config", "cbindgen.toml", 21 | "--crate", "proj4rs-clib", 22 | "--output", "../target/cbindgen/proj4rs.h", 23 | "--quiet", 24 | ] 25 | 26 | [tasks.deb] 27 | command = "cargo" 28 | args = [ 29 | "deb", 30 | "--profile", "release", 31 | "--no-separate-debug-symbols", 32 | "--no-strip", 33 | "--no-build", 34 | ] 35 | dependencies = ["build-release", "cbindgen"] 36 | 37 | [tasks.release] 38 | dependencies = ["deb"] 39 | 40 | 41 | [tasks."python.lint"] 42 | command = "ruff" 43 | args = [ 44 | "check", 45 | "--output-format", "concise", 46 | "python", 47 | ] 48 | 49 | 50 | [tasks."python.build-dev"] 51 | command = "maturin" 52 | args = ["develop"] 53 | 54 | 55 | [tasks."python.lint-fix"] 56 | command = "ruff" 57 | args = [ 58 | "check", 59 | "--preview", 60 | "--fix", 61 | "python", 62 | ] 63 | 64 | 65 | [tasks."python.typing"] 66 | command = "mypy" 67 | args = ["python"] 68 | 69 | 70 | [tasks."python.test"] 71 | command = "pytest" 72 | args = [ "-v", "python/tests"] 73 | dependencies = ["python.build-dev", "python.lint", "python.typing"] 74 | -------------------------------------------------------------------------------- /js/ol-proj4rs-demo-app/reprojection-image.js: -------------------------------------------------------------------------------- 1 | import Map from 'ol/Map.js'; 2 | import OSM from 'ol/source/OSM.js'; 3 | import Static from 'ol/source/ImageStatic.js'; 4 | import View from 'ol/View.js'; 5 | import {proj4} from './assets/js/proj4.js'; 6 | import {Image as ImageLayer, Tile as TileLayer} from 'ol/layer.js'; 7 | import {getCenter} from 'ol/extent.js'; 8 | import {register} from 'ol/proj/proj4.js'; 9 | import {transform} from 'ol/proj.js'; 10 | 11 | proj4.defs( 12 | 'EPSG:27700', 13 | '+proj=tmerc +lat_0=49 +lon_0=-2 +k=0.9996012717 ' + 14 | '+x_0=400000 +y_0=-100000 +ellps=airy ' + 15 | '+towgs84=446.448,-125.157,542.06,0.15,0.247,0.842,-20.489 ' + 16 | '+units=m +no_defs' 17 | ); 18 | register(proj4); 19 | 20 | const imageExtent = [0, 0, 700000, 1300000]; 21 | const imageLayer = new ImageLayer(); 22 | 23 | const map = new Map({ 24 | layers: [ 25 | new TileLayer({ 26 | source: new OSM(), 27 | }), 28 | imageLayer, 29 | ], 30 | target: 'map', 31 | view: new View({ 32 | center: transform(getCenter(imageExtent), 'EPSG:27700', 'EPSG:3857'), 33 | zoom: 5, 34 | }), 35 | }); 36 | 37 | const interpolate = document.getElementById('interpolate'); 38 | 39 | function setSource() { 40 | const source = new Static({ 41 | url: 42 | 'https://upload.wikimedia.org/wikipedia/commons/thumb/1/18/' + 43 | 'British_National_Grid.svg/2000px-British_National_Grid.svg.png', 44 | crossOrigin: '', 45 | projection: 'EPSG:27700', 46 | imageExtent: imageExtent, 47 | interpolate: interpolate.checked, 48 | }); 49 | imageLayer.setSource(source); 50 | } 51 | setSource(); 52 | 53 | interpolate.addEventListener('change', setSource); 54 | -------------------------------------------------------------------------------- /js/tests/test_core.js: -------------------------------------------------------------------------------- 1 | import { proj4, Proj } from '../proj4.js'; 2 | 3 | function test_core() { 4 | //EPSG:3006 5 | var sweref99tm = '+proj=utm +zone=33 +ellps=GRS80 +towgs84=0,0,0,0,0,0,0 +units=m +no_defs'; 6 | // EPSG:3021 7 | var 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'; 8 | console.log(proj4(sweref99tm, rt90, [319180, 6399862])); 9 | var rslt = proj4(sweref99tm, rt90).forward([319180, 6399862]); 10 | console.log(rslt); 11 | let from = new Proj.Projection(sweref99tm); 12 | let to = new Proj.Projection(rt90); 13 | let point = new Proj.Point(319180, 6399862, 0.0); 14 | Proj.transform(from, to, point); 15 | console.log(`=> ${point.x} ${point.y}`); 16 | console.log(point.x == rslt[0]); 17 | console.log(point.y == rslt[1]); 18 | 19 | var epsg2154 = '+proj=lcc +lat_0=46.5 +lon_0=3 +lat_1=49 +lat_2=44 +x_0=700000 +y_0=6600000 +ellps=GRS80 +towgs84=0,0,0,0,0,0,0 +units=m +no_defs +type=crs'; 20 | var epsg3857 = '+proj=merc +a=6378137 +b=6378137 +lat_ts=0 +lon_0=0 +x_0=0 +y_0=0 +k=1 +units=m +nadgrids=@null +wktext +no_defs +type=crs'; 21 | rslt = proj4(epsg2154, epsg3857).forward([489353.59, 6587552.2]); 22 | console.log(rslt); 23 | from = new Proj.Projection(epsg2154); 24 | to = new Proj.Projection(epsg3857); 25 | point = new Proj.Point(489353.59, 6587552.2, 0.0); 26 | Proj.transform(from, to, point); 27 | console.log(`=> ${point.x} ${point.y}`); 28 | console.log(point.x == rslt[0]); 29 | console.log(point.y == rslt[1]); 30 | } 31 | 32 | test_core(); 33 | -------------------------------------------------------------------------------- /proj4rs/src/parse.rs: -------------------------------------------------------------------------------- 1 | //! 2 | //! Parser for numbers 3 | //! 4 | //! If we are in wasm mode, then fallback to the Js functions 5 | //! this is a 20Ko gain for the .wasm binary. 6 | //! 7 | 8 | #[cfg(all(target_arch = "wasm32", target_os = "unknown"))] 9 | mod wasm { 10 | //! Use js lib 11 | 12 | use crate::errors::Error; 13 | use js_sys::{parse_float, parse_int}; 14 | 15 | pub trait FromStr: Sized { 16 | type Err; 17 | 18 | fn from_str(s: &str) -> Result; 19 | } 20 | 21 | impl FromStr for f64 { 22 | type Err = Error; 23 | 24 | fn from_str(s: &str) -> Result { 25 | let v = parse_float(s); 26 | if v.is_nan() { 27 | Err(Error::JsParseError) 28 | } else { 29 | Ok(v) 30 | } 31 | } 32 | } 33 | 34 | impl FromStr for i32 { 35 | type Err = Error; 36 | 37 | fn from_str(s: &str) -> Result { 38 | let v = parse_int(s, 10); 39 | if v.is_nan() { 40 | Err(Error::JsParseError) 41 | } else { 42 | Ok(v as i32) 43 | } 44 | } 45 | } 46 | 47 | impl FromStr for bool { 48 | type Err = Error; 49 | 50 | fn from_str(s: &str) -> Result { 51 | match s { 52 | "true" => Ok(true), 53 | "false" => Ok(false), 54 | _ => Err(Error::JsParseError), 55 | } 56 | } 57 | } 58 | } 59 | 60 | #[cfg(all(target_arch = "wasm32", target_os = "unknown"))] 61 | pub use wasm::FromStr; 62 | 63 | #[cfg(not(all(target_arch = "wasm32", target_os = "unknown")))] 64 | pub use std::str::FromStr; 65 | -------------------------------------------------------------------------------- /proj4rs/src/projections/mill.rs: -------------------------------------------------------------------------------- 1 | //! 2 | //! Miller Cylindrical (Spherical) 3 | //! 4 | //! See https://proj.org/en/stable/operations/projections/mill.html 5 | //! 6 | use crate::errors::Result; 7 | use crate::math::consts::FRAC_PI_4; 8 | use crate::parameters::ParamList; 9 | use crate::proj::ProjData; 10 | 11 | super::projection! { mill } 12 | 13 | #[derive(Debug, Clone)] 14 | pub(crate) struct Projection {} 15 | 16 | impl Projection { 17 | pub fn mill(p: &mut ProjData, _: &ParamList) -> Result { 18 | p.ellps = crate::ellps::Ellipsoid::sphere(p.ellps.a)?; 19 | Ok(Self {}) 20 | } 21 | 22 | #[inline(always)] 23 | pub fn forward(&self, lam: f64, phi: f64, z: f64) -> Result<(f64, f64, f64)> { 24 | Ok((lam, (FRAC_PI_4 + phi * 0.4).tan().ln() * 1.25, z)) 25 | } 26 | 27 | #[inline(always)] 28 | pub fn inverse(&self, x: f64, y: f64, z: f64) -> Result<(f64, f64, f64)> { 29 | Ok((x, 2.5 * ((0.8 * y).exp().atan() - FRAC_PI_4), z)) 30 | } 31 | 32 | pub const fn has_inverse() -> bool { 33 | true 34 | } 35 | 36 | pub const fn has_forward() -> bool { 37 | true 38 | } 39 | } 40 | 41 | //============ 42 | // Tests 43 | //============ 44 | 45 | #[cfg(test)] 46 | mod tests { 47 | use crate::proj::Proj; 48 | use crate::tests::utils::{test_proj_forward, test_proj_inverse}; 49 | 50 | #[test] 51 | fn proj_mill() { 52 | let p = Proj::from_proj_string("+proj=mill").unwrap(); 53 | 54 | println!("{:#?}", p.projection()); 55 | 56 | let inputs = [( 57 | (-100., 35.0, 0.), 58 | (-11131949.079327356070, 4061217.237063715700, 0.), 59 | )]; 60 | 61 | test_proj_forward(&p, &inputs, 1e-8); 62 | test_proj_inverse(&p, &inputs, 1e-8); 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /js/ol-proj4rs-demo-app/sphere-mollweide.js: -------------------------------------------------------------------------------- 1 | import GeoJSON from 'ol/format/GeoJSON.js'; 2 | import Graticule from 'ol/layer/Graticule.js'; 3 | import Map from 'ol/Map.js'; 4 | import Projection from 'ol/proj/Projection.js'; 5 | import VectorLayer from 'ol/layer/Vector.js'; 6 | import VectorSource from 'ol/source/Vector.js'; 7 | import View from 'ol/View.js'; 8 | import {proj4} from './assets/js/proj4.js'; 9 | import {Fill, Style} from 'ol/style.js'; 10 | import {register} from 'ol/proj/proj4.js'; 11 | 12 | proj4.defs( 13 | 'ESRI:53009', 14 | '+proj=moll +lon_0=0 +x_0=0 +y_0=0 +a=6371000 ' + 15 | '+b=6371000 +units=m +no_defs' 16 | ); 17 | register(proj4); 18 | 19 | // Configure the Sphere Mollweide projection object with an extent, 20 | // and a world extent. These are required for the Graticule. 21 | const sphereMollweideProjection = new Projection({ 22 | code: 'ESRI:53009', 23 | extent: [ 24 | -18019909.21177587, -9009954.605703328, 18019909.21177587, 25 | 9009954.605703328, 26 | ], 27 | worldExtent: [-179, -89.99, 179, 89.99], 28 | }); 29 | 30 | const style = new Style({ 31 | fill: new Fill({ 32 | color: '#eeeeee', 33 | }), 34 | }); 35 | 36 | const map = new Map({ 37 | keyboardEventTarget: document, 38 | layers: [ 39 | new VectorLayer({ 40 | source: new VectorSource({ 41 | url: 'https://openlayers.org/data/vector/ecoregions.json', 42 | format: new GeoJSON(), 43 | }), 44 | style: function (feature) { 45 | const color = feature.get('COLOR_BIO') || '#eeeeee'; 46 | style.getFill().setColor(color); 47 | return style; 48 | }, 49 | }), 50 | new Graticule(), 51 | ], 52 | target: 'map', 53 | view: new View({ 54 | center: [0, 0], 55 | projection: sphereMollweideProjection, 56 | zoom: 2, 57 | }), 58 | }); 59 | -------------------------------------------------------------------------------- /.github/workflows/docs.yml: -------------------------------------------------------------------------------- 1 | name: 📖 Documentation 2 | 3 | on: 4 | push: 5 | # tags: 6 | # - '*' 7 | branches: 8 | - main 9 | pull_request: 10 | branches: 11 | - main 12 | workflow_dispatch: 13 | 14 | permissions: 15 | id-token: write 16 | pages: write 17 | 18 | jobs: 19 | proj4rs-demo: 20 | name: "🟨 Proj4rs demo" 21 | runs-on: ubuntu-latest 22 | steps: 23 | 24 | - name: Check out repository 25 | uses: actions/checkout@v4 26 | 27 | - name: Install Rust toolchain 28 | uses: actions-rs/toolchain@v1 29 | with: 30 | toolchain: stable 31 | 32 | - name: Install WASM pack 33 | uses: jetli/wasm-pack-action@v0.4.0 34 | with: 35 | # Optional version of wasm-pack to install(eg. 'v0.9.1', 'latest') 36 | version: 'latest' 37 | 38 | - name: WASM Pack 39 | working-directory: proj4rs 40 | run: wasm-pack build --target web --release --out-dir=../js/pkg --no-default-features --features=proj4js-compat 41 | 42 | - name: Install Node 43 | uses: actions/setup-node@v4 44 | 45 | - name: Npm update 46 | working-directory: js/ol-proj4rs-demo-app 47 | run: npm --loglevel=verbose update 48 | 49 | - name: Npm build 50 | working-directory: js/ol-proj4rs-demo-app 51 | run: | 52 | npm run build 53 | 54 | - name: Upload artifact 55 | uses: actions/upload-pages-artifact@v3 56 | with: 57 | path: js/ol-proj4rs-demo-app/dist 58 | 59 | - name: Setup Pages 60 | if: github.ref == 'refs/heads/main' 61 | uses: actions/configure-pages@v4 62 | 63 | - name: Deploy to GitHub Pages 64 | if: github.ref == 'refs/heads/main' 65 | id: deployment 66 | uses: actions/deploy-pages@v4 67 | 68 | - name: Summary 69 | if: github.ref == 'refs/heads/main' 70 | run: | 71 | echo " 72 | ### Published ! :rocket: 73 | 74 | [Visit the doc](https://docs.3liz.org/proj4rs/) 75 | " >> $GITHUB_STEP_SUMMARY 76 | -------------------------------------------------------------------------------- /proj4rs/src/projections/geocent.rs: -------------------------------------------------------------------------------- 1 | //! 2 | //! Stub projection implementation for geocent coordinates. 3 | //! 4 | //! No transformation occurs here because it is handled in transform.rs 5 | //! 6 | use crate::errors::Result; 7 | use crate::parameters::ParamList; 8 | use crate::proj::{ProjData, ProjType}; 9 | 10 | // Projection stub 11 | super::projection! { geocent, cart } 12 | 13 | #[derive(Debug, Clone)] 14 | pub(crate) struct Projection {} 15 | 16 | impl Projection { 17 | pub fn geocent(p: &mut ProjData, _: &ParamList) -> Result { 18 | p.proj_type = ProjType::Geocentric; 19 | p.x0 = 0.; 20 | p.y0 = 0.; 21 | Ok(Self {}) 22 | } 23 | 24 | pub fn cart(p: &mut ProjData, params: &ParamList) -> Result { 25 | Self::geocent(p, params) 26 | } 27 | 28 | #[inline(always)] 29 | pub fn forward(&self, lam: f64, phi: f64, z: f64) -> Result<(f64, f64, f64)> { 30 | Ok((lam, phi, z)) 31 | } 32 | 33 | #[inline(always)] 34 | pub fn inverse(&self, x: f64, y: f64, z: f64) -> Result<(f64, f64, f64)> { 35 | Ok((x, y, z)) 36 | } 37 | 38 | pub const fn has_inverse() -> bool { 39 | true 40 | } 41 | 42 | pub const fn has_forward() -> bool { 43 | true 44 | } 45 | } 46 | 47 | #[cfg(test)] 48 | mod tests { 49 | use crate::adaptors::transform_xyz; 50 | use crate::proj::Proj; 51 | use approx::assert_abs_diff_eq; 52 | 53 | #[test] 54 | fn proj_geocent() { 55 | let p_from = Proj::from_proj_string("+proj=latlong").unwrap(); 56 | let p_to = Proj::from_proj_string("+proj=geocent +R=1").unwrap(); 57 | 58 | let (lon_in, lat_in, z_in) = (0.0f64, 0.0f64, 0.0f64); 59 | 60 | let (x, y, z) = transform_xyz( 61 | &p_from, 62 | &p_to, 63 | lon_in.to_radians(), 64 | lat_in.to_radians(), 65 | z_in, 66 | ) 67 | .unwrap(); 68 | 69 | assert_abs_diff_eq!(x, 1.0, epsilon = 1.0e-8); 70 | assert_abs_diff_eq!(y, 0.0, epsilon = 1.0e-8); 71 | assert_abs_diff_eq!(z, 0.0, epsilon = 1.0e-8); 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /js/tests/test_defs.js: -------------------------------------------------------------------------------- 1 | import { proj4 } from '../proj4.js'; 2 | 3 | var defs = proj4.defs; 4 | 5 | function test_defs() { 6 | defs( 7 | 'EPSG:27700', 8 | '+proj=tmerc +lat_0=49 +lon_0=-2 +k=0.9996012717 ' + 9 | '+x_0=400000 +y_0=-100000 +ellps=airy ' + 10 | '+towgs84=446.448,-125.157,542.06,0.15,0.247,0.842,-20.489 ' + 11 | '+units=m +no_defs' 12 | ); 13 | defs( 14 | 'EPSG:23032', 15 | '+proj=utm +zone=32 +ellps=intl ' + 16 | '+towgs84=-87,-98,-121,0,0,0,0 +units=m +no_defs' 17 | ); 18 | defs( 19 | 'EPSG:5479', 20 | '+proj=lcc +lat_1=-76.66666666666667 +lat_2=' + 21 | '-79.33333333333333 +lat_0=-78 +lon_0=163 +x_0=7000000 +y_0=5000000 ' + 22 | '+ellps=GRS80 +towgs84=0,0,0,0,0,0,0 +units=m +no_defs' 23 | ); 24 | /* somerc projection not supported yet 25 | defs( 26 | 'EPSG:21781', 27 | '+proj=somerc +lat_0=46.95240555555556 ' + 28 | '+lon_0=7.439583333333333 +k_0=1 +x_0=600000 +y_0=200000 +ellps=bessel ' + 29 | '+towgs84=674.4,15.1,405.3,0,0,0,0 +units=m +no_defs' 30 | );*/ 31 | defs( 32 | 'EPSG:3413', 33 | '+proj=stere +lat_0=90 +lat_ts=70 +lon_0=-45 +k=1 ' + 34 | '+x_0=0 +y_0=0 +datum=WGS84 +units=m +no_defs' 35 | ); 36 | /* laea projection not supported yet 37 | defs( 38 | 'EPSG:2163', 39 | '+proj=laea +lat_0=45 +lon_0=-100 +x_0=0 +y_0=0 ' + 40 | '+a=6370997 +b=6370997 +units=m +no_defs' 41 | );*/ 42 | /* moll projection not supported yet 43 | defs( 44 | 'ESRI:54009', 45 | '+proj=moll +lon_0=0 +x_0=0 +y_0=0 +datum=WGS84 ' + '+units=m +no_defs' 46 | );*/ 47 | console.log(defs('EPSG:27700')) 48 | console.log(defs['EPSG:27700']) 49 | console.log(defs['EPSG:4326']); 50 | console.log(defs['EPSG:4269']); 51 | console.log(defs['EPSG:3857']); 52 | console.log(defs.WGS84 == defs('EPSG:4326')); 53 | console.log(defs['EPSG:3785'] == defs('EPSG:3785')); 54 | console.log(defs.GOOGLE == defs('EPSG:3785')); 55 | console.log(defs['EPSG:900913'] == defs('EPSG:3785')); 56 | console.log(defs['EPSG:102113'] == defs('EPSG:3785')); 57 | } 58 | 59 | test_defs(); 60 | 61 | -------------------------------------------------------------------------------- /proj4rs/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "proj4rs" 3 | version = "0.1.9" 4 | edition = "2021" 5 | description = "Rust adaptation of Proj4" 6 | readme = "../README.md" 7 | keywords.workspace = true 8 | authors.workspace = true 9 | license.workspace = true 10 | homepage.workspace = true 11 | repository.workspace = true 12 | categories.workspace = true 13 | documentation = "https://docs.rs/proj4rs/" 14 | exclude = [ 15 | "js/*", 16 | "Makefile.toml", 17 | "ol-proj4rs-demo-app", 18 | "fixtures", 19 | ".docker", 20 | "PROJ4_General_Parameters.pdf", 21 | "index.html", 22 | ] 23 | 24 | [dependencies] 25 | thiserror = "2.0" 26 | crs-definitions = { version = "0.3", optional = true, default-features = false, features = ["proj4"] } 27 | geo-types = { version = "0.7.12", optional = true } 28 | log = { version = "0.4", optional = true } 29 | proj4rs-geodesic = { version = "~0.1", optional = true } 30 | 31 | [dev-dependencies] 32 | approx = "0.5" 33 | clap = { version = "4", features = ["derive"] } 34 | env_logger = "0.11" 35 | log = "0.4" 36 | rand = "0.9" 37 | 38 | # XXX Defined in workspace 39 | #[profile.release] 40 | #lto = true 41 | #codegen-units = 1 42 | #strip = "debuginfo" 43 | 44 | [lib] 45 | crate-type = ["cdylib", "rlib"] 46 | 47 | [features] 48 | default = ["multi-thread", "binaries"] 49 | binaries = [] 50 | multi-thread = [] 51 | geo-types = ["dep:geo-types"] 52 | logging = ["log"] 53 | local_tests = [] 54 | wasm-strict = [] 55 | proj4js-compat = [] 56 | with-wasm-entrypoint = [] 57 | aeqd = ["proj4rs-geodesic"] 58 | krovak = [] 59 | esri = [] 60 | 61 | [target.wasm32-unknown-unknown.dependencies] 62 | wasm-bindgen = "0.2" 63 | js-sys = "0.3" 64 | web-sys = { version = "0.3", features = ["console"] } 65 | console_log = "1.0" 66 | 67 | [[bin]] 68 | name = "nadinfos" 69 | required-features = ["binaries"] 70 | 71 | [[example]] 72 | name = "rsproj" 73 | 74 | [package.metadata.doc.rs] 75 | all-features = true 76 | 77 | [package.metadata.wasm-pack.profile.release] 78 | # Fix 'table.fill requires bulk-memory on' error 79 | # Maybe related to https://github.com/rustwasm/wasm-bindgen/issues/4250 80 | # Should be resolved with wasm-bindgen 0.2.95 81 | wasm-opt = ["-O", "--enable-bulk-memory"] 82 | -------------------------------------------------------------------------------- /proj4rs/src/projections/latlong.rs: -------------------------------------------------------------------------------- 1 | //! 2 | //! Stub projection implementation for lat/long coordinates. 3 | //! 4 | //! We don't actually change the coordinates, but we want proj=latlong 5 | //! to act sort of like a projection. 6 | //! 7 | use crate::errors::Result; 8 | use crate::parameters::ParamList; 9 | use crate::proj::{ProjData, ProjType}; 10 | 11 | // Projection stub 12 | super::projection! { latlong, longlat } 13 | 14 | #[derive(Debug, Clone)] 15 | pub(crate) struct Projection {} 16 | 17 | impl Projection { 18 | pub fn latlong(p: &mut ProjData, _: &ParamList) -> Result { 19 | p.proj_type = ProjType::Latlong; 20 | p.x0 = 0.; 21 | p.y0 = 0.; 22 | Ok(Self {}) 23 | } 24 | 25 | pub fn longlat(p: &mut ProjData, params: &ParamList) -> Result { 26 | Self::latlong(p, params) 27 | } 28 | 29 | #[inline(always)] 30 | pub fn forward(&self, lam: f64, phi: f64, z: f64) -> Result<(f64, f64, f64)> { 31 | Ok((lam, phi, z)) 32 | } 33 | 34 | #[inline(always)] 35 | pub fn inverse(&self, x: f64, y: f64, z: f64) -> Result<(f64, f64, f64)> { 36 | Ok((x, y, z)) 37 | } 38 | 39 | pub const fn has_inverse() -> bool { 40 | true 41 | } 42 | 43 | pub const fn has_forward() -> bool { 44 | true 45 | } 46 | } 47 | 48 | #[cfg(test)] 49 | mod tests { 50 | use crate::adaptors::transform_xy; 51 | use crate::proj::Proj; 52 | 53 | #[test] 54 | fn proj_latlon_init() { 55 | let p = Proj::from_proj_string("+proj=latlong +datum=WGS84").unwrap(); 56 | 57 | let d = p.data(); 58 | 59 | assert_eq!(d.x0, 0.); 60 | assert_eq!(d.y0, 0.); 61 | assert_eq!(p.projname(), "latlong"); 62 | } 63 | 64 | #[test] 65 | fn proj_latlon_to_latlon() { 66 | let p_from = Proj::from_proj_string("+proj=latlong +datum=WGS84").unwrap(); 67 | let p_to = Proj::from_proj_string("+proj=latlong +datum=WGS84").unwrap(); 68 | 69 | let (lon_in, lat_in) = (2.3522219, 48.856614); 70 | 71 | let (lon_out, lat_out) = transform_xy(&p_from, &p_to, lon_in, lat_in).unwrap(); 72 | assert_eq!((lon_out, lat_out), (lon_in, lat_in)); 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /proj4rs/Makefile.toml: -------------------------------------------------------------------------------- 1 | [tasks.default] 2 | alias = "defaults" 3 | 4 | [tasks.defaults] 5 | dependencies = [ 6 | "build" 7 | ] 8 | 9 | [tasks.build] 10 | description = "Dev build" 11 | command = "cargo" 12 | args = ["build"] 13 | 14 | [tasks.release] 15 | description = "Release build" 16 | command = "cargo" 17 | args = ["build", "--release"] 18 | 19 | [tasks.doc] 20 | description = "Build documentation" 21 | command = "cargo" 22 | args = ["doc", "--all-features", "--no-deps"] 23 | 24 | # 25 | # WASM 26 | # 27 | 28 | [tasks.wasm] 29 | description = "Build wasm module (web target)" 30 | command = "wasm-pack" 31 | args = [ 32 | "build", 33 | "--out-dir=../js/pkg", 34 | "--release", 35 | "--target=web", 36 | "--no-default-features", 37 | "--features=proj4js-compat", 38 | ] 39 | 40 | 41 | [tasks.wasm_debug] 42 | description = "Build wasm module with logging feature (web target)" 43 | command = "wasm-pack" 44 | args = [ 45 | "build", 46 | "--out-dir=../js/pkg", 47 | "--dev", 48 | "--target=web", 49 | "--no-default-features", 50 | "--features=logging", 51 | "--features=proj4js-compat", 52 | "--features=with-wasm-entrypoint", 53 | ] 54 | 55 | [tasks.wasm_strict] 56 | description = "Build wasm module in strict-mode (web target)" 57 | command = "wasm-pack" 58 | args = [ 59 | "build", 60 | "--out-dir=../js/pkg", 61 | "--release", 62 | "--target=web", 63 | "--no-default-features", 64 | "--features=wasm-strict", 65 | "--features=proj4js-compat", 66 | ] 67 | 68 | 69 | [tasks.wasm_bundle] 70 | description = "Build wasm package as nodejs bundle" 71 | command = "wasm-pack" 72 | args = [ 73 | "build", 74 | "--out-dir=../js/pkg-bundle", 75 | "--release", 76 | "--target=bundler", 77 | "--no-default-features", 78 | "--features=proj4js-compat", 79 | ] 80 | 81 | [tasks.wasm_node] 82 | description = "Build wasm package as nodejs bundle" 83 | command = "wasm-pack" 84 | args = [ 85 | "build", 86 | "--out-dir=../js/pkg-node", 87 | "--release", 88 | "--target=nodejs", 89 | "--no-default-features", 90 | "--features=logging", 91 | "--features=proj4js-compat", 92 | "--features=with-wasm-entrypoint", 93 | ] 94 | 95 | -------------------------------------------------------------------------------- /proj4rs/src/math/mlfn.rs: -------------------------------------------------------------------------------- 1 | //! 2 | //! mlfn 3 | //! Meridional distance 4 | //! 5 | //! 6 | use crate::errors::{Error, Result}; 7 | 8 | // XXX Use clenshaw coefficients 9 | // with the third flattening ? 10 | // (cf Proj 9) 11 | 12 | /// Alias for mlfn coefficients 13 | pub(crate) type Enfn = (f64, f64, f64, f64, f64); 14 | 15 | /// Meridional distance for ellipsoid and inverse 16 | /// 8th degree - accurate to < 1e-5 meters when used in conjunction 17 | /// with typical major axis values. 18 | /// Inverse determines phi to EPS (1e-11) radians, about 1e-6 seconds. 19 | pub(crate) fn enfn(es: f64) -> Enfn { 20 | const C00: f64 = 1.; 21 | const C02: f64 = 0.25; 22 | const C04: f64 = 0.046875; 23 | const C06: f64 = 0.01953125; 24 | const C08: f64 = 0.01068115234375; 25 | const C22: f64 = 0.75; 26 | const C44: f64 = 0.46875; 27 | const C46: f64 = 0.013_020_833_333_333_334; 28 | const C48: f64 = 0.007_120_768_229_166_667; 29 | const C66: f64 = 0.364_583_333_333_333_3; 30 | const C68: f64 = 0.005_696_614_583_333_334; 31 | const C88: f64 = 0.3076171875; 32 | 33 | let t = es * es; 34 | ( 35 | C00 - es * (C02 + es * (C04 + es * (C06 + es * C08))), 36 | es * (C22 - es * (C04 + es * (C06 + es * C08))), 37 | t * (C44 - es * (C46 + es * C48)), 38 | t * es * (C66 - es * C68), 39 | t * es * es * C88, 40 | ) 41 | } 42 | 43 | pub(crate) fn mlfn(phi: f64, mut sphi: f64, mut cphi: f64, en: Enfn) -> f64 { 44 | cphi *= sphi; 45 | sphi *= sphi; 46 | en.0 * phi - cphi * (en.1 + sphi * (en.2 + sphi * (en.3 + sphi * en.4))) 47 | } 48 | 49 | pub(crate) fn inv_mlfn(arg: f64, es: f64, en: Enfn) -> Result { 50 | const MAX_ITER: usize = 10; 51 | const EPS: f64 = 1e-11; 52 | let k = 1. / (1. - es); 53 | let mut phi = arg; 54 | let mut i = MAX_ITER; 55 | // rarely goes over 2 iterations 56 | while i > 0 { 57 | let s = phi.sin(); 58 | let mut t = 1. - es * s * s; 59 | t = (mlfn(phi, s, phi.cos(), en) - arg) * (t * t.sqrt()) * k; 60 | phi -= t; 61 | if t.abs() < EPS { 62 | break; 63 | } 64 | i -= 1; 65 | } 66 | if i > 0 { 67 | Ok(phi) 68 | } else { 69 | Err(Error::InvMeridDistConvError) 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /js/ol-proj4rs-demo-app/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | OpenLayers + Proj4rs 6 | 7 | 8 | 9 |
OpenLayers + Proj4rs
10 |

Image Reprojection

11 | 12 | 19 | 26 | 33 | 40 | 47 | 48 | 49 | 50 | -------------------------------------------------------------------------------- /proj4rs/src/projections/eqc.rs: -------------------------------------------------------------------------------- 1 | //! 2 | //! From proj/eqc.cpp 3 | //! 4 | //! See also 5 | //! 6 | //! Simplest of all projections 7 | //! 8 | //! eqc: "Equidistant Cylindrical (Plate Carree)" 9 | //! 10 | 11 | use crate::ellps::Ellipsoid; 12 | use crate::errors::{Error, Result}; 13 | use crate::parameters::ParamList; 14 | use crate::proj::ProjData; 15 | 16 | // Projection stub 17 | super::projection! { eqc } 18 | 19 | #[derive(Debug, Clone)] 20 | pub(crate) struct Projection { 21 | rc: f64, 22 | phi0: f64, 23 | } 24 | 25 | impl Projection { 26 | pub fn eqc(p: &mut ProjData, params: &ParamList) -> Result { 27 | let rc = params.try_angular_value("lat_ts")?.unwrap_or(0.).cos(); 28 | if rc <= 0. { 29 | return Err(Error::InvalidParameterValue("lat_ts should be <= 90°")); 30 | } 31 | p.ellps = Ellipsoid::sphere(p.ellps.a)?; 32 | Ok(Self { rc, phi0: p.phi0 }) 33 | } 34 | 35 | #[inline(always)] 36 | pub fn forward(&self, lam: f64, phi: f64, z: f64) -> Result<(f64, f64, f64)> { 37 | Ok((lam * self.rc, phi - self.phi0, z)) 38 | } 39 | 40 | #[inline(always)] 41 | pub fn inverse(&self, x: f64, y: f64, z: f64) -> Result<(f64, f64, f64)> { 42 | Ok((x / self.rc, y + self.phi0, z)) 43 | } 44 | 45 | pub const fn has_inverse() -> bool { 46 | true 47 | } 48 | 49 | pub const fn has_forward() -> bool { 50 | true 51 | } 52 | } 53 | 54 | #[cfg(test)] 55 | mod tests { 56 | use crate::math::consts::EPS_10; 57 | use crate::proj::Proj; 58 | use crate::tests::utils::{test_proj_forward, test_proj_inverse}; 59 | 60 | #[test] 61 | fn proj_eqc_wgs84() { 62 | let p = Proj::from_proj_string("+proj=eqc +ellps=WGS84").unwrap(); 63 | 64 | println!("{:#?}", p.projection()); 65 | 66 | let inputs = [((2., 47., 0.), (222638.98158654713, 5232016.06728385761, 0.))]; 67 | 68 | test_proj_forward(&p, &inputs, EPS_10); 69 | test_proj_inverse(&p, &inputs, EPS_10); 70 | } 71 | 72 | #[test] 73 | fn proj_eqc_lat_ts() { 74 | let p = Proj::from_proj_string("+proj=eqc +lat_ts=30 +lon_0=-90").unwrap(); 75 | 76 | println!("{:#?}", p.projection()); 77 | 78 | let inputs = [( 79 | (-88., 30., 0.), 80 | (192811.01392664597, 3339584.72379820701, 0.), 81 | )]; 82 | 83 | test_proj_forward(&p, &inputs, EPS_10); 84 | test_proj_inverse(&p, &inputs, EPS_10); 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /proj4rs/src/nadgrids/files/mod.rs: -------------------------------------------------------------------------------- 1 | //! 2 | //! Read grid from files 3 | //! 4 | use std::env; 5 | use std::fs::File; 6 | use std::io::{BufReader, Read, Seek, SeekFrom}; 7 | use std::path::{Path, PathBuf}; 8 | 9 | use crate::errors::{Error, Result}; 10 | use crate::nadgrids::Catalog; 11 | 12 | mod ntv2; 13 | 14 | use super::header::Header; 15 | use ntv2::read_ntv2; 16 | 17 | /// Define a default file finder functions 18 | fn default_file_finder(name: &str) -> Result { 19 | let p = Path::new(name); 20 | match p.exists().then_some(p.into()).or_else(|| { 21 | if let Ok(val) = env::var("PROJ_NADGRIDS").or_else(|_| env::var("PROJ_DATA")) { 22 | val.split(':').find_map(|s| { 23 | let p = Path::new(s).join(name); 24 | p.exists().then_some(p) 25 | }) 26 | } else { 27 | None 28 | } 29 | }) { 30 | Some(p) => Ok(p), 31 | None => Err(Error::GridFileNotFound(name.into())), 32 | } 33 | } 34 | 35 | pub(crate) enum FileType { 36 | Ntv1, 37 | Ntv2, 38 | Gtx, 39 | Ctable, 40 | Ctable2, 41 | } 42 | 43 | /// Recognize grid file type 44 | pub(crate) fn recognize(key: &str, read: &mut R) -> Result { 45 | const BUFSIZE: usize = 160; 46 | let pos = read.stream_position()?; 47 | let mut header = Header::::new(); 48 | 49 | let rv = header.read_partial(read).map(|size| { 50 | if size >= 144 + 16 51 | && header.cmp_str(0, "HEADER") 52 | && header.cmp_str(96, "W_GRID") 53 | && header.cmp_str(144, "TO NAD83 ") 54 | { 55 | FileType::Ntv1 56 | } else if size >= 48 + 7 && header.cmp_str(0, "NUM_OREC") && header.cmp_str(48, "GS_TYPE") { 57 | FileType::Ntv2 58 | } else if key.ends_with("gtx") || key.ends_with("GTX") { 59 | FileType::Gtx 60 | } else if size >= 9 && header.cmp_str(0, "CTABLE V2") { 61 | FileType::Ctable2 62 | } else { 63 | // Ctable fallback 64 | FileType::Ctable 65 | } 66 | }); 67 | 68 | // Restore position 69 | read.seek(SeekFrom::Start(pos))?; 70 | rv 71 | } 72 | 73 | /// Grid builder that read from a file 74 | pub fn read_from_file(catalog: &Catalog, key: &str) -> Result<()> { 75 | // Use a BufReader for efficiency 76 | read( 77 | catalog, 78 | key, 79 | &mut BufReader::new(File::open(default_file_finder(key)?)?), 80 | ) 81 | } 82 | 83 | /// Read a grid from a file given by `key` 84 | pub fn read(catalog: &Catalog, key: &str, read: &mut R) -> Result<()> { 85 | // Guess the file 86 | match recognize(key, read)? { 87 | FileType::Ntv2 => read_ntv2(catalog, key, read), 88 | _ => Err(Error::UnknownGridFormat), 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /js/ol-proj4rs-demo-app/wms-image-custom-proj.js: -------------------------------------------------------------------------------- 1 | import ImageLayer from 'ol/layer/Image.js'; 2 | import ImageWMS from 'ol/source/ImageWMS.js'; 3 | import Map from 'ol/Map.js'; 4 | import Projection from 'ol/proj/Projection.js'; 5 | import View from 'ol/View.js'; 6 | import {proj4} from './assets/js/proj4.js'; 7 | import {ScaleLine, defaults as defaultControls} from 'ol/control.js'; 8 | import {fromLonLat} from 'ol/proj.js'; 9 | import {register} from 'ol/proj/proj4.js'; 10 | 11 | // Transparent Proj4js support: 12 | // 13 | // EPSG:21781 is known to Proj4js because its definition is registered by 14 | // calling proj4.defs(). Now when we create an ol/proj/Projection instance with 15 | // the 'EPSG:21781' code, OpenLayers will pick up the transform functions from 16 | // Proj4js. To get the registered ol/proj/Projection instance with other 17 | // parameters like units and axis orientation applied from Proj4js, use 18 | // `ol/proj#get('EPSG:21781')`. 19 | // 20 | // Note that we are setting the projection's extent here, which is used to 21 | // determine the view resolution for zoom level 0. Recommended values for a 22 | // projection's validity extent can be found at https://epsg.io/. 23 | 24 | proj4.defs( 25 | 'EPSG:21781', 26 | '+proj=somerc +lat_0=46.95240555555556 +lon_0=7.439583333333333 +k_0=1 ' + 27 | '+x_0=600000 +y_0=200000 +ellps=bessel ' + 28 | '+towgs84=660.077,13.551,369.344,2.484,1.783,2.939,5.66 +units=m +no_defs' 29 | ); 30 | register(proj4); 31 | 32 | const projection = new Projection({ 33 | code: 'EPSG:21781', 34 | extent: [485869.5728, 76443.1884, 837076.5648, 299941.7864], 35 | }); 36 | 37 | const extent = [420000, 30000, 900000, 350000]; 38 | const layers = [ 39 | new ImageLayer({ 40 | extent: extent, 41 | source: new ImageWMS({ 42 | url: 'https://wms.geo.admin.ch/', 43 | crossOrigin: 'anonymous', 44 | attributions: 45 | '© Pixelmap 1:1000000 / geo.admin.ch', 47 | params: { 48 | 'LAYERS': 'ch.swisstopo.pixelkarte-farbe-pk1000.noscale', 49 | 'FORMAT': 'image/jpeg', 50 | }, 51 | serverType: 'mapserver', 52 | }), 53 | }), 54 | new ImageLayer({ 55 | extent: extent, 56 | source: new ImageWMS({ 57 | url: 'https://wms.geo.admin.ch/', 58 | crossOrigin: 'anonymous', 59 | attributions: 60 | '© Flood Alert / geo.admin.ch', 62 | params: {'LAYERS': 'ch.bafu.hydroweb-warnkarte_national'}, 63 | serverType: 'mapserver', 64 | }), 65 | }), 66 | ]; 67 | 68 | const map = new Map({ 69 | controls: defaultControls().extend([new ScaleLine()]), 70 | layers: layers, 71 | target: 'map', 72 | view: new View({ 73 | projection: projection, 74 | center: fromLonLat([8.23, 46.86], projection), 75 | extent: extent, 76 | zoom: 2, 77 | }), 78 | }); 79 | -------------------------------------------------------------------------------- /proj4rs/src/units.rs: -------------------------------------------------------------------------------- 1 | //! 2 | //! Predefined units for conversion 3 | //! 4 | 5 | #[derive(Debug, Copy, Clone)] 6 | pub struct UnitDefn { 7 | pub name: &'static str, 8 | pub to_meter: f64, 9 | } 10 | 11 | macro_rules! unit { 12 | ($name:expr, $display:expr, $comment:expr, $to_meter:expr) => { 13 | UnitDefn { 14 | name: $name, 15 | to_meter: $to_meter, 16 | } 17 | }; 18 | } 19 | 20 | pub const METER: UnitDefn = unit!("m", "1", "Meter", 1.0); 21 | 22 | pub const DEGREES: &str = "degrees"; 23 | 24 | mod constants { 25 | use super::*; 26 | /// Static units table 27 | /// id, to_meter, display to_meter value, comment, to_meter 28 | #[rustfmt::skip] 29 | pub const UNITS: [UnitDefn;21] = [ 30 | unit!("km", "1000", "Kilometer", 1000.0), 31 | unit!("m", "1", "Meter", 1.0), 32 | unit!("dm", "1/10", "Decimeter", 0.1), 33 | unit!("cm", "1/100", "Centimeter", 0.01), 34 | unit!("mm", "1/1000", "Millimeter", 0.001), 35 | unit!("kmi", "1852", "International Nautical Mile", 1852.0), 36 | unit!("in", "0.0254", "International Inch", 0.0254), 37 | unit!("ft", "0.3048", "International Foot", 0.3048), 38 | unit!("yd", "0.9144", "International Yard", 0.9144), 39 | unit!("mi", "1609.344", "International Statute Mile", 1609.344), 40 | unit!("fath", "1.8288", "International Fathom", 1.8288), 41 | unit!("ch", "20.1168", "International Chain", 20.1168), 42 | unit!("link", "0.201168", "International Link", 0.201168), 43 | unit!("us-in", "1/39.37", "U.S. Surveyor's Inch", 100./3937.0), 44 | unit!("us-ft", "0.304800609601219", "U.S. Surveyor's Foot", 1200./3937.0), 45 | unit!("us-yd", "0.914401828803658", "U.S. Surveyor's Yard", 3600./3937.0), 46 | unit!("us-ch", "20.11684023368047", "U.S. Surveyor's Chain", 79200./3937.0), 47 | unit!("us-mi", "1609.347218694437", "U.S. Surveyor's Statute Mile", 6336000./3937.0), 48 | unit!("ind-yd", "0.91439523", "Indian Yard", 0.91439523), 49 | unit!("ind-ft", "0.30479841", "Indian Foot", 0.30479841), 50 | unit!("ind-ch", "20.11669506", "Indian Chain", 20.11669506), 51 | ]; 52 | } 53 | 54 | pub fn from_value(to_meter: f64) -> UnitDefn { 55 | UnitDefn { name: "", to_meter } 56 | } 57 | 58 | /// Return the unit definition 59 | pub fn find_units(name: &str) -> Option { 60 | constants::UNITS 61 | .iter() 62 | .find(|d| d.name.eq_ignore_ascii_case(name)) 63 | .copied() 64 | } 65 | -------------------------------------------------------------------------------- /proj4rs/src/datum_params.rs: -------------------------------------------------------------------------------- 1 | //! 2 | //! Handle datum parameters 3 | //! 4 | use crate::datums::DatumParamDefn; 5 | use crate::errors::{Error, Result}; 6 | use crate::math::consts::SEC_TO_RAD; 7 | use crate::nadgrids::NadGrids; 8 | use crate::parse::FromStr; 9 | 10 | /// Datum parameters 11 | #[derive(Default, Clone, Debug, PartialEq)] 12 | pub(crate) enum DatumParams { 13 | ToWGS84_0, 14 | ToWGS84_3(f64, f64, f64), 15 | ToWGS84_7(f64, f64, f64, f64, f64, f64, f64), 16 | NadGrids(NadGrids), 17 | #[default] 18 | NoDatum, 19 | } 20 | 21 | impl DatumParams { 22 | /// Create parameters from a 'towgs84 like string' 23 | /// Values are expected to be in second of arcs 24 | pub fn from_towgs84_str(towgs84: &str) -> Result { 25 | let mut i = towgs84.split(','); 26 | 27 | // XXX Use js_sys::parsefloat with Wasm 28 | // It save about 20ko ! 29 | fn parse(v: Option<&str>) -> Result { 30 | f64::from_str(v.unwrap_or("").trim()).map_err(|_| Error::InvalidToWGS84String) 31 | } 32 | 33 | match towgs84.split(',').count() { 34 | 3 => Ok(DatumParams::ToWGS84_3( 35 | parse(i.next())?, 36 | parse(i.next())?, 37 | parse(i.next())?, 38 | )), 39 | 7 => Ok(DatumParams::ToWGS84_7( 40 | parse(i.next())?, 41 | parse(i.next())?, 42 | parse(i.next())?, 43 | parse(i.next())? * SEC_TO_RAD, 44 | parse(i.next())? * SEC_TO_RAD, 45 | parse(i.next())? * SEC_TO_RAD, 46 | parse(i.next())? / 1_000_000.0 + 1., 47 | )), 48 | _ => Err(Error::InvalidToWGS84String), 49 | } 50 | } 51 | 52 | pub fn from_nadgrid_str(nadgrids: &str) -> Result { 53 | if nadgrids == "@null" || nadgrids == "null" { 54 | // See https://proj.org/en/stable/usage/transformation.html#the-null-grid 55 | // for discussion about null nadgrid 56 | Ok(Self::NoDatum) 57 | } else { 58 | NadGrids::new_grid_transform(nadgrids).map(Self::NadGrids) 59 | } 60 | } 61 | 62 | pub fn use_nadgrids(&self) -> bool { 63 | matches!(self, Self::NadGrids(_)) 64 | } 65 | 66 | pub fn no_datum(&self) -> bool { 67 | matches!(self, Self::NoDatum) 68 | } 69 | } 70 | 71 | // Convert from datum parameters definition 72 | impl TryFrom<&DatumParamDefn> for DatumParams { 73 | type Error = Error; 74 | 75 | fn try_from(defn: &DatumParamDefn) -> Result { 76 | match defn { 77 | DatumParamDefn::ToWGS84_0 => Ok(Self::ToWGS84_0), 78 | DatumParamDefn::ToWGS84_3(dx, dy, dz) => Ok(Self::ToWGS84_3(*dx, *dy, *dz)), 79 | DatumParamDefn::ToWGS84_7(dx, dy, dz, rx, ry, rz, s) => Ok(Self::ToWGS84_7( 80 | *dx, 81 | *dy, 82 | *dz, 83 | *rx * SEC_TO_RAD, 84 | *ry * SEC_TO_RAD, 85 | *rz * SEC_TO_RAD, 86 | *s / 1_000_000.0 + 1., 87 | )), 88 | DatumParamDefn::NadGrids(s) => Self::from_nadgrid_str(s), 89 | } 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /proj4rs/examples/rsproj.rs: -------------------------------------------------------------------------------- 1 | //! 2 | //! Compute forward and inverse projections 3 | //! 4 | use clap::{ArgAction, Parser}; 5 | use proj4rs::{ 6 | errors::{Error, Result}, 7 | proj, transform, 8 | }; 9 | 10 | use std::io::{self, BufRead}; 11 | 12 | #[derive(Parser)] 13 | #[command(author, version="0.1", about="Compute projections", long_about = None)] 14 | #[command(propagate_version = true)] 15 | struct Cli { 16 | /// Destination projection 17 | #[arg(long, required = true)] 18 | to: String, 19 | /// Source projection 20 | #[arg(long, default_value = "+proj=latlong")] 21 | from: String, 22 | /// Perform inverse projection 23 | #[arg(short, long)] 24 | inverse: bool, 25 | /// Increase verbosity 26 | #[arg(short, long, action = ArgAction::Count)] 27 | verbose: u8, 28 | } 29 | 30 | fn main() -> Result<()> { 31 | let args = Cli::parse(); 32 | 33 | init_logger(args.verbose); 34 | 35 | log::debug!( 36 | "\nfrom: {}\nto: {}\ninverse: {}", 37 | args.from, 38 | args.to, 39 | args.inverse 40 | ); 41 | 42 | let (srcdef, dstdef): (&str, &str) = if args.inverse { 43 | (&args.to, &args.from) 44 | } else { 45 | (&args.from, &args.to) 46 | }; 47 | 48 | let src = proj::Proj::from_user_string(srcdef)?; 49 | let dst = proj::Proj::from_user_string(dstdef)?; 50 | 51 | let stdin = io::stdin().lock(); 52 | 53 | fn from_parse_err(err: std::num::ParseFloatError) -> Error { 54 | Error::ParameterValueError(format!("{err:?}")) 55 | } 56 | 57 | for line in stdin.lines() { 58 | let line = line.unwrap(); 59 | let inputs = line.as_str().split_whitespace().collect::>(); 60 | if inputs.len() < 2 || inputs.len() > 3 { 61 | eprintln!("Expecting: ' , [,]' found: {}", line.as_str()); 62 | std::process::exit(1); 63 | } 64 | 65 | let x: f64 = inputs[0].parse().map_err(from_parse_err)?; 66 | let y: f64 = inputs[1].parse().map_err(from_parse_err)?; 67 | let z: f64 = if inputs.len() > 2 { 68 | inputs[2].parse().map_err(from_parse_err)? 69 | } else { 70 | 0. 71 | }; 72 | 73 | let mut point = (x, y, z); 74 | 75 | if src.is_latlong() { 76 | point.0 = point.0.to_radians(); 77 | point.1 = point.1.to_radians(); 78 | } 79 | transform::transform(&src, &dst, &mut point)?; 80 | if dst.is_latlong() { 81 | point.0 = point.0.to_degrees(); 82 | point.1 = point.1.to_degrees(); 83 | } 84 | 85 | println!("{} {}", point.0, point.1); 86 | } 87 | Ok(()) 88 | } 89 | 90 | // 91 | // Logger 92 | // 93 | fn init_logger(verbose: u8) { 94 | use env_logger::Env; 95 | use log::LevelFilter; 96 | 97 | let mut builder = env_logger::Builder::from_env(Env::default().default_filter_or("info")); 98 | 99 | match verbose { 100 | 1 => builder.filter_level(LevelFilter::Debug), 101 | _ if verbose > 1 => builder.filter_level(LevelFilter::Trace), 102 | _ => &mut builder, 103 | } 104 | .init(); 105 | } 106 | -------------------------------------------------------------------------------- /proj4rs/src/errors.rs: -------------------------------------------------------------------------------- 1 | //! 2 | //! Crate errors 3 | //! 4 | 5 | #[derive(thiserror::Error, Debug)] 6 | pub enum Error { 7 | #[error("{0}")] 8 | InputStringError(&'static str), 9 | #[error("Missing value for parameter {0}")] 10 | NoValueParameter(String), 11 | #[error("Cannot retrieve value for parameter")] 12 | ParameterValueError(String), 13 | #[error("Missing projection name")] 14 | MissingProjectionError, 15 | #[error("Unrecognized datum")] 16 | InvalidDatum, 17 | #[error("Unrecognized ellipsoid")] 18 | InvalidEllipsoid, 19 | #[error("{0}")] 20 | InvalidParameterValue(&'static str), 21 | #[error("Invalid coordinate dimension")] 22 | InvalidCoordinateDimension, 23 | #[error("Latitude out of range")] 24 | LatitudeOutOfRange, 25 | #[error("NAD grid not available")] 26 | NadGridNotAvailable, 27 | #[error("Parent grid not found")] 28 | NadGridParentNotFound, 29 | #[error("Inverse grid shift failed to converge.")] 30 | InverseGridShiftConvError, 31 | #[error("Point outside of NAD outside Shift area")] 32 | PointOutsideNadShiftArea, 33 | #[error("Invalid 'towgs84' string")] 34 | InvalidToWGS84String, 35 | #[error("Invalid axis")] 36 | InvalidAxis, 37 | #[error("Unrecognized format")] 38 | UnrecognizedFormat, 39 | #[error("Latitude or longitude over range")] 40 | LatOrLongExceedLimit, 41 | #[error("Nan value for coordinate")] 42 | NanCoordinateValue, 43 | #[error("Coordinate out of range")] 44 | CoordinateOutOfRange, 45 | #[error("Invalid number of coordinates")] 46 | InvalidNumberOfCoordinates, 47 | #[error("Projection not found")] 48 | ProjectionNotFound, 49 | #[error("No forward projection defined for dest projection")] 50 | NoForwardProjectionDefined, 51 | #[error("No inverse projection defined for src projection")] 52 | NoInverseProjectionDefined, 53 | #[error("ProjErrConicLatEqual")] 54 | ProjErrConicLatEqual, 55 | #[error("Tolerance condition not satisfied")] 56 | ToleranceConditionError, 57 | #[error("Non convergence of phi2 calculation")] 58 | NonInvPhi2Convergence, 59 | #[error("Failed to compute forward projection")] 60 | ForwardProjectionFailure, 61 | #[error("Failed to compute inverse projection")] 62 | InverseProjectionFailure, 63 | #[error("Invalid UTM zone")] 64 | InvalidUtmZone, 65 | #[error("An ellipsoid is required")] 66 | EllipsoidRequired, 67 | #[error("Coordinate transform outside projection domain")] 68 | CoordTransOutsideProjectionDomain, 69 | #[error("No convergence for inv. meridian distance")] 70 | InvMeridDistConvError, 71 | #[error("JS parse error")] 72 | JsParseError, 73 | #[error("Invalid Ntv2 grid format: {0}")] 74 | InvalidNtv2GridFormat(&'static str), 75 | #[error("IO error")] 76 | IoError(#[from] std::io::Error), 77 | #[error("UTF8 error")] 78 | Utf8Error(#[from] std::str::Utf8Error), 79 | #[error("Grid file not found {0}")] 80 | GridFileNotFound(String), 81 | #[error("Unknown grid format")] 82 | UnknownGridFormat, 83 | #[error("Numerical argument too large")] 84 | ArgumentTooLarge, 85 | } 86 | 87 | pub type Result = std::result::Result; 88 | -------------------------------------------------------------------------------- /proj4rs/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | 7 | 8 | ## Unreleased 9 | 10 | ### Changed 11 | 12 | * Remove dependency to lazy-static 13 | - https://github.com/3liz/proj4rs/pull/41 14 | 15 | ## 0.1.9 - 2025-09-16 16 | 17 | ### Fixed 18 | 19 | * Fix Fix tolerance in inverse grid shift iteration 20 | - https://github.com/3liz/proj4rs/issues/37 21 | 22 | ### Changed 23 | 24 | * Simplify away some unsafe code 25 | - https://github.com/3liz/proj4rs/pull/38 26 | 27 | ## 0.1.8 - 2025-06-18 28 | 29 | ### Fixed 30 | 31 | * Fix NTv2 grid interpolation 32 | - https://github.com/3liz/proj4rs/issues/32 33 | * Fix Support for wasm-wasip2 target 34 | - https://github.com/3liz/proj4rs/issues/30 35 | * Fix inverse geos transform and apply more efficient 36 | computation: https://github.com/OSGeo/PROJ/pull/4523 37 | 38 | ### Added 39 | 40 | * DMS notation support for angular proj string values 41 | * Added optional projections: 42 | * aeqd 43 | * krovak 44 | * mill 45 | * cea 46 | 47 | ## 0.1.7 - 2024-06-10 48 | 49 | ### Fixed 50 | 51 | * Fix axis normalisation 52 | 53 | ## 0.1.6 - 2024-06-10 54 | 55 | ### Fixed 56 | 57 | * Fix `+nadgrids=@null` as no-op on datum transformation 58 | 59 | ### Changed 60 | 61 | * Allow 3d inputs in examples/proj4rs 62 | 63 | ### Added 64 | 65 | * Added 'eqc' projection 66 | * Added 'geos' projection 67 | - Partially from work from https://github.com/3liz/proj4rs/pull/20 68 | 69 | ## 0.1.5 - 2024-10-03 70 | 71 | ### Fixed 72 | 73 | * Fix wrong calculation in laea projection 74 | - https://github.com/3liz/proj4rs/issues/18 75 | 76 | ## 0.1.4 - 2024-09-16 77 | 78 | ### Changed 79 | 80 | * Remove wee\_alloc as it's unmaintained 81 | - https://github.com/3liz/proj4rs/pull/16 82 | 83 | ## 0.1.3 - 2024-05-18 84 | 85 | ### Fixed 86 | 87 | * fix UB on NodePtr::get 88 | - https://github.com/3liz/proj4rs/pull/13 89 | 90 | ### Changed 91 | 92 | * Update Vite config to build WASM demo 93 | 94 | ## 0.1.2 - 2023-19-11 95 | 96 | ### Fixed 97 | 98 | * Fix `geo-type` feature as optional 99 | - https://github.com/3liz/proj4rs/pull/11 100 | * Improve documentation 101 | * Fix `Transform` trait signature for WASM 102 | - https://github.com/3liz/proj4rs/issues/9 103 | 104 | ### Added 105 | 106 | * Add ability to create Projs from EPSG codes 107 | - https://github.com/3liz/proj4rs/pull/7 108 | * `Transform` implementations. 109 | - https://github.com/3liz/proj4rs/pull/6 110 | - Implement for a 2-tuple. 111 | - Implement for the `geo-types` geometries, them being placed behind a `geo-types` feature flag. 112 | 113 | ### Changed 114 | 115 | * `Transform` trait signature. 116 | - https://github.com/3liz/proj4rs/pull/6 117 | - Alias `FnMut(f64, f64, f64) -> Result<(f64, f64, f64)>` behind a `TransformClosure` 118 | - `transform_coordinates()` takes a mutable reference to `f`, making it easier to layer `Transform` implementations. 119 | 120 | ## 0.1.1 - 2023-09-07 121 | 122 | ### Added 123 | 124 | * Implement `Clone` on `Proj` type. 125 | - https://github.com/3liz/proj4rs/pull/2 126 | * Added exemple in README 127 | - https://github.com/3liz/proj4rs/pull/3 128 | -------------------------------------------------------------------------------- /proj4rs/src/projections/sterea.rs: -------------------------------------------------------------------------------- 1 | //! 2 | //! Oblique Stereographic Alternative 3 | //! 4 | //! from PJ_sterea.c (proj 5.2.0) 5 | //! 6 | //! sterea: "Oblique Stereographic Alternative" "\n\tAzimuthal, Sph&Ell" 7 | //! 8 | use crate::errors::Result; 9 | use crate::math::{gauss, gauss_ini, inv_gauss, Gauss}; 10 | use crate::parameters::ParamList; 11 | use crate::proj::ProjData; 12 | 13 | // Projection stub 14 | super::projection! { sterea } 15 | 16 | #[derive(Debug, Clone)] 17 | pub(crate) struct Projection { 18 | k0: f64, 19 | phic0: f64, 20 | cosc0: f64, 21 | sinc0: f64, 22 | r2: f64, 23 | en: Gauss, 24 | } 25 | 26 | impl Projection { 27 | pub fn sterea(p: &mut ProjData, _: &ParamList) -> Result { 28 | let (en, phic0, r) = gauss_ini(p.ellps.e, p.phi0)?; 29 | let (sinc0, cosc0) = phic0.sin_cos(); 30 | Ok(Self { 31 | k0: p.k0, 32 | phic0, 33 | cosc0, 34 | sinc0, 35 | r2: 2. * r, 36 | en, 37 | }) 38 | } 39 | 40 | #[inline(always)] 41 | pub fn forward(&self, lam: f64, phi: f64, z: f64) -> Result<(f64, f64, f64)> { 42 | let (lam, phi) = gauss(lam, phi, &self.en); 43 | let (sinc, cosc) = phi.sin_cos(); 44 | let cosl = lam.cos(); 45 | let k = self.k0 * self.r2 / (1. + self.sinc0 * sinc + self.cosc0 * cosc * cosl); 46 | Ok(( 47 | k * cosc * lam.sin(), 48 | k * (self.cosc0 * sinc - self.sinc0 * cosc * cosl), 49 | z, 50 | )) 51 | } 52 | 53 | #[inline(always)] 54 | pub fn inverse(&self, x: f64, y: f64, z: f64) -> Result<(f64, f64, f64)> { 55 | let x = x / self.k0; 56 | let y = y / self.k0; 57 | let rho = x.hypot(y); 58 | let (lam, phi) = if rho != 0.0 { 59 | let c = 2. * rho.atan2(self.r2); 60 | let (sinc, cosc) = c.sin_cos(); 61 | inv_gauss( 62 | (x * sinc).atan2(rho * self.cosc0 * cosc - y * self.sinc0 * sinc), 63 | (cosc * self.sinc0 + y * sinc * self.cosc0 / rho).asin(), 64 | &self.en, 65 | ) 66 | } else { 67 | inv_gauss(0., self.phic0, &self.en) 68 | }?; 69 | Ok((lam, phi, z)) 70 | } 71 | 72 | pub const fn has_inverse() -> bool { 73 | true 74 | } 75 | 76 | pub const fn has_forward() -> bool { 77 | true 78 | } 79 | } 80 | 81 | #[cfg(test)] 82 | mod tests { 83 | use crate::math::consts::EPS_10; 84 | use crate::proj::Proj; 85 | use crate::tests::utils::{test_proj_forward, test_proj_inverse}; 86 | 87 | #[test] 88 | fn proj_sterea() { 89 | let p = Proj::from_proj_string("+proj=sterea +ellps=GRS80").unwrap(); 90 | 91 | println!("{:#?}", p.data()); 92 | println!("{:#?}", p.projection()); 93 | 94 | let inputs = [ 95 | ((2., 1., 0.), (222644.89410919772, 110611.09187173686, 0.)), 96 | ((2., -1., 0.), (222644.89410919772, -110611.09187173827, 0.)), 97 | ((-2., 1., 0.), (-222644.89410919772, 110611.09187173686, 0.)), 98 | ( 99 | (-2., -1., 0.), 100 | (-222644.89410919772, -110611.09187173827, 0.), 101 | ), 102 | ]; 103 | 104 | test_proj_forward(&p, &inputs, EPS_10); 105 | test_proj_inverse(&p, &inputs, EPS_10); 106 | } 107 | } 108 | -------------------------------------------------------------------------------- /proj4rs-clib/python/tests/test_all.py: -------------------------------------------------------------------------------- 1 | import proj4rs 2 | import pytest 3 | 4 | 5 | def test_transform_sequence(): 6 | 7 | src = proj4rs.Proj("WGS84") 8 | dst = proj4rs.Proj("+proj=laea +lat_0=52 +lon_0=10 +x_0=4321000 +y_0=3210000 +ellps=GRS80") 9 | 10 | print("Src:", src.projname) 11 | print("Dst:", dst.projname) 12 | 13 | x = [15.4213696] 14 | y = [47.0766716] 15 | 16 | trans = proj4rs.Transform(src, dst) 17 | 18 | x, y = trans.transform(x, y) 19 | 20 | print("x =", x) # Should be 4732659.007426 21 | print("y =", y) # Should be 2677630.726961 22 | 23 | assert x[0] == pytest.approx(4732659.007426266, 1e-6) 24 | assert y[0] == pytest.approx(2677630.7269610995, 1e-6) 25 | 26 | 27 | 28 | def test_transform_scalar(): 29 | 30 | src = proj4rs.Proj("WGS84") 31 | dst = proj4rs.Proj("+proj=laea +lat_0=52 +lon_0=10 +x_0=4321000 +y_0=3210000 +ellps=GRS80") 32 | 33 | print("Src:", src.projname) 34 | print("Dst:", dst.projname) 35 | 36 | x = 15.4213696 37 | y = 47.0766716 38 | 39 | print("Transform") 40 | trans = proj4rs.Transform(src, dst) 41 | 42 | x, y = trans.transform(x, y) 43 | 44 | print("x =", x) # Should be 4732659.007426 45 | print("y =", y) # Should be 2677630.726961 46 | 47 | assert x == pytest.approx(4732659.007426266, 1e-6) 48 | assert y == pytest.approx(2677630.7269610995, 1e-6) 49 | 50 | 51 | def test_transform_buffer_inplace(): 52 | from array import array 53 | 54 | src = proj4rs.Proj("WGS84") 55 | dst = proj4rs.Proj("+proj=laea +lat_0=52 +lon_0=10 +x_0=4321000 +y_0=3210000 +ellps=GRS80") 56 | 57 | print("Src:", src.projname) 58 | print("Dst:", dst.projname) 59 | 60 | x = array('d', [15.4213696]) 61 | y = array('d', [47.0766716]) 62 | 63 | trans = proj4rs.Transform(src, dst) 64 | 65 | xx, yy = trans.transform(x, y, inplace=True) 66 | 67 | print("x =", x) # Should be 4732659.007426 68 | print("y =", y) # Should be 2677630.726961 69 | 70 | assert xx is x 71 | assert yy is y 72 | 73 | assert xx[0] == pytest.approx(4732659.007426266, 1e-6) 74 | assert yy[0] == pytest.approx(2677630.7269610995, 1e-6) 75 | 76 | 77 | def test_transform_invalid_buffer(): 78 | from array import array 79 | 80 | src = proj4rs.Proj("WGS84") 81 | dst = proj4rs.Proj("+proj=laea +lat_0=52 +lon_0=10 +x_0=4321000 +y_0=3210000 +ellps=GRS80") 82 | 83 | print("Src:", src.projname) 84 | print("Dst:", dst.projname) 85 | 86 | x = array('d', [15.4213696]) 87 | 88 | trans = proj4rs.Transform(src, dst) 89 | 90 | with pytest.raises(ValueError, match="Expecting two-dimensional buffer"): 91 | trans.transform(x, inplace=True) 92 | 93 | 94 | def test_transform_buffer_2d(): 95 | from array import array 96 | 97 | x = array('d', [15.4213696, 47.0766716]) 98 | 99 | # Reshape to a two dimensionnal array 100 | m = memoryview(x).cast('b').cast('d', shape=(1,2)) 101 | print("* shape =", m.shape, "ndim", m.ndim) 102 | 103 | transform = proj4rs.Transform( 104 | "WGS84", 105 | "+proj=laea +lat_0=52 +lon_0=10 +x_0=4321000 +y_0=3210000 +ellps=GRS80", 106 | ).transform 107 | 108 | xx, yy = transform(m, inplace=True) 109 | 110 | print("xx =", list(xx)) 111 | print("yy =", list(yy)) 112 | 113 | assert x[0] == pytest.approx(4732659.007426266, 1e-6) 114 | assert x[1] == pytest.approx(2677630.7269610995, 1e-6) 115 | 116 | 117 | 118 | -------------------------------------------------------------------------------- /proj4rs/src/nadgrids/header.rs: -------------------------------------------------------------------------------- 1 | //! 2 | //! Nadgrid parser 3 | //! 4 | use crate::errors::{Error, Result}; 5 | use crate::nadgrids::grid::GridId; 6 | use std::io::Read; 7 | 8 | #[derive(Copy, Clone)] 9 | pub(crate) enum Endianness { 10 | Be = 0, 11 | Le = 1, 12 | } 13 | 14 | #[cfg(target_endian = "big")] 15 | impl Endianness { 16 | pub fn native() -> Self { 17 | Endianness::Be 18 | } 19 | pub fn other() -> Self { 20 | Endianness::Le 21 | } 22 | } 23 | #[cfg(target_endian = "little")] 24 | impl Endianness { 25 | pub fn native() -> Self { 26 | Endianness::Le 27 | } 28 | pub fn other() -> Self { 29 | Endianness::Be 30 | } 31 | } 32 | 33 | /// Generic header struct 34 | pub(crate) struct Header { 35 | buf: [u8; N], 36 | pub endian: Endianness, 37 | } 38 | 39 | impl Header { 40 | pub fn new() -> Self { 41 | Self::new_endian(Endianness::native()) 42 | } 43 | 44 | pub fn new_endian(endian: Endianness) -> Self { 45 | Self { 46 | buf: [0u8; N], 47 | endian, 48 | } 49 | } 50 | 51 | pub fn rebind(&self) -> Header { 52 | Header::::new_endian(self.endian) 53 | } 54 | 55 | #[inline] 56 | pub fn read(&mut self, read: &mut R) -> Result<&Self> { 57 | read.read_exact(&mut self.buf)?; 58 | Ok(self) 59 | } 60 | 61 | #[inline] 62 | pub fn read_partial(&mut self, read: &mut R) -> Result { 63 | read.read(&mut self.buf).map_err(Error::from) 64 | } 65 | 66 | pub fn get_f64(&self, offset: usize) -> f64 { 67 | match self.endian { 68 | Endianness::Be => f64::from_be_bytes(self.buf[offset..offset + 8].try_into().unwrap()), 69 | Endianness::Le => f64::from_le_bytes(self.buf[offset..offset + 8].try_into().unwrap()), 70 | } 71 | } 72 | 73 | pub fn get_f32(&self, offset: usize) -> f32 { 74 | match self.endian { 75 | Endianness::Be => f32::from_be_bytes(self.buf[offset..offset + 4].try_into().unwrap()), 76 | Endianness::Le => f32::from_le_bytes(self.buf[offset..offset + 4].try_into().unwrap()), 77 | } 78 | } 79 | 80 | pub fn get_u32(&self, offset: usize) -> u32 { 81 | match self.endian { 82 | Endianness::Be => u32::from_be_bytes(self.buf[offset..offset + 4].try_into().unwrap()), 83 | Endianness::Le => u32::from_le_bytes(self.buf[offset..offset + 4].try_into().unwrap()), 84 | } 85 | } 86 | 87 | #[inline] 88 | pub fn get_str(&self, offset: usize, len: usize) -> Result<&str> { 89 | std::str::from_utf8(&self.buf[offset..offset + len]).map_err(Error::from) 90 | } 91 | 92 | #[inline] 93 | pub fn get_u8(&self, offset: usize) -> u8 { 94 | self.buf[offset] 95 | } 96 | 97 | pub fn get_id(&self, offset: usize) -> GridId { 98 | let mut buf = [0u8; 8]; 99 | buf.copy_from_slice(&self.buf[offset..offset + 8]); 100 | buf.into() 101 | } 102 | 103 | pub fn cmp_str(&self, offset: usize, s: &str) -> bool { 104 | self.get_str(offset, s.len()) 105 | .map(|x| x == s) 106 | .unwrap_or(false) 107 | } 108 | } 109 | 110 | pub mod error_str { 111 | pub const ERR_INVALID_HEADER: &str = "Invalid header"; 112 | pub const ERR_GSCOUNT_NOT_MATCHING: &str = "GS COUNT not matching"; 113 | } 114 | -------------------------------------------------------------------------------- /proj4rs/src/math/mod.rs: -------------------------------------------------------------------------------- 1 | //! 2 | //! Utilities 3 | //! 4 | //! 5 | pub(crate) mod consts { 6 | //! 7 | //! Define constants 8 | //! 9 | 10 | // Note that TAU is 2*PI 11 | // see https://doc.rust-lang.org/std/f64/consts/constant.TAU.html 12 | pub(crate) use std::f64::consts::{FRAC_PI_2, FRAC_PI_4, PI, TAU}; 13 | 14 | // Was defined in proj4js for preventing divergence 15 | // of Mollweied algorithm 16 | pub(crate) const EPS_10: f64 = 1.0e-10; 17 | 18 | // Other value op epsilon used 19 | pub(crate) const EPS_12: f64 = 1.0e-12; 20 | 21 | // Other value op epsilon used 22 | pub(crate) const EPS_7: f64 = 1.0e-7; 23 | 24 | pub(crate) const SEC_TO_RAD: f64 = (PI / 180.0) / 3600.0; 25 | } 26 | 27 | // Redefinition of mathematical functions 28 | // 29 | // Some of these functions has been redefined for various reason. 30 | // It would be nice to investigate if some of them are still relevant 31 | // 32 | // Note that proj redefine ln1p (i.e ln(1+x)), while rust rely on platform native (libm) 33 | // implementation: 34 | // 35 | // ```C 36 | // static double log1py(double x) { /* Compute log(1+x) accurately */ 37 | // volatile double 38 | // y = 1 + x, 39 | // z = y - 1; 40 | // /* Here's the explanation for this magic: y = 1 + z, exactly, and z 41 | // * approx x, thus log(y)/z (which is nearly constant near z = 0) returns 42 | // * a good approximation to the true log(1 + x)/x. The multiplication x * 43 | // * (log(y)/z) introduces little additional error. */ 44 | // return z == 0 ? x : x * log(y) / z; 45 | // ``` 46 | // 47 | // For now we are going to stick to the native implementation of `ln_1p`, let's see if that 48 | // may cause problems in the future 49 | // 50 | // 51 | // The same for hypot, for now we are going to stick to the native implementation. 52 | // since latest version of glibc seems to handle case of potential overflow. 53 | 54 | // ---------- 55 | // asinh 56 | // --------- 57 | // 58 | // In the case of [`asinh`], rust define this as (https://doc.rust-lang.org/src/std/f64.rs.html#882-884) 59 | // 60 | // ```rust 61 | // pub fn asinh(self) -> f64 { 62 | // (self.abs() + ((self * self) + 1.0).sqrt()).ln().copysign(self) 63 | // } 64 | // ``` 65 | // 66 | // Note that proj use the following formula: 67 | // ```rust 68 | // #[inline] 69 | // pub fn asinh(x: f64) -> f64 { 70 | // let y = x.abs(); // Enforce odd parity 71 | // (y * (1. + y/(hypot(1.0,y) + 1.))).ln_1p().copysign(x) 72 | // } 73 | // ``` 74 | 75 | // The formula below is mathematically equivalent, but the rust version use 76 | // a naive implementation of `hypot` 77 | // wich may (eventually) leads to overflow. 78 | // 79 | // We prefer to use our own implementation using [`hypot`] with the simpler 80 | // rust formula. This implementation will give accurate result for `0.89e308f64` while the 81 | // `[f64::asinh`] implementation overflow (return `f64::INFINITE`) 82 | #[inline] 83 | pub fn asinh(x: f64) -> f64 { 84 | (x.abs() + 1.0f64.hypot(x)).ln().copysign(x) 85 | } 86 | 87 | //pub fn asinh(x: f64) -> f64 { 88 | // let y = x.abs(); // Enforce odd parity 89 | // (y * (1. + y/(1.0f64.hypot(y) + 1.))).ln_1p().copysign(x) 90 | //} 91 | 92 | mod aasincos; 93 | mod adjlon; 94 | mod auth; 95 | mod gauss; 96 | mod mlfn; 97 | mod msfn; 98 | mod phi2; 99 | mod qsfn; 100 | mod tsfn; 101 | 102 | pub(crate) use aasincos::aasin; 103 | pub(crate) use adjlon::adjlon; 104 | pub(crate) use auth::{authlat, authset}; 105 | pub(crate) use gauss::{gauss, gauss_ini, inv_gauss, Gauss}; 106 | pub(crate) use mlfn::{enfn, inv_mlfn, mlfn, Enfn}; 107 | pub(crate) use msfn::msfn; 108 | pub(crate) use phi2::phi2; 109 | pub(crate) use qsfn::qsfn; 110 | pub(crate) use tsfn::tsfn; 111 | -------------------------------------------------------------------------------- /proj4rs/src/math/gauss.rs: -------------------------------------------------------------------------------- 1 | //! 2 | //! Gauss 3 | //! 4 | 5 | // Original copyright 6 | // 7 | // Copyright (c) 2003 Gerald I. Evenden 8 | // 9 | // 10 | // Permission is hereby granted, free of charge, to any person obtaining 11 | // a copy of this software and associated documentation files (the 12 | // "Software"), to deal in the Software without restriction, including 13 | // without limitation the rights to use, copy, modify, merge, publish, 14 | // distribute, sublicense, and/or sell copies of the Software, and to 15 | // permit persons to whom the Software is furnished to do so, subject to 16 | // the following conditions: 17 | // 18 | // The above copyright notice and this permission notice shall be 19 | // included in all copies or substantial portions of the Software. 20 | // 21 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 22 | // EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 23 | // MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 24 | // IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY 25 | // CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, 26 | // TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE 27 | // SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 28 | // 29 | use super::consts::{FRAC_PI_2, FRAC_PI_4}; 30 | use crate::errors::{Error, Result}; 31 | 32 | #[inline] 33 | fn srat(esinp: f64, ratexp: f64) -> f64 { 34 | ((1. - esinp) / (1. + esinp)).powf(ratexp) 35 | } 36 | 37 | #[derive(Debug, Clone)] 38 | pub(crate) struct Gauss { 39 | c: f64, 40 | k: f64, 41 | e: f64, 42 | ratexp: f64, 43 | } 44 | 45 | pub(crate) fn gauss_ini(e: f64, phi0: f64) -> Result<(Gauss, f64, f64)> { 46 | let es = e * e; 47 | let (sphi, mut cphi) = phi0.sin_cos(); 48 | 49 | cphi *= cphi; 50 | 51 | let rc = (1. - es).sqrt() / (1. - es * sphi * sphi); 52 | let c = (1. + es * cphi * cphi / (1. - es)).sqrt(); 53 | 54 | if c == 0. { 55 | return Err(Error::ToleranceConditionError); 56 | } 57 | 58 | let chi = (sphi / c).asin(); 59 | let ratexp = 0.5 * c * e; 60 | let k = (0.5 * chi + FRAC_PI_4).tan() 61 | / ((0.5 * phi0 + FRAC_PI_4).tan().powf(c) * srat(e * sphi, ratexp)); 62 | Ok((Gauss { c, k, e, ratexp }, chi, rc)) 63 | } 64 | 65 | pub(crate) fn gauss(lam: f64, phi: f64, en: &Gauss) -> (f64, f64) { 66 | ( 67 | // lam 68 | en.c * lam, 69 | // phi 70 | 2. * (en.k * (0.5 * phi + FRAC_PI_4).tan().powf(en.c) * srat(en.e * phi.sin(), en.ratexp)) 71 | .atan() 72 | - FRAC_PI_2, 73 | ) 74 | } 75 | 76 | pub(crate) fn inv_gauss(lam: f64, mut phi: f64, en: &Gauss) -> Result<(f64, f64)> { 77 | const DEL_TOL: f64 = 1.0e-14; 78 | const MAX_ITER: usize = 20; 79 | let mut i = MAX_ITER; 80 | let num = ((0.5 * phi + FRAC_PI_4).tan() / en.k).powf(1. / en.c); 81 | // XXX should try 82 | /* 83 | match (0..MAX_ITER).try_fold(phi, |phi, _| { 84 | let e_phi = 2. * (num * srat(en.e * phi.sin(), -0.5 * en.e)).atan() - FRAC_PI_2; 85 | if (e_phi - phi).abs() < DEL_TOL { 86 | Break(e_phi) 87 | } 88 | Continue(e_phi) 89 | }) { 90 | Break(phi) => Ok((lam / en.c, phi)), 91 | _ => Err(Error::InvMeridDistConvError), 92 | } 93 | */ 94 | while i > 0 { 95 | let e_phi = 2. * (num * srat(en.e * phi.sin(), -0.5 * en.e)).atan() - FRAC_PI_2; 96 | if (e_phi - phi).abs() < DEL_TOL { 97 | phi = e_phi; 98 | break; 99 | } 100 | phi = e_phi; 101 | i -= 1; 102 | } 103 | if i > 0 { 104 | Ok((lam / en.c, phi)) 105 | } else { 106 | Err(Error::InvMeridDistConvError) 107 | } 108 | } 109 | -------------------------------------------------------------------------------- /proj4rs/src/nadgrids/mod.rs: -------------------------------------------------------------------------------- 1 | //! 2 | //! Handle Nadgrids 3 | //! 4 | use crate::errors::{Error, Result}; 5 | use crate::transform::Direction; 6 | 7 | mod catlg; 8 | pub(crate) mod grid; 9 | 10 | pub use catlg::{catalog, Catalog, GridRef}; 11 | 12 | #[cfg(not(all(target_arch = "wasm32", target_os = "unknown")))] 13 | mod header; 14 | 15 | #[cfg(not(all(target_arch = "wasm32", target_os = "unknown")))] 16 | pub mod files; 17 | 18 | use std::ops::ControlFlow; 19 | 20 | pub use grid::Grid; 21 | 22 | /// NadGrids 23 | /// 24 | /// Returned from the sequence 25 | /// of nadgrids from projstring definition 26 | #[derive(Debug, Clone)] 27 | pub struct NadGrids(Vec); 28 | 29 | impl PartialEq for NadGrids { 30 | fn eq(&self, other: &Self) -> bool { 31 | // Don't bother to compare all names 32 | self.0.is_empty() && other.0.is_empty() 33 | } 34 | } 35 | 36 | impl NadGrids { 37 | pub fn apply_shift( 38 | &self, 39 | dir: Direction, 40 | lam: f64, 41 | phi: f64, 42 | z: f64, 43 | ) -> Result<(f64, f64, f64)> { 44 | if self.0.is_empty() { 45 | return Ok((lam, phi, z)); 46 | } 47 | 48 | // Find the correct (root) grid for an input 49 | let mut iter = self.0.iter(); 50 | let mut candidate = iter.find(|g| g.is_root() && g.matches(lam, phi, z)); 51 | 52 | // Check for childs grid 53 | if let Some(grid) = candidate { 54 | let _ = iter.try_fold(grid, |grid, g| { 55 | if !g.is_child_of(grid) { 56 | // No more childs, stop with the last candidate 57 | ControlFlow::Break(()) 58 | } else if g.matches(lam, phi, z) { 59 | // Match, check for childs 60 | candidate.replace(g); 61 | ControlFlow::Continue(g) 62 | } else { 63 | // Go next child 64 | ControlFlow::Continue(grid) 65 | } 66 | }); 67 | } 68 | 69 | match candidate { 70 | Some(g) => g.nad_cvt(dir, lam, phi, z), 71 | None => Err(Error::PointOutsideNadShiftArea), 72 | } 73 | } 74 | 75 | /// Return a list of grids from the catalog 76 | pub fn new_grid_transform(names: &str) -> Result { 77 | // Parse the grid list and return an error 78 | // if there is any mandatory grid or the list is not terminated by 79 | // '@null' 80 | let mut v: Vec = vec![]; 81 | 82 | match names.split(',').try_for_each(|s| { 83 | let s = s.trim(); 84 | if s == "@null" || s == "null" { 85 | // Allow empty list 86 | // Mark also the end of parsing 87 | ControlFlow::Break(true) 88 | } else if let Some(s) = s.strip_prefix('@') { 89 | // Optional grid 90 | catalog::find_grids(s, &mut v); 91 | ControlFlow::Continue(()) 92 | } else { 93 | // Mandatory grid 94 | if catalog::find_grids(s, &mut v) { 95 | ControlFlow::Continue(()) 96 | } else { 97 | ControlFlow::Break(false) 98 | } 99 | } 100 | }) { 101 | ControlFlow::Break(true) => Ok(Self(v)), 102 | ControlFlow::Break(false) => Err(Error::NadGridNotAvailable), 103 | _ => { 104 | if v.is_empty() { 105 | Err(Error::NadGridNotAvailable) 106 | } else { 107 | Ok(Self(v)) 108 | } 109 | } 110 | } 111 | } 112 | 113 | pub fn is_empty(&self) -> bool { 114 | self.0.is_empty() 115 | } 116 | } 117 | -------------------------------------------------------------------------------- /proj4rs/src/adaptors.rs: -------------------------------------------------------------------------------- 1 | //! 2 | //! Transform adaptors 3 | //! 4 | #[cfg(feature = "geo-types")] 5 | pub mod geo_types; 6 | 7 | use crate::errors::Result; 8 | use crate::proj::Proj; 9 | use crate::transform::{transform, Transform, TransformClosure}; 10 | 11 | // 12 | // Transform a 3-tuple 13 | // 14 | impl Transform for (f64, f64, f64) { 15 | fn transform_coordinates(&mut self, f: &mut F) -> Result<()> { 16 | (self.0, self.1, self.2) = f(self.0, self.1, self.2)?; 17 | Ok(()) 18 | } 19 | } 20 | 21 | // 22 | // Transform a 2-tuple 23 | // 24 | impl Transform for (f64, f64) { 25 | fn transform_coordinates(&mut self, f: &mut F) -> Result<()> { 26 | (self.0, self.1) = f(self.0, self.1, 0.).map(|(x, y, _)| (x, y))?; 27 | Ok(()) 28 | } 29 | } 30 | 31 | /// Transform a 3-tuple 32 | /// 33 | /// ```rust 34 | /// use proj4rs::Proj; 35 | /// use proj4rs::adaptors::transform_vertex_3d; 36 | /// 37 | /// let dst = Proj::from_proj_string("+proj=utm +ellps=GRS80 +zone=30").unwrap(); 38 | /// let src = Proj::from_proj_string("+proj=latlong +ellps=GRS80").unwrap(); 39 | /// 40 | /// let (x, y, z) = transform_vertex_3d(&src, &dst, (2.0, 1.0, 0.0)).unwrap(); 41 | /// ``` 42 | pub fn transform_vertex_3d(src: &Proj, dst: &Proj, pt: (f64, f64, f64)) -> Result<(f64, f64, f64)> { 43 | let mut pt_out = pt; 44 | transform(src, dst, &mut pt_out)?; 45 | Ok(pt_out) 46 | } 47 | 48 | /// Transform a 2-tuple 49 | /// 50 | /// ```rust 51 | /// use proj4rs::Proj; 52 | /// use proj4rs::adaptors::transform_vertex_2d; 53 | /// 54 | /// let dst = Proj::from_proj_string("+proj=utm +ellps=GRS80 +zone=30").unwrap(); 55 | /// let src = Proj::from_proj_string("+proj=latlong +ellps=GRS80").unwrap(); 56 | /// 57 | /// let (x, y) = transform_vertex_2d(&src, &dst, (2.0, 1.0)).unwrap(); 58 | /// ``` 59 | #[inline(always)] 60 | pub fn transform_vertex_2d(src: &Proj, dst: &Proj, pt: (f64, f64)) -> Result<(f64, f64)> { 61 | transform_vertex_3d(src, dst, (pt.0, pt.1, 0.)).map(|(x, y, _)| (x, y)) 62 | } 63 | 64 | /// Transform x, y and z value 65 | /// 66 | /// ```rust 67 | /// use proj4rs::Proj; 68 | /// use proj4rs::adaptors::transform_xyz; 69 | /// 70 | /// let dst = Proj::from_proj_string("+proj=utm +ellps=GRS80 +zone=30").unwrap(); 71 | /// let src = Proj::from_proj_string("+proj=latlong +ellps=GRS80").unwrap(); 72 | /// 73 | /// let (x, y, z) = transform_xyz(&src, &dst, 2.0, 1.0, 0.0).unwrap(); 74 | /// ``` 75 | #[inline(always)] 76 | pub fn transform_xyz(src: &Proj, dst: &Proj, x: f64, y: f64, z: f64) -> Result<(f64, f64, f64)> { 77 | transform_vertex_3d(src, dst, (x, y, z)) 78 | } 79 | 80 | /// Transform x, y value 81 | /// 82 | /// ```rust 83 | /// use proj4rs::Proj; 84 | /// use proj4rs::adaptors::transform_xy; 85 | /// 86 | /// let dst = Proj::from_proj_string("+proj=utm +ellps=GRS80 +zone=30").unwrap(); 87 | /// let src = Proj::from_proj_string("+proj=latlong +ellps=GRS80").unwrap(); 88 | /// 89 | /// let (x, y) = transform_xy(&src, &dst, 2.0, 1.0).unwrap(); 90 | /// ``` 91 | #[inline(always)] 92 | pub fn transform_xy(src: &Proj, dst: &Proj, x: f64, y: f64) -> Result<(f64, f64)> { 93 | transform_xyz(src, dst, x, y, 0.).map(|(x, y, _)| (x, y)) 94 | } 95 | 96 | // 97 | // Transform an array of 3-tuple: 98 | // 99 | impl Transform for [(f64, f64, f64)] { 100 | fn transform_coordinates(&mut self, f: &mut F) -> Result<()> { 101 | self.iter_mut() 102 | .try_for_each(|xyz| xyz.transform_coordinates(f)) 103 | } 104 | } 105 | 106 | // 107 | // Transform an array of 2-tuple: 108 | // 109 | impl Transform for [(f64, f64)] { 110 | fn transform_coordinates(&mut self, f: &mut F) -> Result<()> { 111 | self.iter_mut() 112 | .try_for_each(|xy| xy.transform_coordinates(f)) 113 | } 114 | } 115 | -------------------------------------------------------------------------------- /proj4rs/src/projections/tmerc.rs: -------------------------------------------------------------------------------- 1 | //! 2 | //! Transverse mercator 3 | //! 4 | //! Provide both Evenden/Snyder and Poder/ensager algorithm, depending on 5 | //! parameters 6 | //! 7 | //! The default algorithm is Poder/Ensager except for the spherical case 8 | //! where the Evenden/Snyder is used 9 | //! 10 | //! 11 | 12 | use crate::errors::{Error, Result}; 13 | use crate::parameters::ParamList; 14 | use crate::proj::ProjData; 15 | use crate::projections::{estmerc, etmerc}; 16 | 17 | // Projection stub 18 | super::projection! { tmerc } 19 | 20 | #[derive(Debug, Clone)] 21 | pub(crate) enum Projection { 22 | Exact(etmerc::Projection), 23 | Approx(estmerc::Projection), 24 | } 25 | 26 | use Projection::*; 27 | 28 | impl Projection { 29 | const ALG_PARAM: &'static str = "algo"; 30 | 31 | pub fn tmerc(p: &mut ProjData, params: &ParamList) -> Result { 32 | if p.ellps.is_sphere() || params.check_option("approx")? { 33 | Ok(Approx(estmerc::Projection::estmerc(p, params)?)) 34 | } else { 35 | // try 'algo' parameter 36 | match params.try_value(Self::ALG_PARAM)? { 37 | Some("evenden_snyder") => Ok(Approx(estmerc::Projection::estmerc(p, params)?)), 38 | Some("poder_engsager") | None => Ok(Exact(etmerc::Projection::etmerc(p, params)?)), 39 | Some(_) => Err(Error::InvalidParameterValue(Self::ALG_PARAM)), 40 | } 41 | } 42 | } 43 | 44 | pub fn forward(&self, lam: f64, phi: f64, z: f64) -> Result<(f64, f64, f64)> { 45 | match self { 46 | Exact(p) => p.forward(lam, phi, z), 47 | Approx(p) => p.forward(lam, phi, z), 48 | } 49 | } 50 | 51 | pub fn inverse(&self, x: f64, y: f64, z: f64) -> Result<(f64, f64, f64)> { 52 | match self { 53 | Exact(p) => p.inverse(x, y, z), 54 | Approx(p) => p.inverse(x, y, z), 55 | } 56 | } 57 | 58 | pub const fn has_inverse() -> bool { 59 | true 60 | } 61 | 62 | pub const fn has_forward() -> bool { 63 | true 64 | } 65 | } 66 | 67 | #[cfg(test)] 68 | mod tests { 69 | use crate::math::consts::EPS_10; 70 | use crate::proj::Proj; 71 | use crate::tests::utils::{test_proj_forward, test_proj_inverse}; 72 | 73 | #[test] 74 | fn proj_estmerc_ell() { 75 | let p = Proj::from_proj_string("+proj=tmerc +ellps=GRS80 +approx").unwrap(); 76 | 77 | println!("{:#?}", p.data()); 78 | println!("{:#?}", p.projection()); 79 | 80 | let inputs = [ 81 | ((2., 1., 0.), (222650.79679577847, 110642.22941192707, 0.)), 82 | ((2., -1., 0.), (222650.79679577847, -110642.22941192707, 0.)), 83 | ((-2., 1., 0.), (-222650.79679577847, 110642.22941192707, 0.)), 84 | ( 85 | (-2., -1., 0.), 86 | (-222650.79679577847, -110642.22941192707, 0.), 87 | ), 88 | ]; 89 | 90 | test_proj_forward(&p, &inputs, EPS_10); 91 | test_proj_inverse(&p, &inputs, EPS_10); 92 | } 93 | 94 | #[test] 95 | fn proj_estmerc_sph() { 96 | // Spherical planet will choose estmerc algorithm 97 | let p = Proj::from_proj_string("+proj=tmerc +R=6400000").unwrap(); 98 | 99 | println!("{:#?}", p.data()); 100 | println!("{:#?}", p.projection()); 101 | 102 | // Sames results as Proj9 'proj -d 11 +proj=tmerc +R=6400000 +approx' 103 | 104 | let inputs = [ 105 | ((2., 1., 0.), (223413.46640632232, 111769.14504059685, 0.)), 106 | ((2., -1., 0.), (223413.46640632232, -111769.14504059685, 0.)), 107 | ((-2., 1., 0.), (-223413.46640632208, 111769.14504059685, 0.)), 108 | ( 109 | (-2., -1., 0.), 110 | (-223413.46640632208, -111769.14504059685, 0.), 111 | ), 112 | ]; 113 | 114 | test_proj_forward(&p, &inputs, EPS_10); 115 | test_proj_inverse(&p, &inputs, EPS_10); 116 | } 117 | } 118 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![Crates.io](https://img.shields.io/crates/d/proj4rs)](https://crates.io/crates/proj4rs) 2 | [![Documentation](https://img.shields.io/badge/Documentation-Published-green)](https://docs.rs/proj4rs/latest/proj4rs/) 3 | [![Demo](https://img.shields.io/badge/Demo-Published-green)](https://docs.3liz.org/proj4rs/) 4 | 5 | --- 6 | 7 | Rust library for transforming geographic point coordinates 8 | from one coordinate system to another. 9 | This is a pure Rust implementation 10 | of the [PROJ.4 project](https://proj.org/en/9.2/faq.html#what-happened-to-proj-4). 11 | 12 | The documentation is available on [docs.rs](https://docs.rs/proj4rs/) and the demo on [docs.3liz.org](https://docs.3liz.org/proj4rs/). 13 | 14 | # Features and Limitations 15 | 16 | - The aim of Proj4rs is to provide the same functionality as the 17 | [proj4js library](https://github.com/proj4js/proj4js). 18 | - This port implements the PROJ.4 API, 19 | which means there's no 3D/4D/orthometric transformation ATM. 20 | - The goal of Proj4rs is not to be a replacement of PROJ, 21 | but instead being a lightweight implementation of transformations 22 | from CRS to CRS that could be used in Rust and WASM environments. 23 | - This crate does not provide support for WKT. Instead, 24 | there is a dedicated crate for transforming WKT strings to proj string. 25 | - It aims to be WASM compatible for the `wasm32-unknown-unknown` target. 26 | - No installation of external C libraries such as `libproj` or `sqlite3` is needed. 27 | 28 | ## Basic usage in Rust 29 | 30 | Define the coordinate system with proj strings and use the `transform` function. 31 | You can easily get the projection string of any coordinate system 32 | from [EPSG.io](https://epsg.io/). 33 | 34 | **Note**: Proj4rs use *radians* as natural angular unit (as does the original proj library) 35 | 36 | Example: 37 | 38 | ```rust 39 | use proj4rs; 40 | use proj4rs::proj::Proj; 41 | 42 | // EPSG:5174 - Example 43 | let from = Proj::from_proj_string(concat!( 44 | "+proj=tmerc +lat_0=38 +lon_0=127.002890277778", 45 | " +k=1 +x_0=200000 +y_0=500000 +ellps=bessel", 46 | " +towgs84=-145.907,505.034,685.756,-1.162,2.347,1.592,6.342", 47 | " +units=m +no_defs +type=crs" 48 | )) 49 | .unwrap(); 50 | 51 | // EPSG:4326 - WGS84, known to us as basic longitude and latitude. 52 | let to = Proj::from_proj_string(concat!( 53 | "+proj=longlat +ellps=WGS84", 54 | " +datum=WGS84 +no_defs" 55 | )) 56 | .unwrap(); 57 | 58 | let mut point_3d = (198236.3200000003, 453407.8560000006, 0.0); 59 | proj4rs::transform::transform(&from, &to, &mut point_3d).unwrap(); 60 | 61 | // Note that WGS84 output from this library is in radians, not degrees. 62 | point_3d.0 = point_3d.0.to_degrees(); 63 | point_3d.1 = point_3d.1.to_degrees(); 64 | 65 | // Output in longitude, latitude, and height. 66 | println!("{} {}",point_3d.0, point_3d.1); // 126.98069676435814, 37.58308534678718 67 | ``` 68 | 69 | ## WKT support 70 | 71 | If you need full support for WKT, please rely on `proj` which provides 72 | a great implementation of the standards. 73 | 74 | If you want WKT support in WASM, please have a look at: 75 | 76 | - https://github.com/3liz/proj4wkt-rs 77 | - https://github.com/frewsxcv/crs-definitions 78 | 79 | ## Grid shift supports 80 | 81 | Nadgrid support is still experimental. 82 | Currently, only Ntv2 multi grids are supported for native build and WASM. 83 | 84 | ## JavaScript API 85 | 86 | When compiled for WASM, the library exposes JavaScript API 87 | that is very similar to that of proj4js. 88 | A thin JavaScript layer provides full compatibility with proj4js 89 | and thus can be used as a proj4js replacement. 90 | 91 | Example: 92 | 93 | ```javascript 94 | let from = new Proj.Projection("+proj=latlong +ellps=GRS80"); 95 | let to = new Proj.Projection("+proj=etmerc +ellps=GRS80"); 96 | let point = new Proj.Point(2.0, 1.0, 0.0); 97 | 98 | // Point is transformed in place 99 | Proj.transform(from, to, point); 100 | ``` 101 | 102 | ## Contributing 103 | 104 | You can contribute to this library by going on the [proj4rs](./CONTRIBUTING.md) repository 105 | -------------------------------------------------------------------------------- /proj4rs-php/src/lib.rs: -------------------------------------------------------------------------------- 1 | //! 2 | //! Php binding entry point 3 | //! 4 | //! See https://davidcole1340.github.io/ext-php-rs 5 | 6 | 7 | use proj4rs::{errors, proj, transform}; 8 | use ext_php_rs::prelude::*; 9 | 10 | 11 | #[cfg(feature = "logging")] 12 | use log; 13 | 14 | // Entry point 15 | //pub fn main() { 16 | // #[cfg(feature = "logging")] 17 | // console_log::init_with_level(log::Level::Trace).unwrap(); 18 | //} 19 | 20 | // ---------------------------- 21 | // Wrapper for Projection 22 | // --------------------------- 23 | #[php_class] 24 | pub struct Projection { 25 | inner: proj::Proj, 26 | } 27 | 28 | impl From for Projection { 29 | fn from(p: proj::Proj) -> Self { 30 | Self { inner: p } 31 | } 32 | } 33 | 34 | 35 | #[php_impl(rename_methods = "camelCase")] 36 | impl Projection { 37 | 38 | #[constructor] 39 | fn new(defn: &str) -> PhpResult { 40 | proj::Proj::from_user_string(defn) 41 | .map(Projection::from) 42 | .map_err(|e| PhpException::from(e.to_string())) 43 | } 44 | 45 | // see https://github.com/davidcole1340/ext-php-rs/issues/325 46 | // pub fn projname(&self) -> &'static str { 47 | // self.inner.projname() 48 | // } 49 | #[getter(rename = "projName")] 50 | pub fn projname(&self) -> String { 51 | self.inner.projname().into() 52 | } 53 | 54 | #[getter(rename = "isLatlong")] 55 | pub fn is_latlong(&self) -> bool { 56 | self.inner.is_latlong() 57 | } 58 | 59 | #[getter(rename = "isGeocentric")] 60 | pub fn is_geocent(&self) -> bool { 61 | self.inner.is_geocent() 62 | } 63 | 64 | #[getter] 65 | pub fn axis(&self) -> String { 66 | String::from_utf8_lossy(self.inner.axis()).into_owned() 67 | } 68 | 69 | #[getter(rename = "isNormalizedAxis")] 70 | pub fn is_normalized_axis(&self) -> bool { 71 | self.inner.is_normalized_axis() 72 | } 73 | 74 | #[getter(rename = "toMeter")] 75 | pub fn to_meter(&self) -> f64 { 76 | self.inner.to_meter() 77 | } 78 | 79 | // see https://github.com/davidcole1340/ext-php-rs/issues/325 80 | // pub fn units(&self) -> &'static str { 81 | // self.inner.units() 82 | // } 83 | #[getter] 84 | pub fn units(&self) -> String { 85 | self.inner.units().into() 86 | } 87 | } 88 | 89 | 90 | // ---------------------------- 91 | // Wrapper for Transform 92 | // --------------------------- 93 | #[php_class] 94 | pub struct Point { 95 | #[prop] 96 | pub x: f64, 97 | #[prop] 98 | pub y: f64, 99 | #[prop] 100 | pub z: f64, 101 | } 102 | 103 | #[php_impl] 104 | impl Point { 105 | pub fn __construct(x: f64, y: f64, z: f64) -> Self { 106 | Self { x, y, z } 107 | } 108 | } 109 | 110 | // Transform Point 111 | 112 | impl transform::Transform for Point { 113 | /// Strict mode: return exception 114 | /// as soon as with have invalid coordinates or 115 | /// that the reprojection failed 116 | fn transform_coordinates(&mut self, f: &mut F) -> errors::Result<()> 117 | where 118 | F: FnMut(f64, f64, f64) -> errors::Result<(f64, f64, f64)>, 119 | { 120 | f(self.x, self.y, self.z).map(|(x, y, z)| { 121 | self.x = x; 122 | self.y = y; 123 | self.z = z; 124 | }) 125 | } 126 | } 127 | 128 | #[php_function] 129 | pub fn transform_point( 130 | src: &Projection, 131 | dst: &Projection, 132 | point: &mut Point, 133 | convert: bool, 134 | ) -> PhpResult<()> { 135 | if point.x.is_nan() || point.y.is_nan() { 136 | return Err(PhpException::from(errors::Error::NanCoordinateValue.to_string())); 137 | } 138 | 139 | if convert && src.inner.is_latlong() { 140 | point.x = point.x.to_radians(); 141 | point.y = point.y.to_radians(); 142 | } 143 | 144 | transform::transform(&src.inner, &dst.inner, point) 145 | .map_err(|e| PhpException::from(e.to_string()))?; 146 | 147 | if convert && dst.inner.is_latlong() { 148 | point.x = point.x.to_degrees(); 149 | point.y = point.y.to_degrees(); 150 | } 151 | Ok(()) 152 | } 153 | 154 | 155 | #[php_module] 156 | pub fn get_module(module: ModuleBuilder) -> ModuleBuilder { 157 | module 158 | } 159 | -------------------------------------------------------------------------------- /proj4rs/src/wasm/mod.rs: -------------------------------------------------------------------------------- 1 | //! 2 | //! Wasm bindgen entry point 3 | //! 4 | mod nadgrids; 5 | 6 | use crate::{errors, proj, transform, transform::TransformClosure}; 7 | use wasm_bindgen::prelude::*; 8 | 9 | use crate::log; 10 | 11 | // Js entry point 12 | #[cfg(feature = "with-wasm-entrypoint")] 13 | #[wasm_bindgen(start)] 14 | pub fn main() { 15 | #[cfg(feature = "logging")] 16 | console_log::init_with_level(log::Level::Trace).unwrap(); 17 | 18 | log::info!("Initialized proj4rs wasm module.") 19 | } 20 | 21 | // ---------------------------- 22 | // Wrapper for Projection 23 | // --------------------------- 24 | #[wasm_bindgen] 25 | pub struct Projection { 26 | inner: proj::Proj, 27 | } 28 | 29 | #[wasm_bindgen] 30 | impl Projection { 31 | #[wasm_bindgen(constructor)] 32 | pub fn new(defn: &str) -> Result { 33 | Ok(Self { 34 | inner: proj::Proj::from_user_string(defn)?, 35 | }) 36 | } 37 | 38 | #[wasm_bindgen(getter, js_name = projName)] 39 | pub fn projname(&self) -> String { 40 | self.inner.projname().into() 41 | } 42 | 43 | #[wasm_bindgen(getter, js_name = isLatlon)] 44 | pub fn is_latlong(&self) -> bool { 45 | self.inner.is_latlong() 46 | } 47 | 48 | #[wasm_bindgen(getter, js_name = isGeocentric)] 49 | pub fn is_geocent(&self) -> bool { 50 | self.inner.is_geocent() 51 | } 52 | 53 | #[wasm_bindgen(getter)] 54 | pub fn axis(&self) -> String { 55 | String::from_utf8_lossy(self.inner.axis()).into_owned() 56 | } 57 | 58 | #[wasm_bindgen(getter, js_name = isNormalizedAxis)] 59 | pub fn is_normalized_axis(&self) -> bool { 60 | self.inner.is_normalized_axis() 61 | } 62 | 63 | #[wasm_bindgen(getter)] 64 | pub fn to_meter(&self) -> f64 { 65 | self.inner.to_meter() 66 | } 67 | 68 | #[wasm_bindgen(getter)] 69 | pub fn units(&self) -> String { 70 | self.inner.units().into() 71 | } 72 | } 73 | 74 | // ---------------------------- 75 | // Wrapper for Transform 76 | // --------------------------- 77 | #[wasm_bindgen] 78 | pub struct Point { 79 | pub x: f64, 80 | pub y: f64, 81 | pub z: f64, 82 | } 83 | 84 | #[wasm_bindgen] 85 | impl Point { 86 | #[wasm_bindgen(constructor)] 87 | pub fn new(x: f64, y: f64, z: f64) -> Point { 88 | Self { x, y, z } 89 | } 90 | } 91 | 92 | impl transform::Transform for Point { 93 | /// Strict mode: return exception 94 | /// as soon as with have invalid coordinates or 95 | /// that the reprojection failed 96 | #[cfg(feature = "wasm-strict")] 97 | fn transform_coordinates(&mut self, f: &mut F) -> errors::Result<()> { 98 | f(self.x, self.y, self.z).map(|(x, y, z)| { 99 | self.x = x; 100 | self.y = y; 101 | self.z = z; 102 | }) 103 | } 104 | /// Relaxed mode: allow transformation failure: return NAN in case 105 | /// of projection failure 106 | /// Note: this is what is expected mostly from js app (at least OpenLayer) 107 | #[cfg(not(feature = "wasm-strict"))] 108 | fn transform_coordinates(&mut self, f: &mut F) -> errors::Result<()> { 109 | f(self.x, self.y, self.z) 110 | .map(|(x, y, z)| { 111 | self.x = x; 112 | self.y = y; 113 | self.z = z; 114 | }) 115 | .or_else(|_err| { 116 | // This will be activated with 'logging' feature 117 | log::error!("{:?}: ({}, {}, {})", _err, self.x, self.y, self.z); 118 | self.x = f64::NAN; 119 | self.y = f64::NAN; 120 | self.z = f64::NAN; 121 | Ok(()) 122 | }) 123 | } 124 | } 125 | 126 | #[wasm_bindgen] 127 | pub fn transform(src: &Projection, dst: &Projection, point: &mut Point) -> Result<(), JsError> { 128 | // log::debug!("transform: {}, {}, {}", point.x, point.y, point.z); 129 | 130 | if point.x.is_nan() || point.y.is_nan() { 131 | return Err(JsError::from(errors::Error::NanCoordinateValue)); 132 | } 133 | 134 | if src.inner.is_latlong() { 135 | point.x = point.x.to_radians(); 136 | point.y = point.y.to_radians(); 137 | } 138 | transform::transform(&src.inner, &dst.inner, point)?; 139 | if dst.inner.is_latlong() { 140 | point.x = point.x.to_degrees(); 141 | point.y = point.y.to_degrees(); 142 | } 143 | Ok(()) 144 | } 145 | -------------------------------------------------------------------------------- /proj4rs/src/wasm/nadgrids.rs: -------------------------------------------------------------------------------- 1 | //! 2 | //! WASM provider for nadgrids 3 | //! 4 | //! Use JS Dataview for passing nadgrids definition 5 | //! 6 | use crate::errors::Error; 7 | use crate::math::consts::SEC_TO_RAD; 8 | use crate::nadgrids::catalog; 9 | use crate::nadgrids::grid::{Grid, GridId, Lp}; 10 | use js_sys::DataView; 11 | use wasm_bindgen::prelude::*; 12 | 13 | const ERR_INVALID_HEADER: &str = "Wrong header"; 14 | const ERR_GSCOUNT_NOT_MATCHING: &str = "GS COUNT not matching"; 15 | 16 | const HEADER_SIZE: usize = 11 * 16; 17 | 18 | /// Read a binary NTv2 from Dataview. 19 | /// 20 | /// Note: only NTv2 file format are supported. 21 | #[wasm_bindgen] 22 | pub fn add_nadgrid(key: &str, view: &DataView) -> Result<(), JsError> { 23 | // Check endianess 24 | let is_le = view.get_int32_endian(8, true) == 11; 25 | 26 | // Read NTv2 overview header 27 | let nfields = view.get_int32_endian(8, is_le); 28 | if nfields != 11 { 29 | return Err(Error::InvalidNtv2GridFormat(ERR_INVALID_HEADER).into()); 30 | } 31 | 32 | let nsubgrids = view.get_int32_endian(40, is_le) as usize; 33 | 34 | // Read subsequent grids 35 | (0..nsubgrids).try_fold(HEADER_SIZE, |offset, _i| { 36 | read_subgrid(view, offset, is_le).and_then(|grid| { 37 | let offs = offset + grid.gs_count() * 16 + HEADER_SIZE; 38 | catalog::add_grid(key.into(), grid)?; 39 | Ok(offs) 40 | }) 41 | })?; 42 | Ok(()) 43 | } 44 | 45 | fn read_subgrid(view: &DataView, offset: usize, is_le: bool) -> Result { 46 | match view 47 | .buffer() 48 | .slice_with_end(offset as u32, offset as u32 + 8) 49 | .as_string() 50 | { 51 | Some(s) if s == "SUB_NAME" => Ok(()), 52 | _ => Err(Error::InvalidNtv2GridFormat(ERR_INVALID_HEADER)), 53 | }?; 54 | 55 | // SUB_NAME 56 | let id = GridId::from(( 57 | view.get_uint32_endian(offset + 4, is_le), 58 | view.get_uint32_endian(offset + 8, is_le), 59 | )); 60 | 61 | // PARENT 62 | let mut lineage = GridId::from(( 63 | view.get_uint32_endian(offset + 24, is_le), 64 | view.get_uint32_endian(offset + 24 + 4, is_le), 65 | )); 66 | 67 | if lineage.as_str() == "NONE" { 68 | lineage = GridId::root(); 69 | } 70 | 71 | let mut ll = Lp { 72 | lam: -view.get_float64_endian(120 + offset, is_le), // W_LONG 73 | phi: view.get_float64_endian(72 + offset, is_le), // S_LAT 74 | }; 75 | 76 | let ur = Lp { 77 | lam: -view.get_float64_endian(104 + offset, is_le), // E_LONG 78 | phi: view.get_float64_endian(88 + offset, is_le), // N_LAT 79 | }; 80 | 81 | let mut del = Lp { 82 | lam: view.get_float64_endian(152 + offset, is_le), // longitude interval 83 | phi: view.get_float64_endian(136 + offset, is_le), // latitude interval 84 | }; 85 | 86 | let lim = Lp { 87 | lam: (((ur.lam - ll.lam).abs() / del.lam + 0.5) + 1.).floor(), 88 | phi: (((ur.phi - ll.phi).abs() / del.phi + 0.5) + 1.).floor(), 89 | }; 90 | 91 | // units are in seconds of degree. 92 | ll.lam *= SEC_TO_RAD; 93 | ll.phi *= SEC_TO_RAD; 94 | del.lam *= SEC_TO_RAD; 95 | del.phi *= SEC_TO_RAD; 96 | 97 | // Read matrix data 98 | let nrows = lim.phi as usize; 99 | let rowsize = lim.lam as usize; 100 | 101 | let gs_count = view.get_int32_endian(168 + offset, is_le) as usize; 102 | if gs_count != nrows * rowsize { 103 | return Err(Error::InvalidNtv2GridFormat(ERR_GSCOUNT_NOT_MATCHING)); 104 | } 105 | 106 | let cvsoffset = offset + HEADER_SIZE; 107 | let mut cvs: Vec = (0..gs_count) 108 | .map(|i| Lp { 109 | lam: SEC_TO_RAD * (view.get_float32_endian(cvsoffset + i * 16, is_le) as f64), 110 | phi: SEC_TO_RAD * (view.get_float32_endian(cvsoffset + i * 16 + 4, is_le) as f64), 111 | }) 112 | .collect(); 113 | 114 | // See https://geodesie.ign.fr/contenu/fichiers/documentation/algorithmes/notice/NT111_V1_HARMEL_TransfoNTF-RGF93_FormatGrilleNTV2.pdf 115 | 116 | // In proj4, rows are stored in reverse order 117 | for i in 0..nrows { 118 | let offs = i * rowsize; 119 | cvs[offs..(offs + rowsize)].reverse(); 120 | } 121 | 122 | let epsilon = (del.lam.abs() + del.phi.abs()) / 10_000.; 123 | 124 | Ok(Grid { 125 | id, 126 | lineage, 127 | ll, 128 | ur, 129 | del, 130 | lim, 131 | epsilon, 132 | cvs: cvs.into_boxed_slice(), 133 | }) 134 | } 135 | -------------------------------------------------------------------------------- /js/ol-proj4rs-demo-app/sphere-mollweide.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Sphere Mollweide 6 | 7 | 8 | 9 | 15 | 16 | 17 |
OpenLayers + Proj4rs
18 |

Sphere Mollweide

19 |
20 |

21 | Example of a Sphere Mollweide map with a Graticule layer. This demo is the same as the 22 | OpenLayers one 23 | but here, it uses the proj4rs library instead of the proj4js one. 24 |

25 |
26 |
27 |             

proj4 OpenLayers

28 | 29 | import GeoJSON from 'ol/format/GeoJSON.js'; 30 | import Graticule from 'ol/layer/Graticule.js'; 31 | import Map from 'ol/Map.js'; 32 | import Projection from 'ol/proj/Projection.js'; 33 | import VectorLayer from 'ol/layer/Vector.js'; 34 | import VectorSource from 'ol/source/Vector.js'; 35 | import View from 'ol/View.js'; 36 | import proj4 from 'proj4'; 37 | import {register} from 'ol/proj/proj4.js'; 38 | ... 39 |
40 |
41 |             

proj4rs

42 | 43 | import GeoJSON from 'ol/format/GeoJSON.js'; 44 | import Graticule from 'ol/layer/Graticule.js'; 45 | import Map from 'ol/Map.js'; 46 | import Projection from 'ol/proj/Projection.js'; 47 | import VectorLayer from 'ol/layer/Vector.js'; 48 | import VectorSource from 'ol/source/Vector.js'; 49 | import View from 'ol/View.js'; 50 | import {proj4} from 'proj4rs/proj4.js'; 51 | import {register} from 'ol/proj/proj4.js'; 52 | ... 53 |
54 |
55 | 56 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | -------------------------------------------------------------------------------- /js/ol-proj4rs-demo-app/vector-projections.js: -------------------------------------------------------------------------------- 1 | import Map from 'ol/Map.js'; 2 | import View from 'ol/View.js'; 3 | import {proj4} from './assets/js/proj4.js'; 4 | import {getCenter} from 'ol/extent.js'; 5 | import {get as getProjection} from 'ol/proj.js'; 6 | import {register} from 'ol/proj/proj4.js'; 7 | import GeoJSON from 'ol/format/GeoJSON.js'; 8 | import Graticule from 'ol/layer/Graticule.js'; 9 | import VectorLayer from 'ol/layer/Vector.js'; 10 | import VectorSource from 'ol/source/Vector.js'; 11 | import {Fill, Style} from 'ol/style.js'; 12 | 13 | proj4.defs( 14 | 'EPSG:27700', 15 | '+proj=tmerc +lat_0=49 +lon_0=-2 +k=0.9996012717 ' + 16 | '+x_0=400000 +y_0=-100000 +ellps=airy ' + 17 | '+towgs84=446.448,-125.157,542.06,0.15,0.247,0.842,-20.489 ' + 18 | '+units=m +no_defs' 19 | ); 20 | proj4.defs( 21 | 'EPSG:23032', 22 | '+proj=utm +zone=32 +ellps=intl ' + 23 | '+towgs84=-87,-98,-121,0,0,0,0 +units=m +no_defs' 24 | ); 25 | proj4.defs( 26 | 'EPSG:5479', 27 | '+proj=lcc +lat_1=-76.66666666666667 +lat_2=' + 28 | '-79.33333333333333 +lat_0=-78 +lon_0=163 +x_0=7000000 +y_0=5000000 ' + 29 | '+ellps=GRS80 +towgs84=0,0,0,0,0,0,0 +units=m +no_defs' 30 | ); 31 | proj4.defs( 32 | 'EPSG:3413', 33 | '+proj=stere +lat_0=90 +lat_ts=70 +lon_0=-45 +k=1 ' + 34 | '+x_0=0 +y_0=0 +datum=WGS84 +units=m +no_defs' 35 | ); 36 | proj4.defs( 37 | 'EPSG:2163', 38 | '+proj=laea +lat_0=45 +lon_0=-100 +x_0=0 +y_0=0 ' + 39 | '+a=6370997 +b=6370997 +units=m +no_defs' 40 | ); 41 | proj4.defs( 42 | 'ESRI:54009', 43 | '+proj=moll +lon_0=0 +x_0=0 +y_0=0 +datum=WGS84 ' + '+units=m +no_defs' 44 | ); 45 | register(proj4); 46 | 47 | const proj27700 = getProjection('EPSG:27700'); 48 | proj27700.setExtent([-650000, -150000, 1350000, 1450000]); 49 | proj27700.setWorldExtent([-65, -15, 135, 145]); 50 | 51 | const proj23032 = getProjection('EPSG:23032'); 52 | proj23032.setExtent([-1206118.71, 4021309.92, 1295389.0, 8051813.28]); 53 | proj23032.setWorldExtent([-121, 20, 130, 75]); 54 | 55 | const proj5479 = getProjection('EPSG:5479'); 56 | proj5479.setExtent([6825737.53, 4189159.8, 9633741.96, 5782472.71]); 57 | proj5479.setWorldExtent([0, 0, 0, 0]); 58 | 59 | const proj3413 = getProjection('EPSG:3413'); 60 | proj3413.setExtent([-4194304, -4194304, 4194304, 4194304]); 61 | proj3413.setWorldExtent([-179.99, -40, 179.99, 84]); 62 | 63 | const proj2163 = getProjection('EPSG:2163'); 64 | proj2163.setExtent([-8040784.5135, -2577524.921, 3668901.4484, 4785105.1096]); 65 | proj2163.setWorldExtent([-180, 10, -10, 84]); 66 | 67 | const proj54009 = getProjection('ESRI:54009'); 68 | proj54009.setExtent([-18019909.21177587, -9009954.605703328, 18019909.21177587, 9009954.605703328]); 69 | proj54009.setWorldExtent([-179, -89.99, 179, 89.99]); 70 | 71 | const viewProjSelect = document.getElementById('view-projection'); 72 | 73 | const style = new Style({ 74 | fill: new Fill({ 75 | color: '#eeeeee', 76 | }), 77 | }); 78 | 79 | let vectorMap = new VectorLayer({ 80 | source: new VectorSource({ 81 | url: 'https://openlayers.org/data/vector/ecoregions.json', 82 | format: new GeoJSON(), 83 | }), 84 | style: function (feature) { 85 | const color = feature.get('COLOR_BIO') || '#eeeeee'; 86 | style.getFill().setColor(color); 87 | return style; 88 | }, 89 | }) 90 | 91 | const map = new Map({ 92 | keyboardEventTarget: document, 93 | layers: [ 94 | vectorMap, 95 | new Graticule(), 96 | ], 97 | target: 'map', 98 | view: new View({ 99 | projection: getProjection(viewProjSelect.value), 100 | center: getCenter(getProjection(viewProjSelect.value).getExtent() || [0, 0, 0, 0]), 101 | zoom: 0, 102 | extent: getProjection(viewProjSelect.value).getExtent() || undefined, 103 | }), 104 | }); 105 | 106 | function updateViewProjection() { 107 | const newProj = getProjection(viewProjSelect.value); 108 | const newProjExtent = newProj.getExtent(); 109 | const newView = new View({ 110 | projection: newProj, 111 | center: getCenter(newProjExtent || [0, 0, 0, 0]), 112 | zoom: 0, 113 | extent: newProjExtent || undefined, 114 | }); 115 | updateMapVar(newView) 116 | } 117 | 118 | function updateMapVar(view) { 119 | map.setView(view); 120 | 121 | let vectorMap = new VectorLayer({ 122 | source: new VectorSource({ 123 | url: 'https://openlayers.org/data/vector/ecoregions.json', 124 | format: new GeoJSON(), 125 | }), 126 | style: function (feature) { 127 | const color = feature.get('COLOR_BIO') || '#eeeeee'; 128 | style.getFill().setColor(color); 129 | return style; 130 | }, 131 | }) 132 | 133 | map.setLayers([vectorMap, new Graticule()]); 134 | } 135 | 136 | /** 137 | * Handle change event. 138 | */ 139 | viewProjSelect.onchange = function () { 140 | updateViewProjection(); 141 | }; 142 | -------------------------------------------------------------------------------- /proj4rs/examples/rsproj_bench.rs: -------------------------------------------------------------------------------- 1 | //! 2 | //! Display benchmarks for computing forward and inverse projections 3 | //! 4 | //! Compute benchmarks the same way as the `bench_proj_trans` PROJ 5 | //! test utility 6 | //! 7 | use clap::{ArgAction, Parser}; 8 | use proj4rs::{ 9 | errors::{Error, Result}, 10 | proj, transform, 11 | }; 12 | 13 | use rand::prelude::*; 14 | 15 | use std::io::{self, BufRead}; 16 | use std::time::Instant; 17 | 18 | #[derive(Parser)] 19 | #[command(author, version="0.1", about="Bench projections", long_about = None)] 20 | #[command(propagate_version = true)] 21 | struct Cli { 22 | /// Destination projection 23 | #[arg(long, required = true)] 24 | to: String, 25 | /// Source projection 26 | #[arg(long, default_value = "+proj=latlong")] 27 | from: String, 28 | /// Perform inverse projection 29 | #[arg(short, long)] 30 | inverse: bool, 31 | /// Increase verbosity 32 | #[arg(short, long, action = ArgAction::Count)] 33 | verbose: u8, 34 | #[arg(short, long, default_value_t = 5_000_000)] 35 | loops: u32, 36 | #[arg(long, default_value_t = 0.0)] 37 | noise_x: f64, 38 | #[arg(long, default_value_t = 0.0)] 39 | noise_y: f64, 40 | } 41 | 42 | fn main() -> Result<()> { 43 | let args = Cli::parse(); 44 | 45 | init_logger(args.verbose); 46 | 47 | log::debug!( 48 | "\nfrom: {}\nto: {}\ninverse: {}", 49 | args.from, 50 | args.to, 51 | args.inverse 52 | ); 53 | 54 | let (srcdef, dstdef): (&str, &str) = if args.inverse { 55 | (&args.to, &args.from) 56 | } else { 57 | (&args.from, &args.to) 58 | }; 59 | 60 | let loops = args.loops; 61 | let noise_x = args.noise_x; 62 | let noise_y = args.noise_y; 63 | 64 | let src = proj::Proj::from_user_string(srcdef)?; 65 | let dst = proj::Proj::from_user_string(dstdef)?; 66 | 67 | let stdin = io::stdin().lock(); 68 | 69 | fn from_parse_err(err: std::num::ParseFloatError) -> Error { 70 | Error::ParameterValueError(format!("{err:?}")) 71 | } 72 | 73 | for line in stdin.lines() { 74 | let line = line.unwrap(); 75 | let inputs = line.as_str().split_whitespace().collect::>(); 76 | if inputs.len() < 2 || inputs.len() > 3 { 77 | eprintln!("Expecting: ' , [,]' found: {}", line.as_str()); 78 | std::process::exit(1); 79 | } 80 | 81 | let mut x: f64 = inputs[0].parse().map_err(from_parse_err)?; 82 | let mut y: f64 = inputs[1].parse().map_err(from_parse_err)?; 83 | let z: f64 = if inputs.len() > 2 { 84 | inputs[2].parse().map_err(from_parse_err)? 85 | } else { 86 | 0. 87 | }; 88 | 89 | if src.is_latlong() { 90 | x = x.to_radians(); 91 | y = y.to_radians(); 92 | } 93 | 94 | let mut point = (x, y, z); 95 | 96 | transform::transform(&src, &dst, &mut point)?; 97 | println!("{} {} -> {:.16} {:.16}", x, y, point.0, point.1); 98 | 99 | let mut rng = rand::rng(); 100 | 101 | // Time noise generation 102 | let start = Instant::now(); 103 | for _ in 0..loops { 104 | if noise_x > 0. { 105 | point.0 = x + noise_x * (2. * rng.random::() - 1.); 106 | } 107 | if noise_y > 0. { 108 | point.1 = y + noise_y * (2. * rng.random::() - 1.); 109 | } 110 | } 111 | let noise_elapsed = start.elapsed(); 112 | 113 | let start = Instant::now(); 114 | for _ in 0..loops { 115 | if noise_x > 0. { 116 | point.0 = x + noise_x * (2. * rng.random::() - 1.); 117 | } 118 | if noise_y > 0. { 119 | point.1 = y + noise_y * (2. * rng.random::() - 1.); 120 | } 121 | transform::transform(&src, &dst, &mut point)?; 122 | } 123 | 124 | let elapsed = start.elapsed(); 125 | 126 | println!("Duration: {} ms", (elapsed - noise_elapsed).as_millis()); 127 | println!( 128 | "Throughput: {:.2} million coordinates/s", 129 | 1e-3 * (loops as f64) / (elapsed - noise_elapsed).as_millis() as f64 130 | ); 131 | } 132 | Ok(()) 133 | } 134 | 135 | // 136 | // Logger 137 | // 138 | fn init_logger(verbose: u8) { 139 | use env_logger::Env; 140 | use log::LevelFilter; 141 | 142 | let mut builder = env_logger::Builder::from_env(Env::default().default_filter_or("info")); 143 | 144 | match verbose { 145 | 1 => builder.filter_level(LevelFilter::Debug), 146 | _ if verbose > 1 => builder.filter_level(LevelFilter::Trace), 147 | _ => &mut builder, 148 | } 149 | .init(); 150 | } 151 | -------------------------------------------------------------------------------- /js/ol-proj4rs-demo-app/wms-image-custom-proj.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Single Image WMS with Proj4rs 6 | 7 | 8 | 9 | 10 | 11 |
OpenLayers + Proj4rs
12 |

Single Image WMS with Proj4rs

13 |
14 |

15 | With Proj4rs integration, OpenLayers can transform coordinates between arbitrary projections. This demo is the same as the 16 | OpenLayers one but here, it uses 17 | the proj4rs library instead of the proj4js one. 18 |

19 |
20 |
21 |             

proj4 OpenLayers

22 | 23 | import ImageLayer from 'ol/layer/Image.js'; 24 | import ImageWMS from 'ol/source/ImageWMS.js'; 25 | import Map from 'ol/Map.js'; 26 | import Projection from 'ol/proj/Projection.js'; 27 | import View from 'ol/View.js'; 28 | import proj4 from 'proj4'; 29 | import {ScaleLine, defaults as defaultControls} from 'ol/control.js'; 30 | import {fromLonLat} from 'ol/proj.js'; 31 | import {register} from 'ol/proj/proj4.js'; 32 | ... 33 |
34 |
35 |             

proj4rs

36 | 37 | import ImageLayer from 'ol/layer/Image.js'; 38 | import ImageWMS from 'ol/source/ImageWMS.js'; 39 | import Map from 'ol/Map.js'; 40 | import Projection from 'ol/proj/Projection.js'; 41 | import View from 'ol/View.js'; 42 | import {proj4} from 'proj4rs/proj4.js'; 43 | import {ScaleLine, defaults as defaultControls} from 'ol/control.js'; 44 | import {fromLonLat} from 'ol/proj.js'; 45 | import {register} from 'ol/proj/proj4.js'; 46 | ... 47 |
48 |
49 | 50 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | -------------------------------------------------------------------------------- /js/ol-proj4rs-demo-app/style.css: -------------------------------------------------------------------------------- 1 | @import "node_modules/ol/ol.css"; 2 | 3 | /* Main page */ 4 | 5 | :root { 6 | --main-transition: ease 200ms; 7 | } 8 | 9 | html, body { 10 | font-family: "Roboto Light", sans-serif; 11 | text-align: center; 12 | margin: 0; 13 | } 14 | 15 | header { 16 | font-weight: bold; 17 | font-size: 28px; 18 | padding-top: 20px; 19 | } 20 | 21 | h4 { 22 | font-size: 23px; 23 | } 24 | 25 | container { 26 | display: flex; 27 | flex-wrap: wrap; 28 | justify-content: center; 29 | } 30 | 31 | container > div { 32 | width: 370px; 33 | height: 210px; 34 | margin: 5px; 35 | display: flex; 36 | justify-content: center; 37 | align-items: center; 38 | } 39 | 40 | .example { 41 | -webkit-user-drag: none; 42 | background-color: #ffffff; 43 | color: black; 44 | width: 90%; 45 | height: 90%; 46 | display: flex; 47 | flex-direction: column; 48 | justify-content: center; 49 | align-items: center; 50 | text-decoration: none; 51 | border-radius: 23px; 52 | box-shadow: 0px 0px 16px 0px rgba(0,0,0,0.2); 53 | transition: var(--main-transition); 54 | } 55 | 56 | .example:hover { 57 | box-shadow: 0px 0px 16px 0px rgba(0,0,0,0.35); 58 | transition: var(--main-transition); 59 | width: 95%; 60 | height: 95%; 61 | 62 | strong { 63 | font-size: 135%; 64 | transition: var(--main-transition); 65 | } 66 | 67 | small { 68 | font-size: 84%; 69 | transition: var(--main-transition); 70 | } 71 | 72 | p { 73 | font-size: 100%; 74 | transition: var(--main-transition); 75 | } 76 | } 77 | 78 | .example:active { 79 | background-color: rgba(246, 246, 246, 0.36); 80 | } 81 | 82 | .example > strong { 83 | width: 90%; 84 | font-size: 125%; 85 | transition: var(--main-transition); 86 | } 87 | 88 | .example > small { 89 | width: 90%; 90 | font-size: 78%; 91 | font-style: italic; 92 | transition: var(--main-transition); 93 | } 94 | 95 | .example > p { 96 | width: 90%; 97 | font-size: 95%; 98 | transition: var(--main-transition); 99 | } 100 | 101 | /* Demos */ 102 | 103 | #map { 104 | margin: 0 12.5%; 105 | width: 75%; 106 | height: 650px; 107 | min-width: 580px; 108 | border: #3a3a3a solid 2px; 109 | border-radius: 10px 10px 0 0; 110 | border-bottom: none; 111 | } 112 | 113 | #map.simple { 114 | border-radius: 10px; 115 | border-bottom: #3a3a3a solid 2px;; 116 | .ol-layer > canvas { 117 | border-radius: 8px; 118 | } 119 | } 120 | 121 | .ol-layer > canvas { 122 | border-radius: 8px 8px 0 0; 123 | } 124 | 125 | .reprojection-form { 126 | background: linear-gradient(#f8f8f8, #ffffff); 127 | margin: 0 12.5%; 128 | width: 75%; 129 | min-width: 580px; 130 | padding: 15px 0; 131 | border: #3a3a3a solid 2px; 132 | display: grid; 133 | grid-template-columns: max-content max-content; 134 | grid-gap: 5px; 135 | column-gap: 20px; 136 | row-gap: 25px; 137 | align-items: center; 138 | justify-content: center; 139 | border-radius: 0 0 10px 10px; 140 | } 141 | 142 | .reprojection-form > select { 143 | background-color: white; 144 | border: none; 145 | height: 30px; 146 | padding: 0 15px; 147 | border-radius: 15px; 148 | box-shadow: inset 0px 0px 5px 0px rgba(0,0,0,0.2); 149 | transition: ease 80ms; 150 | } 151 | 152 | .reprojection-form > select:hover { 153 | box-shadow: inset 0px 0px 5px 0px rgba(0,0,0,0.35); 154 | transition: ease 80ms; 155 | } 156 | 157 | .reprojection-form > select:active { 158 | box-shadow: inset 0px 0px 5px 0px rgba(0,0,0,0.6); 159 | } 160 | 161 | .demo-description { 162 | width: 95%; 163 | margin: 25px auto; 164 | } 165 | 166 | /* Nav bar */ 167 | 168 | nav { 169 | background-color: white; 170 | border-radius: 0 7px 7px 0; 171 | position: fixed; 172 | width: 40px; 173 | height: 250px; 174 | top: 15vh; 175 | display: flex; 176 | flex-direction: column; 177 | text-indent: -460%; 178 | transition: ease 200ms; 179 | } 180 | 181 | nav > a:first-child { 182 | border-radius: 0 10px 0 0; 183 | border-top: solid 2px #000000; 184 | font-weight: bold; 185 | } 186 | 187 | nav > a:last-child { 188 | border-radius: 0 0 10px 0; 189 | border-bottom: solid 2px #000000; 190 | } 191 | 192 | nav:hover { 193 | width: 200px; 194 | text-indent: 0; 195 | .navLabel { 196 | width: 200px; 197 | color: black; 198 | } 199 | #navLabelActive { 200 | color: black; 201 | } 202 | } 203 | 204 | .navLabel { 205 | border-right: solid 3px #000000; 206 | color: black; 207 | background-color: white; 208 | width: 40px; 209 | height: 50px; 210 | transition: ease 200ms; 211 | text-decoration: none; 212 | text-align: center; 213 | line-height: 50px; 214 | } 215 | 216 | .navLabel:hover { 217 | background-color: rgb(237, 237, 237); 218 | } 219 | 220 | .navLabel:active { 221 | background-color: rgb(220, 220, 220); 222 | } 223 | 224 | #navLabelActive { 225 | background-color: rgb(220, 220, 220); 226 | } 227 | 228 | #navLabelActive:hover { 229 | background-color: rgb(237, 237, 237); 230 | } 231 | -------------------------------------------------------------------------------- /js/ol-proj4rs-demo-app/reprojection-image.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Image Reprojection 6 | 7 | 8 | 9 | 15 | 16 | 17 |
OpenLayers + Proj4rs
18 |

Image Reprojection

19 |
20 |
21 | 22 | 23 |
24 |

25 | This example shows client-side reprojection of single image source. This demo is the same as the 26 | OpenLayers one but here, it uses 27 | the proj4rs library instead of the proj4js one. 28 |

29 |
30 |
31 |             

proj4 OpenLayers

32 | 33 | import Map from 'ol/Map.js'; 34 | import OSM from 'ol/source/OSM.js'; 35 | import Static from 'ol/source/ImageStatic.js'; 36 | import View from 'ol/View.js'; 37 | import proj4 from 'proj4'; 38 | import {Image as, Tile as TileLayer} from 'ol/layer.js'; 39 | import {getCenter} from 'ol/extent.js'; 40 | import {register} from 'ol/proj/proj4.js'; 41 | import {transform} from 'ol/proj.js'; 42 | ... 43 |
44 |
45 |             

proj4rs

46 | 47 | import Map from 'ol/Map.js'; 48 | import OSM from 'ol/source/OSM.js'; 49 | import Static from 'ol/source/ImageStatic.js'; 50 | import View from 'ol/View.js'; 51 | import {proj4} from 'proj4rs/proj4.js'; 52 | import {Image as, Tile as TileLayer} from 'ol/layer.js'; 53 | import {getCenter} from 'ol/extent.js'; 54 | import {register} from 'ol/proj/proj4.js'; 55 | import {transform} from 'ol/proj.js'; 56 | ... 57 |
58 |
59 | 60 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | -------------------------------------------------------------------------------- /proj4rs/src/lib.rs: -------------------------------------------------------------------------------- 1 | //! 2 | //! Coordinate transformation library 3 | //! 4 | //! Based on Proj4 implementation 5 | //! 6 | //! References: 7 | //! * 8 | //! * 9 | //! 10 | //! The aim of Proj4rs is to provide at short term the same functionality as the original 11 | //! proj4 library. 12 | //! 13 | //! The long term project is to integrate feature from the proj library in its latest 14 | //! version. 15 | //! 16 | //! The goal of proj4rs is not to be a remplacement of proj, but instead being a light 17 | //! weight implementation of transformations from crs to crs that could be used 18 | //! in WASM environment 19 | //! 20 | //! ## Usage 21 | //! 22 | //! Note that angular units are in radians, not degrees ! 23 | //! 24 | //! Radian is natural unit for trigonometric opérations, like proj, proj4rs use radians 25 | //! for its operation while degrees are mostly used as end user input/output. 26 | //! 27 | //! Example: 28 | //! ``` 29 | //! use proj4rs::proj::Proj; 30 | //! 31 | //! // EPSG:5174 - Example 32 | //! let from = Proj::from_proj_string(concat!( 33 | //! "+proj=tmerc +lat_0=38 +lon_0=127.002890277778", 34 | //! " +k=1 +x_0=200000 +y_0=500000 +ellps=bessel", 35 | //! " +towgs84=-145.907,505.034,685.756,-1.162,2.347,1.592,6.342", 36 | //! " +units=m +no_defs +type=crs" 37 | //! )) 38 | //! .unwrap(); 39 | //! 40 | //! // EPSG:4326 - WGS84, known to us as basic longitude and latitude. 41 | //! let to = Proj::from_proj_string(concat!( 42 | //! "+proj=longlat +ellps=WGS84", 43 | //! " +datum=WGS84 +no_defs" 44 | //! )) 45 | //! .unwrap(); 46 | //! 47 | //! let mut point_3d = (198236.3200000003, 453407.8560000006, 0.0); 48 | //! proj4rs::transform::transform(&from, &to, &mut point_3d).unwrap(); 49 | //! 50 | //! // XXX Note that angular unit is radians, not degrees ! 51 | //! point_3d.0 = point_3d.0.to_degrees(); 52 | //! point_3d.1 = point_3d.1.to_degrees(); 53 | //! 54 | //! // Output in longitude, latitude, and height. 55 | //! println!("{} {}",point_3d.0, point_3d.1); // 126.98069676435814, 37.58308534678718 56 | //! ``` 57 | //! 58 | //! ## Optional features 59 | //! 60 | //! * **geo-types**: [geo-types]() support 61 | //! * **logging**: support for logging with [log](https://docs.rs/log/latest/log/) crate. 62 | //! If activated for WASM, it will use the [console-log](https://docs.rs/console_log/latest/console_log/) 63 | //! adaptor. 64 | //! * **wasm-strict**: used with WASM; Transformation operation will return exception as soon as we 65 | //! have invalid coordinates or that the reprojection failed. 66 | //! The default is to use a relaxed-mode that return NaN in case of projection failure: this is expected 67 | //! mostly from js app (at least with OpenLayer). 68 | //! * **multi-thread**: Support for multi-thread with NAD Grid processing, this is activated by 69 | //! default and disabled when compiling for WASM. 70 | //! * **crs-definitions**: Support for initializing projections from EPSG codes with the 71 | //! [crs_definitions](https://docs.rs/crs-definitions/latest/crs_definitions/) crate. 72 | //! 73 | //! ## WKT Support 74 | //! 75 | //! There is no actual default support for WKT in proj4rs 76 | //! If you are looking for WTK/Proje string conversion support in Rust, 77 | //! then have a look at: 78 | //! 79 | //! - 80 | //! - 81 | //! 82 | //! Note that the proj library provides a great implementation of the standard. 83 | //! 84 | //! ## Grid shift supports 85 | //! 86 | //! Nadgrid support is still experimental. 87 | //! Currently, only Ntv2 multi grids are supported for native build and WASM. 88 | //! 89 | 90 | mod datum_params; 91 | mod datum_transform; 92 | mod datums; 93 | mod ellipsoids; 94 | mod ellps; 95 | mod geocent; 96 | mod math; 97 | mod parameters; 98 | mod parse; 99 | mod prime_meridians; 100 | mod projstring; 101 | mod units; 102 | 103 | pub mod adaptors; 104 | pub mod errors; 105 | pub mod nadgrids; 106 | pub mod proj; 107 | pub mod projections; 108 | pub mod transform; 109 | 110 | // Reexport 111 | pub use proj::Proj; 112 | 113 | // Include wasm entry point for wasm32-unknown-unknown 114 | #[cfg(all(target_arch = "wasm32", target_os = "unknown"))] 115 | mod wasm; 116 | 117 | #[cfg(test)] 118 | mod tests; 119 | 120 | // log for logging (optional). 121 | #[cfg(feature = "logging")] 122 | use log; 123 | 124 | #[cfg(not(feature = "logging"))] 125 | mod log { 126 | // Use __XXX__ to prevent 'ambiguous name' error 127 | // when exporting 128 | macro_rules! __trace__ ( ($($tt:tt)*) => {{}} ); 129 | macro_rules! __debug__ ( ($($tt:tt)*) => {{}} ); 130 | macro_rules! __error__ ( ($($tt:tt)*) => {{}} ); 131 | macro_rules! __info__ ( ($($tt:tt)*) => {{}} ); 132 | macro_rules! __warn__ ( ($($tt:tt)*) => {{}} ); 133 | 134 | #[allow(unused_imports)] 135 | pub(crate) use { 136 | __debug__ as debug, __error__ as error, __info__ as info, __trace__ as trace, 137 | __warn__ as warn, 138 | }; 139 | } 140 | -------------------------------------------------------------------------------- /proj4rs/tests/proj4js_tests.rs: -------------------------------------------------------------------------------- 1 | //! 2 | //! Tests from proj4js 3 | //! 4 | //! Note: projection results may differs from proj by 10^-4 due to difference 5 | //! in math functions implementations (asinh, log1py...) 6 | //! 7 | use approx::assert_abs_diff_eq; 8 | use proj4rs::{proj, transform}; 9 | 10 | #[test] 11 | fn test_transform_with_datum() { 12 | //EPSG:3006 Definition - Sweden coordinate reference system 13 | let sweref99tm = concat!( 14 | "+proj=utm +zone=33 +ellps=GRS80 ", 15 | "+towgs84=0,0,0,0,0,0,0 +units=m +no_defs" 16 | ); 17 | // EPSG:3021 Definition - Sweden coordinate reference system 18 | let rt90 = concat!( 19 | "+proj=tmerc +lon_0=15.808277777799999 +lat_0=0.0 +k=1.0 ", 20 | "+x_0=1500000.0 +y_0=0.0 +ellps=bessel ", 21 | "+units=m +towgs84=414.1,41.3,603.1,-0.855,2.141,-7.023,0 ", 22 | "+no_defs" 23 | ); 24 | 25 | let from = proj::Proj::from_user_string(sweref99tm).unwrap(); 26 | let to = proj::Proj::from_user_string(rt90).unwrap(); 27 | 28 | let mut inp = (319180., 6399862., 0.); 29 | 30 | transform::transform(&from, &to, &mut inp).unwrap(); 31 | assert_abs_diff_eq!(inp.0, 1271137.92755580, epsilon = 1.0e-6); 32 | assert_abs_diff_eq!(inp.1, 6404230.29136189, epsilon = 1.0e-6); 33 | } 34 | 35 | #[test] 36 | fn test_transform_null_datum() { 37 | // Test when nadgrid list is empty 38 | // ESPG:2154 definition 39 | let epsg2154 = concat!( 40 | "+proj=lcc +lat_0=46.5 +lon_0=3 +lat_1=49 +lat_2=44 ", 41 | "+x_0=700000 +y_0=6600000 +ellps=GRS80 +towgs84=0,0,0,0,0,0,0 ", 42 | "+units=m +no_defs +type=crs" 43 | ); 44 | // ESPG:3857 definition 45 | let epsg3857 = concat!( 46 | "+proj=merc +a=6378137 +b=6378137 +lat_ts=0 +lon_0=0 +x_0=0 +y_0=0 +k=1 ", 47 | "+units=m +nadgrids=@null +wktext +no_defs +type=crs", 48 | ); 49 | 50 | let from = proj::Proj::from_user_string(epsg2154).unwrap(); 51 | let to = proj::Proj::from_user_string(epsg3857).unwrap(); 52 | 53 | let mut inp = (489353.59, 6587552.2, 0.); 54 | transform::transform(&from, &to, &mut inp).unwrap(); 55 | // Check against cs2cs output 56 | assert_abs_diff_eq!(inp.0, 28943.07106251, epsilon = 1.0e-6); 57 | assert_abs_diff_eq!(inp.1, 5837421.86634143, epsilon = 1.0e-6); 58 | } 59 | 60 | #[test] 61 | fn test_longlat_alias() { 62 | let wgs84 = concat!( 63 | "+title=WGS 84 (long/lat) +proj=longlat +ellps=WGS84 ", 64 | "+datum=WGS84 +units=degrees", 65 | ); 66 | 67 | let projection = proj::Proj::from_user_string(wgs84); 68 | assert!(projection.is_ok()); 69 | } 70 | 71 | #[test] 72 | fn test_transform_epsg3044() { 73 | // ESPG:3044 definition 74 | let epsg3044 = concat!("+proj=utm +zone=32 +ellps=GRS80 +units=m +towgs84=0,0,0,0,0,0,0 ",); 75 | // ESPG:3857 definition 76 | let epsg3857 = concat!( 77 | "+proj=merc +a=6378137 +b=6378137 +lat_ts=0 +lon_0=0 +x_0=0 +y_0=0 +k=1 ", 78 | "+units=m +nadgrids=@null", 79 | ); 80 | 81 | let from = proj::Proj::from_user_string(epsg3044).unwrap(); 82 | let to = proj::Proj::from_user_string(epsg3857).unwrap(); 83 | 84 | let mut inp = (580900., 5625000., 0.); 85 | transform::transform(&from, &to, &mut inp).unwrap(); 86 | assert_abs_diff_eq!(inp.0, 1129592.3568078864, epsilon = 1.0e-6); 87 | assert_abs_diff_eq!(inp.1, 6580906.077194334, epsilon = 1.0e-6); 88 | } 89 | 90 | #[test] 91 | fn test_axis_denormalize() { 92 | // ESPG:3044 definition 93 | let epsg3044 = concat!("+proj=utm +zone=32 +ellps=GRS80 +units=m +towgs84=0,0,0,0,0,0,0 ",); 94 | // ESPG:3857 definition 95 | let epsg3857 = concat!( 96 | "+proj=merc +a=6378137 +b=6378137 +lat_ts=0 +lon_0=0 +x_0=0 +y_0=0 +k=1 ", 97 | "+units=m +nadgrids=@null +axis=neu", 98 | ); 99 | 100 | let from = proj::Proj::from_user_string(epsg3044).unwrap(); 101 | let to = proj::Proj::from_user_string(epsg3857).unwrap(); 102 | 103 | let mut inp = (580900., 5625000., 0.); 104 | transform::transform(&from, &to, &mut inp).unwrap(); 105 | assert_abs_diff_eq!(inp.0, 6580906.077194334, epsilon = 1.0e-6); 106 | assert_abs_diff_eq!(inp.1, 1129592.3568078864, epsilon = 1.0e-6); 107 | } 108 | 109 | #[test] 110 | fn test_transform_epsg3844() { 111 | // ESPG:3844 definition 112 | let epsg3844 = concat!( 113 | "+proj=sterea +lat_0=46 +lon_0=25 +k=0.99975 +x_0=500000 +y_0=500000 ", 114 | "+ellps=krass ", 115 | //"+towgs84=2.329,-147.042,-92.08,0.309,-0.325,-0.497,5.69 ", 116 | //"+towgs84=44.107,-116.147,-54.648 ", 117 | //"+towgs84=28,-121,-77 ", 118 | "+units=m +no_defs +type=crs" 119 | ); 120 | // ESPG:3857 definition 121 | let epsg3857 = concat!( 122 | "+proj=merc +a=6378137 +b=6378137 +lat_ts=0 +lon_0=0 +x_0=0 +y_0=0 +k=1 ", 123 | "+units=m", 124 | ); 125 | 126 | // ESPG:3857 definition 2 127 | //let epsg3857 = concat!( 128 | // "+proj=webmerc +ellps=WGS84 +lat_ts=0 +lon_0=0 +x_0=0 +y_0=0 +k=1 ", 129 | // "+units=m +towgs84=0,0,0", 130 | //); 131 | 132 | let from = proj::Proj::from_user_string(epsg3844).unwrap(); 133 | let to = proj::Proj::from_user_string(epsg3857).unwrap(); 134 | 135 | let mut inp = (505000., 500000., 0.); 136 | transform::transform(&from, &to, &mut inp).unwrap(); 137 | // Compare results from cs2cs output 138 | assert_abs_diff_eq!(inp.0, 2790174.2500622645, epsilon = 1.0e-6); 139 | assert_abs_diff_eq!(inp.1, 5780346.2980352566, epsilon = 1.0e-6); 140 | } 141 | -------------------------------------------------------------------------------- /proj4rs/src/datum_transform.rs: -------------------------------------------------------------------------------- 1 | //! 2 | //! Datum transformation 3 | //! 4 | //! As with proj4/5 the datum transformation use WGS84 as hub for 5 | //! converting data from one crs to another 6 | //! 7 | //! Datum shifts are carried out with the following steps: 8 | //! 9 | //! 1. Convert (latitude, longitude, ellipsoidal height) to 10 | //! 3D geocentric cartesian coordinates (X, Y, Z) 11 | //! 2. Transform the (X, Y, Z) coordinates to the new datum, using a 12 | //! 7 parameter Helmert transformation. 13 | //! 3. Convert (X, Y, Z) back to (latitude, longitude, ellipsoidal height) 14 | //! 15 | //! Actually, the step 2 use WGS84 as conversion *hub* which leads to apply 16 | //! 2 Helmert transformations. 17 | //! 18 | //! With natgrids the steps are slightly different: 19 | //! 1. Apply nadgrid transformation with source datum 20 | //! 2. Convert to geocentric with source ellipsoid parameters 21 | //! 3. Convert to geodetic with dest ellipsoid. 22 | //! 4. Apply inverse nadgrids transformation with destination datum 23 | //! 24 | use crate::datum_params::DatumParams; 25 | use crate::ellps::Ellipsoid; 26 | use crate::errors::Result; 27 | use crate::geocent::{geocentric_to_geodetic, geodetic_to_geocentric}; 28 | use crate::transform::Direction; 29 | 30 | use DatumParams::*; 31 | 32 | const SRS_WGS84_SEMIMAJOR: f64 = 6378137.0; 33 | const SRS_WGS84_SEMIMINOR: f64 = 6356752.314; 34 | const SRS_WGS84_ES: f64 = 0.0066943799901413165; 35 | 36 | /// Hold datum Informations 37 | #[derive(Debug, Clone)] 38 | pub(crate) struct Datum { 39 | params: DatumParams, 40 | pub a: f64, 41 | pub b: f64, 42 | pub es: f64, 43 | } 44 | 45 | impl Datum { 46 | pub fn new(ellps: &Ellipsoid, params: DatumParams) -> Self { 47 | // Change ellipse parameters to wgs84 48 | // when using nadgrids 49 | let (a, b, es) = if params.use_nadgrids() { 50 | (SRS_WGS84_SEMIMAJOR, SRS_WGS84_SEMIMINOR, SRS_WGS84_ES) 51 | } else { 52 | (ellps.a, ellps.b, ellps.es) 53 | }; 54 | 55 | Self { 56 | // check for WGS84/GRS80 57 | params: if params == ToWGS84_3(0., 0., 0.) 58 | && ellps.a == SRS_WGS84_SEMIMAJOR 59 | && (ellps.es - SRS_WGS84_ES).abs() < 0.000000000050 60 | { 61 | ToWGS84_0 62 | } else { 63 | params 64 | }, 65 | a, 66 | b, 67 | es, 68 | } 69 | } 70 | 71 | /// Convert from geodetic coordinates to wgs84/geocentric 72 | fn towgs84(&self, x: f64, y: f64, z: f64) -> Result<(f64, f64, f64)> { 73 | match &self.params { 74 | ToWGS84_0 => geodetic_to_geocentric(x, y, z, self.a, self.es), 75 | ToWGS84_3(dx, dy, dz) => geodetic_to_geocentric(x, y, z, self.a, self.es) 76 | .map(|(x, y, z)| (x + dx, y + dy, z + dz)), 77 | ToWGS84_7(dx, dy, dz, rx, ry, rz, s) => { 78 | geodetic_to_geocentric(x, y, z, self.a, self.es).map(|(x, y, z)| { 79 | ( 80 | dx + s * (x - rz * y + ry * z), 81 | dy + s * (rz * x + y - rx * z), 82 | dz + s * (-ry * x + rx * y + z), 83 | ) 84 | }) 85 | } 86 | NadGrids(grids) => grids 87 | .apply_shift(Direction::Forward, x, y, z) 88 | .and_then(|(x, y, z)| geodetic_to_geocentric(x, y, z, self.a, self.es)), 89 | NoDatum => Ok((x, y, z)), 90 | } 91 | } 92 | 93 | /// Convert from geocentric/wgs84 to geodetic coordinates 94 | fn fromwgs84(&self, x: f64, y: f64, z: f64) -> Result<(f64, f64, f64)> { 95 | match &self.params { 96 | ToWGS84_0 => geocentric_to_geodetic(x, y, z, self.a, self.es, self.b), 97 | ToWGS84_3(dx, dy, dz) => { 98 | geocentric_to_geodetic(x - dx, y - dy, z - dz, self.a, self.es, self.b) 99 | } 100 | ToWGS84_7(dx, dy, dz, rx, ry, rz, s) => { 101 | let (x, y, z) = ((x - dx) / s, (y - dy) / s, (z - dz) / s); 102 | geocentric_to_geodetic( 103 | x + rz * y - ry * z, 104 | -rz * x + y + rx * z, 105 | ry * x - rx * y + z, 106 | self.a, 107 | self.es, 108 | self.b, 109 | ) 110 | } 111 | NadGrids(grids) => geocentric_to_geodetic(x, y, z, self.a, self.es, self.b) 112 | .and_then(|(x, y, z)| grids.apply_shift(Direction::Inverse, x, y, z)), 113 | NoDatum => Ok((x, y, z)), 114 | } 115 | } 116 | 117 | #[inline] 118 | pub fn no_datum(&self) -> bool { 119 | self.params.no_datum() 120 | } 121 | 122 | /// Return true if the datum are identical 123 | pub fn is_identical_to(&self, other: &Self) -> bool { 124 | // the tolerance for es is to ensure that GRS80 and WGS84 125 | // are considered identical 126 | (self.params == other.params) 127 | && self.a == other.a 128 | && (self.es - other.es).abs() < 0.000000000050 129 | } 130 | 131 | /// Transform geographic coordinates between datums 132 | /// 133 | /// No identity checking is done 134 | #[inline] 135 | pub fn transform(src: &Self, dst: &Self, x: f64, y: f64, z: f64) -> Result<(f64, f64, f64)> { 136 | src.towgs84(x, y, z) 137 | .and_then(|(x, y, z)| dst.fromwgs84(x, y, z)) 138 | } 139 | } 140 | -------------------------------------------------------------------------------- /proj4rs/src/projections/somerc.rs: -------------------------------------------------------------------------------- 1 | //! 2 | //! Swiss Oblique Mercator 3 | //! 4 | //! ref: 5 | //! 6 | //! somerc: "Swiss. Obl. Mercator" "\n\tCyl, Ell\n\tFor CH1903"; 7 | //! 8 | use crate::errors::{Error, Result}; 9 | use crate::math::{ 10 | aasin, 11 | consts::{EPS_10, FRAC_PI_2, FRAC_PI_4}, 12 | }; 13 | use crate::parameters::ParamList; 14 | use crate::proj::ProjData; 15 | 16 | // Projection stub 17 | super::projection! { somerc } 18 | 19 | #[derive(Debug, Clone)] 20 | pub(crate) struct Projection { 21 | e: f64, 22 | rone_es: f64, 23 | k: f64, 24 | c: f64, 25 | hlf_e: f64, 26 | k_r: f64, 27 | cosp0: f64, 28 | sinp0: f64, 29 | } 30 | 31 | #[allow(non_snake_case)] 32 | impl Projection { 33 | pub fn somerc(p: &mut ProjData, _: &ParamList) -> Result { 34 | let el = &p.ellps; 35 | let hlf_e = 0.5 * el.e; 36 | 37 | let (sinphi, cosphi) = p.phi0.sin_cos(); 38 | 39 | let cp = cosphi * cosphi; 40 | let c = (1. + el.es * cp * cp * el.rone_es).sqrt(); 41 | let sinp0 = sinphi / c; 42 | let phip0 = aasin(sinp0)?; 43 | let cosp0 = phip0.cos(); 44 | let sp = sinphi * el.e; 45 | let k = (FRAC_PI_4 + 0.5 * phip0).tan().ln() 46 | - c * ((FRAC_PI_4 + 0.5 * p.phi0).tan().ln() - hlf_e * ((1. + sp) / (1. - sp)).ln()); 47 | let k_r = p.k0 * el.one_es.sqrt() / (1. - sp * sp); 48 | Ok(Self { 49 | e: el.e, 50 | rone_es: el.rone_es, 51 | k, 52 | c, 53 | hlf_e, 54 | k_r, 55 | cosp0, 56 | sinp0, 57 | }) 58 | } 59 | 60 | #[inline(always)] 61 | pub fn forward(&self, lam: f64, phi: f64, z: f64) -> Result<(f64, f64, f64)> { 62 | let sp = self.e * phi.sin(); 63 | let phip = 2. 64 | * ((self.c 65 | * ((FRAC_PI_4 + 0.5 * phi).tan().ln() 66 | - self.hlf_e * ((1. + sp) / (1. - sp)).ln()) 67 | + self.k) 68 | .exp()) 69 | .atan() 70 | - FRAC_PI_2; 71 | 72 | let lamp = self.c * lam; 73 | let cp = phip.cos(); 74 | let phipp = aasin(self.cosp0 * phip.sin() - self.sinp0 * cp * lamp.cos())?; 75 | let lampp = aasin(cp * lamp.sin() / phipp.cos())?; 76 | 77 | Ok(( 78 | self.k_r * lampp, 79 | self.k_r * (FRAC_PI_4 + 0.5 * phipp).tan().ln(), 80 | z, 81 | )) 82 | } 83 | 84 | #[inline(always)] 85 | pub fn inverse(&self, x: f64, y: f64, z: f64) -> Result<(f64, f64, f64)> { 86 | const NITER: isize = 6; 87 | 88 | let phipp = 2. * (((y / self.k_r).exp()).atan() - FRAC_PI_4); 89 | let lampp = x / self.k_r; 90 | let cp = phipp.cos(); 91 | let mut phip = aasin(self.cosp0 * phipp.sin() + self.sinp0 * cp * lampp.cos())?; 92 | let lamp = aasin(cp * lampp.sin() / phip.cos())?; 93 | let con = (self.k - (FRAC_PI_4 + 0.5 * phip).tan().ln()) / self.c; 94 | 95 | let mut i = NITER; 96 | while i > 0 { 97 | let esp = self.e * phip.sin(); 98 | let delp = (con + (FRAC_PI_4 + 0.5 * phip).tan().ln() 99 | - self.hlf_e * ((1. + esp) / (1. - esp)).ln()) 100 | * (1. - esp * esp) 101 | * phip.cos() 102 | * self.rone_es; 103 | phip -= delp; 104 | if delp.abs() < EPS_10 { 105 | break; 106 | } 107 | i -= 1; 108 | } 109 | if i <= 0 { 110 | Err(Error::ToleranceConditionError) 111 | } else { 112 | Ok((lamp / self.c, phip, z)) 113 | } 114 | } 115 | 116 | pub const fn has_inverse() -> bool { 117 | true 118 | } 119 | 120 | pub const fn has_forward() -> bool { 121 | true 122 | } 123 | } 124 | 125 | #[cfg(test)] 126 | mod tests { 127 | use crate::math::consts::EPS_10; 128 | use crate::proj::Proj; 129 | use crate::tests::utils::{test_proj_forward, test_proj_inverse}; 130 | 131 | #[test] 132 | fn proj_somerc_el() { 133 | let p = Proj::from_proj_string("+proj=somerc +ellps=GRS80").unwrap(); 134 | 135 | println!("{:#?}", p.projection()); 136 | 137 | let inputs = [ 138 | ((2., 1., 0.), (222638.98158654713, 110579.96521824898, 0.)), 139 | ((2., -1., 0.), (222638.98158654713, -110579.96521825089, 0.)), 140 | ((-2., 1., 0.), (-222638.98158654713, 110579.96521824898, 0.)), 141 | ( 142 | (-2., -1., 0.), 143 | (-222638.98158654713, -110579.96521825089, 0.), 144 | ), 145 | ]; 146 | 147 | test_proj_forward(&p, &inputs, EPS_10); 148 | test_proj_inverse(&p, &inputs, EPS_10); 149 | } 150 | 151 | #[test] 152 | fn proj_somerc_sp() { 153 | let p = Proj::from_proj_string("+proj=somerc +a=6400000").unwrap(); 154 | 155 | println!("{:#?}", p.projection()); 156 | 157 | let inputs = [ 158 | ((2., 1., 0.), (223402.14425527418, 111706.74357494408, 0.)), 159 | ((2., -1., 0.), (223402.14425527418, -111706.74357494518, 0.)), 160 | ((-2., 1., 0.), (-223402.14425527418, 111706.74357494408, 0.)), 161 | ( 162 | (-2., -1., 0.), 163 | (-223402.14425527418, -111706.74357494518, 0.), 164 | ), 165 | ]; 166 | 167 | test_proj_forward(&p, &inputs, EPS_10); 168 | test_proj_inverse(&p, &inputs, EPS_10); 169 | } 170 | } 171 | -------------------------------------------------------------------------------- /proj4rs/src/datums.rs: -------------------------------------------------------------------------------- 1 | //! 2 | //! Proj4 datum definitions 3 | //! 4 | use crate::ellipsoids::{constants as ellps, EllipsoidDefn}; 5 | 6 | /// Shift method is either 7 | /// defined by Helmert transforms or nadgrids 8 | pub enum DatumParamDefn { 9 | ToWGS84_0, 10 | ToWGS84_3(f64, f64, f64), 11 | ToWGS84_7(f64, f64, f64, f64, f64, f64, f64), 12 | NadGrids(&'static str), 13 | } 14 | 15 | pub struct DatumDefn { 16 | pub id: &'static str, 17 | pub params: DatumParamDefn, 18 | pub ellps: &'static EllipsoidDefn, 19 | //pub comment: &'static str, 20 | } 21 | 22 | //#[rustfmt::skip] 23 | pub mod constants { 24 | use super::*; 25 | 26 | macro_rules! nadgrids { 27 | ($grids:expr) => { 28 | DatumParamDefn::NadGrids($grids) 29 | }; 30 | } 31 | 32 | macro_rules! towgs84 { 33 | ($x:expr, $y:expr, $z:expr) => { 34 | DatumParamDefn::ToWGS84_3($x, $y, $z) 35 | }; 36 | ($x:expr, $y:expr, $z:expr, $rx:expr, $ry:expr, $rz:expr, $s:expr) => { 37 | DatumParamDefn::ToWGS84_7($x, $y, $z, $rx, $ry, $rz, $s) 38 | }; 39 | () => { 40 | DatumParamDefn::ToWGS84_0 41 | }; 42 | } 43 | 44 | macro_rules! datum { 45 | ($name:ident, $id:expr, $params:expr, $ellps:ident, $c:expr $(,)?) => { 46 | pub(crate) const $name: DatumDefn = DatumDefn { 47 | id: $id, 48 | params: $params, 49 | ellps: &ellps::$ellps, 50 | //comment: $c, 51 | }; 52 | }; 53 | } 54 | 55 | // --------------------------- 56 | // 57 | // Datum definitions 58 | // 59 | // --------------------------- 60 | datum!(WGS84, "WGS84", towgs84!(), WGS84, ""); 61 | datum!( 62 | GGRS87, 63 | "GGRS87", 64 | towgs84!(-199.87, 74.79, 246.62), 65 | GRS80, 66 | "Greek_Geodetic_Reference_System_1987", 67 | ); 68 | datum!( 69 | NAD83, 70 | "NAD83", 71 | towgs84!(), 72 | GRS80, 73 | "North_American_Datum_1983" 74 | ); 75 | datum!( 76 | NAD27, 77 | "NAD27", 78 | nadgrids!("@conus,@alaska,@ntv2_0.gsb,@ntv1_can.dat"), 79 | CLRK66, 80 | "North_American_Datum_1927", 81 | ); 82 | // defn is "nadgrids=@BETA2007.gsb" in proj 9 83 | datum!( 84 | POTSDAM, 85 | "potsdam", 86 | towgs84!(598.1, 73.7, 418.2, 0.202, 0.045, -2.455, 6.7), 87 | BESSEL, 88 | "Potsdam Rauenberg 1950 DHDN", 89 | ); 90 | datum!( 91 | CARTHAGE, 92 | "carthage", 93 | towgs84!(-263.0, 6.0, 431.0), 94 | CLRK80IGN, 95 | "Carthage 1934 Tunisia", 96 | ); 97 | datum!( 98 | HERMANNSKOGEL, 99 | "hermannskogel", 100 | towgs84!(577.326, 90.129, 463.919, 5.137, 1.474, 5.297, 2.4232), 101 | BESSEL, 102 | "Hermannskogel", 103 | ); 104 | datum!( 105 | IRE65, 106 | "ire65", 107 | towgs84!(482.530, -130.596, 564.557, -1.042, -0.214, -0.631, 8.15), 108 | MOD_AIRY, 109 | "Ireland 1965", 110 | ); 111 | datum!( 112 | NZGD49, 113 | "nzgd49", 114 | towgs84!(59.47, -5.04, 187.44, 0.47, -0.1, 1.024, -4.5993), 115 | INTL, 116 | "New Zealand Geodetic Datum 1949", 117 | ); 118 | datum!( 119 | OSGB36, 120 | "OSGB36", 121 | towgs84!(446.448, -125.157, 542.060, 0.1502, 0.2470, 0.8421, -20.4894), 122 | AIRY, 123 | "Airy 1830", 124 | ); 125 | // Added from proj4js 126 | datum!( 127 | CH1903, 128 | "ch1903", 129 | towgs84!(674.374, 15.056, 405.346), 130 | BESSEL, 131 | "swiss", 132 | ); 133 | datum!( 134 | OSNI52, 135 | "osni52", 136 | towgs84!(482.530, -130.596, 564.557, -1.042, -0.214, -0.631, 8.15), 137 | AIRY, 138 | "Irish National", 139 | ); 140 | datum!( 141 | RASSADIRAN, 142 | "rassadiran", 143 | towgs84!(-133.63, -157.5, -158.62), 144 | INTL, 145 | "Rassadiran", 146 | ); 147 | datum!( 148 | S_JTSK, 149 | "s_jtsk", 150 | towgs84!(589., 76., 480.), 151 | BESSEL, 152 | "S-JTSK (Ferro)", 153 | ); 154 | datum!( 155 | BEDUARAM, 156 | "beduaram", 157 | towgs84!(-106., -87., 188.), 158 | CLRK80, 159 | "Beduaram", 160 | ); 161 | datum!( 162 | GUNUNG_SEGARA, 163 | "gunung_segara", 164 | towgs84!(-403., 684., 41.), 165 | BESSEL, 166 | "Gunung Segara Jakarta", 167 | ); 168 | datum!( 169 | RNB72, 170 | "rnb72", 171 | towgs84!(106.869, -52.2978, 103.724, -0.33657, 0.456955, -1.84218, 1.), 172 | INTL, 173 | "Reseau National Belge 1972", 174 | ); 175 | 176 | /// Static datums table 177 | pub(super) const DATUMS: [&DatumDefn; 17] = [ 178 | &WGS84, 179 | &GGRS87, 180 | &NAD83, 181 | &NAD27, 182 | &POTSDAM, 183 | &CARTHAGE, 184 | &HERMANNSKOGEL, 185 | &IRE65, 186 | &NZGD49, 187 | &OSGB36, 188 | &CH1903, 189 | &OSNI52, 190 | &RASSADIRAN, 191 | &S_JTSK, 192 | &BEDUARAM, 193 | &GUNUNG_SEGARA, 194 | &RNB72, 195 | ]; 196 | } 197 | 198 | /// Return the datum definition 199 | pub fn find_datum(name: &str) -> Option<&DatumDefn> { 200 | constants::DATUMS 201 | .iter() 202 | .find(|d| d.id.eq_ignore_ascii_case(name)) 203 | .copied() 204 | } 205 | -------------------------------------------------------------------------------- /proj4rs/src/projections/cea.rs: -------------------------------------------------------------------------------- 1 | //! 2 | //! Equal Area Cylindrical Cylindrical (Spherical) 3 | //! 4 | //! Parameters: 5 | //! 6 | //! * lat_ts: 7 | //! 8 | //! See https://proj.org/en/stable/operations/projections/cea.html 9 | //! 10 | 11 | use crate::errors::{Error, Result}; 12 | use crate::math::consts::{EPS_10, FRAC_PI_2}; 13 | use crate::math::{authlat, authset, qsfn}; 14 | use crate::parameters::ParamList; 15 | use crate::proj::ProjData; 16 | 17 | super::projection! { cea } 18 | 19 | #[derive(Debug, Clone)] 20 | pub(crate) enum Projection { 21 | Sph { 22 | k0: f64, 23 | }, 24 | Ell { 25 | k0: f64, 26 | e: f64, 27 | one_es: f64, 28 | qp: f64, 29 | apa: (f64, f64, f64), 30 | }, 31 | } 32 | 33 | impl Projection { 34 | pub fn cea(p: &mut ProjData, params: &ParamList) -> Result { 35 | let (mut k0, t) = match params.try_angular_value("lat_ts")? { 36 | Some(t) => { 37 | let k0 = t.cos(); 38 | if k0 < 0. { 39 | return Err(Error::InvalidParameterValue( 40 | "Invalid value for lat_ts: |lat_ts| should be <= 90\u{00b0}", 41 | )); 42 | } 43 | (k0, t) 44 | } 45 | None => (p.k0, 0.0), 46 | }; 47 | 48 | Ok(if p.ellps.is_ellipsoid() { 49 | let sint = t.sin(); 50 | k0 /= (1. - p.ellps.es * sint * sint).sqrt(); 51 | Self::Ell { 52 | k0, 53 | e: p.ellps.e, 54 | one_es: p.ellps.one_es, 55 | qp: qsfn(1., p.ellps.e, p.ellps.one_es), 56 | apa: authset(p.ellps.es), 57 | } 58 | } else { 59 | Self::Sph { k0 } 60 | }) 61 | } 62 | 63 | pub fn forward(&self, lam: f64, phi: f64, z: f64) -> Result<(f64, f64, f64)> { 64 | match self { 65 | Self::Ell { k0, e, one_es, .. } => { 66 | Ok((k0 * lam, 0.5 * qsfn(phi.sin(), *e, *one_es) / k0, z)) 67 | } 68 | Self::Sph { k0 } => Ok((k0 * lam, phi.sin() / k0, z)), 69 | } 70 | } 71 | 72 | pub fn inverse(&self, x: f64, y: f64, z: f64) -> Result<(f64, f64, f64)> { 73 | match self { 74 | Self::Ell { k0, qp, apa, .. } => { 75 | Ok((x / k0, authlat((2. * y * k0 / qp).asin(), *apa), z)) 76 | } 77 | Self::Sph { k0 } => { 78 | let y = y * k0; 79 | let t = y.abs(); 80 | if t - EPS_10 > 1. { 81 | Err(Error::CoordTransOutsideProjectionDomain) 82 | } else { 83 | let phi = if t >= 1. { 84 | if y < 0. { 85 | -FRAC_PI_2 86 | } else { 87 | FRAC_PI_2 88 | } 89 | } else { 90 | y.asin() 91 | }; 92 | Ok((x / k0, phi, z)) 93 | } 94 | } 95 | } 96 | } 97 | 98 | pub const fn has_inverse() -> bool { 99 | true 100 | } 101 | 102 | pub const fn has_forward() -> bool { 103 | true 104 | } 105 | } 106 | 107 | //============ 108 | // Tests 109 | //============ 110 | 111 | #[cfg(test)] 112 | mod tests { 113 | use crate::proj::Proj; 114 | use crate::tests::utils::{test_proj_forward, test_proj_inverse}; 115 | 116 | // lat_ts_0: Lambert cylindrical equal-area 117 | 118 | #[test] 119 | fn proj_cea_lat_ts_0_e() { 120 | let p = Proj::from_proj_string("+proj=cea +ellps=GRS80").unwrap(); 121 | 122 | // NOTE proj 9 use GRS80 as default ellipsoid 123 | 124 | println!("{:#?}", p.projection()); 125 | 126 | let inputs = [( 127 | (12.09, 47.73, 0.), 128 | (1345852.643690677360, 4699614.507911851630, 0.), 129 | )]; 130 | 131 | test_proj_forward(&p, &inputs, 1e-8); 132 | test_proj_inverse(&p, &inputs, 1e-8); 133 | } 134 | 135 | #[test] 136 | fn proj_cea_lat_ts_0_s() { 137 | let p = Proj::from_proj_string("+proj=cea +R_a +ellps=GRS80").unwrap(); 138 | 139 | println!("{:#?}", p.projection()); 140 | 141 | let inputs = [( 142 | (12.09, 47.73, 0.), 143 | (1343596.449131145841, 4711803.232695742510, 0.), 144 | )]; 145 | 146 | test_proj_forward(&p, &inputs, 1e-8); 147 | test_proj_inverse(&p, &inputs, 1e-8); 148 | } 149 | 150 | // lat_ts_30: Berhmann 151 | 152 | #[test] 153 | fn proj_cea_lat_ts_30_e() { 154 | let p = Proj::from_proj_string("+proj=cea +lat_ts=30 +ellps=GRS80").unwrap(); 155 | 156 | // NOTE proj 9 use GRS80 as default ellipsoid 157 | 158 | println!("{:#?}", p.projection()); 159 | 160 | let inputs = [( 161 | (12.09, 47.73, 0.), 162 | (1166519.128238123609, 5422104.495923101902, 0.), 163 | )]; 164 | 165 | test_proj_forward(&p, &inputs, 1e-8); 166 | test_proj_inverse(&p, &inputs, 1e-8); 167 | } 168 | 169 | #[test] 170 | fn proj_cea_lat_ts_30_s() { 171 | let p = Proj::from_proj_string("+proj=cea +lat_ts=30 +R_a +ellps=GRS80").unwrap(); 172 | 173 | println!("{:#?}", p.projection()); 174 | 175 | let inputs = [( 176 | (12.09, 47.73, 0.), 177 | (1163588.657382138772, 5440721.729530871846, 0.), 178 | )]; 179 | 180 | test_proj_forward(&p, &inputs, 1e-8); 181 | test_proj_inverse(&p, &inputs, 1e-8); 182 | } 183 | } 184 | -------------------------------------------------------------------------------- /proj4rs/src/projections/moll.rs: -------------------------------------------------------------------------------- 1 | //! 2 | //! Mollweide Pseudocylindrical and derivatives 3 | //! 4 | //! ref: 5 | //! 6 | //! moll: "Mollweide" "\n\tPCyl., Sph."; 7 | //! wag4: "Wagner IV" "\n\tPCyl., Sph."; 8 | //! wag5: "Wagner V" "\n\tPCyl., Sph."; 9 | //! 10 | use crate::ellps::Ellipsoid; 11 | #[allow(unused_imports)] 12 | use crate::errors::{Error, Result}; 13 | use crate::math::{ 14 | aasin, 15 | consts::{FRAC_PI_2, PI, TAU}, 16 | }; 17 | use crate::parameters::ParamList; 18 | use crate::proj::ProjData; 19 | 20 | // Projection stub 21 | super::projection! { moll, wag4, wag5 } 22 | 23 | #[derive(Debug, Clone)] 24 | pub(crate) struct Projection { 25 | c_x: f64, 26 | c_y: f64, 27 | c_p: f64, 28 | } 29 | 30 | impl Projection { 31 | pub fn moll(p: &mut ProjData, _: &ParamList) -> Result { 32 | Self::new(p, FRAC_PI_2) 33 | } 34 | 35 | pub fn wag4(p: &mut ProjData, _: &ParamList) -> Result { 36 | Self::new(p, PI / 3.) 37 | } 38 | 39 | pub fn wag5(p: &mut ProjData, _: &ParamList) -> Result { 40 | // Map from sphere 41 | p.ellps = Ellipsoid::sphere(p.ellps.a)?; 42 | 43 | Ok(Self { 44 | c_x: 0.90977, 45 | c_y: 1.65014, 46 | c_p: 3.00896, 47 | }) 48 | } 49 | 50 | fn new(p: &mut ProjData, pp: f64) -> Result { 51 | // Map from sphere 52 | p.ellps = Ellipsoid::sphere(p.ellps.a)?; 53 | 54 | let p2 = pp + pp; 55 | let sp = pp.sin(); 56 | let c_p = p2 + p2.sin(); 57 | let r = (TAU * sp / c_p).sqrt(); 58 | 59 | Ok(Self { 60 | c_x: 2. * r / PI, 61 | c_y: r / sp, 62 | c_p, 63 | }) 64 | } 65 | 66 | #[inline(always)] 67 | pub fn forward(&self, lam: f64, mut phi: f64, z: f64) -> Result<(f64, f64, f64)> { 68 | const NITER: isize = 10; 69 | const TOL: f64 = 1e-7; 70 | 71 | let k = self.c_p * phi.sin(); 72 | let mut i = NITER; 73 | while i > 0 { 74 | let v = (phi + phi.sin() - k) / (1. + phi.cos()); 75 | phi -= v; 76 | if v.abs() < TOL { 77 | break; 78 | } 79 | i -= 1; 80 | } 81 | if i == 0 { 82 | phi = FRAC_PI_2 * phi.signum(); 83 | } else { 84 | phi *= 0.5; 85 | } 86 | Ok((self.c_x * lam * phi.cos(), self.c_y * phi.sin(), z)) 87 | } 88 | 89 | #[cfg(not(feature = "proj4js-compat"))] 90 | #[inline(always)] 91 | pub fn inverse(&self, x: f64, y: f64, z: f64) -> Result<(f64, f64, f64)> { 92 | let mut phi = aasin(y / self.c_y)?; 93 | let lam = x / (self.c_x * phi.cos()); 94 | if lam.abs() < PI { 95 | phi += phi; 96 | phi = aasin((phi + phi.sin()) / self.c_p)?; 97 | Ok((lam, phi, z)) 98 | } else { 99 | Err(Error::CoordinateOutOfRange) 100 | } 101 | } 102 | 103 | /// This is an infaillible version of Mollwey 104 | /// While proj version is faillible 105 | #[cfg(feature = "proj4js-compat")] 106 | #[inline(always)] 107 | pub fn inverse(&self, x: f64, y: f64, z: f64) -> Result<(f64, f64, f64)> { 108 | let mut phi = aasin(y / self.c_y)?; 109 | let mut lam = x / (self.c_x * phi.cos()); 110 | if lam.abs() > PI { 111 | lam = PI * lam.signum(); 112 | } 113 | phi += phi; 114 | phi = aasin((phi + phi.sin()) / self.c_p)?; 115 | Ok((lam, phi, z)) 116 | } 117 | 118 | pub const fn has_inverse() -> bool { 119 | true 120 | } 121 | 122 | pub const fn has_forward() -> bool { 123 | true 124 | } 125 | } 126 | 127 | #[cfg(test)] 128 | mod tests { 129 | use crate::math::consts::EPS_10; 130 | use crate::proj::Proj; 131 | use crate::tests::utils::{test_proj_forward, test_proj_inverse}; 132 | 133 | #[test] 134 | fn proj_moll() { 135 | let p = Proj::from_proj_string("+proj=moll").unwrap(); 136 | 137 | println!("{:#?}", p.projection()); 138 | 139 | let inputs = [ 140 | ((2., 1., 0.), (200426.67539284358, 123642.46137843542, 0.)), 141 | ((2., -1., 0.), (200426.67539284358, -123642.46137843542, 0.)), 142 | ((-2., 1., 0.), (-200426.67539284358, 123642.46137843542, 0.)), 143 | ( 144 | (-2., -1., 0.), 145 | (-200426.67539284358, -123642.46137843542, 0.), 146 | ), 147 | ]; 148 | 149 | test_proj_forward(&p, &inputs, EPS_10); 150 | test_proj_inverse(&p, &inputs, EPS_10); 151 | } 152 | 153 | #[test] 154 | fn proj_wag4() { 155 | let p = Proj::from_proj_string("+proj=wag4").unwrap(); 156 | 157 | println!("{:#?}", p.projection()); 158 | 159 | let inputs = [ 160 | ((2., 1., 0.), (192142.59162431932, 128974.11846682805, 0.)), 161 | ((2., -1., 0.), (192142.59162431932, -128974.11846682805, 0.)), 162 | ((-2., 1., 0.), (-192142.59162431932, 128974.11846682805, 0.)), 163 | ( 164 | (-2., -1., 0.), 165 | (-192142.59162431932, -128974.11846682805, 0.), 166 | ), 167 | ]; 168 | 169 | test_proj_forward(&p, &inputs, EPS_10); 170 | test_proj_inverse(&p, &inputs, EPS_10); 171 | } 172 | 173 | #[test] 174 | fn proj_wag5() { 175 | let p = Proj::from_proj_string("+proj=wag5").unwrap(); 176 | 177 | println!("{:#?}", p.projection()); 178 | 179 | let inputs = [ 180 | ((2., 1., 0.), (202532.80926341165, 138177.98447111444, 0.)), 181 | ((2., -1., 0.), (202532.80926341165, -138177.98447111444, 0.)), 182 | ((-2., 1., 0.), (-202532.80926341165, 138177.98447111444, 0.)), 183 | ( 184 | (-2., -1., 0.), 185 | (-202532.80926341165, -138177.98447111444, 0.), 186 | ), 187 | ]; 188 | 189 | test_proj_forward(&p, &inputs, EPS_10); 190 | test_proj_inverse(&p, &inputs, EPS_10); 191 | } 192 | } 193 | -------------------------------------------------------------------------------- /js/ol-proj4rs-demo-app/vector-projections.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Vector Projections 6 | 7 | 8 | 9 | 10 | 11 |
OpenLayers + Proj4rs
12 |

Vector Projections

13 |
14 |
15 | 16 | 17 | 27 | 28 |
29 |

30 | This example shows a GeoJSON layer that is well converted between various projections. 31 |

32 | 33 |
34 |
35 |             

proj4 OpenLayers

36 | 37 | import Map from 'ol/Map.js';import TileGrid from 'ol/tilegrid/TileGrid.js'; 38 | import View from 'ol/View.js'; 39 | import proj4 from 'proj4'; 40 | import {getCenter} from 'ol/extent.js'; 41 | import {get as getProjection} from 'ol/proj.js'; 42 | import {register} from 'ol/proj/proj4.js'; 43 | import GeoJSON from 'ol/format/GeoJSON.js'; 44 | import Graticule from 'ol/layer/Graticule.js'; 45 | import VectorLayer from 'ol/layer/Vector.js'; 46 | import VectorSource from 'ol/source/Vector.js'; 47 | import {Fill, Style} from 'ol/style.js'; 48 | ... 49 |
50 |
51 |             

proj4rs

52 | 53 | import Map from 'ol/Map.js';import TileGrid from 'ol/tilegrid/TileGrid.js'; 54 | import View from 'ol/View.js'; 55 | import {proj4} from 'proj4rs/proj4.js'; 56 | import {getCenter} from 'ol/extent.js'; 57 | import {get as getProjection} from 'ol/proj.js'; 58 | import {register} from 'ol/proj/proj4.js'; 59 | import GeoJSON from 'ol/format/GeoJSON.js'; 60 | import Graticule from 'ol/layer/Graticule.js'; 61 | import VectorLayer from 'ol/layer/Vector.js'; 62 | import VectorSource from 'ol/source/Vector.js'; 63 | import {Fill, Style} from 'ol/style.js'; 64 | ... 65 |
66 |
67 | 68 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | -------------------------------------------------------------------------------- /proj4rs/src/nadgrids/files/ntv2.rs: -------------------------------------------------------------------------------- 1 | //! 2 | //! Nadgrid parser 3 | //! 4 | use crate::errors::{Error, Result}; 5 | use crate::log::trace; 6 | use crate::math::consts::SEC_TO_RAD; 7 | use crate::nadgrids::grid::{Grid, GridId, Lp, REL_TOLERANCE_HGRIDSHIFT}; 8 | use crate::nadgrids::header::error_str::*; 9 | use crate::nadgrids::header::{Endianness, Header}; 10 | use crate::nadgrids::Catalog; 11 | use std::io::Read; 12 | 13 | const NTV2_HEADER_SIZE: usize = 11 * 16; 14 | 15 | /// Ntv2 reader 16 | pub(super) fn read_ntv2(catalog: &Catalog, key: &str, read: &mut R) -> Result<()> { 17 | let mut head = Header::::new(); 18 | 19 | trace!("Reading ntv2 {}", key); 20 | 21 | // Read overview header 22 | head.read(read)?; 23 | // Check endianness 24 | head.endian = if head.get_u8(8) == 11 { 25 | Endianness::native() 26 | } else { 27 | Endianness::other() 28 | }; 29 | 30 | let nsubgrids = head.get_u32(40) as usize; 31 | 32 | trace!("Reading ntv2 {} subgrids {}", key, nsubgrids); 33 | 34 | // Read subsequent grids 35 | (0..nsubgrids).try_for_each(|_| read_ntv2_grid(catalog, key, head.read(read)?, read)) 36 | } 37 | 38 | /// Read ntv2 grid data 39 | fn read_ntv2_grid( 40 | catalog: &Catalog, 41 | key: &str, 42 | head: &Header, 43 | read: &mut R, 44 | ) -> Result<()> { 45 | match head.get_str(0, 8) { 46 | Ok("SUB_NAME") => Ok(()), 47 | _ => Err(Error::InvalidNtv2GridFormat(ERR_INVALID_HEADER)), 48 | }?; 49 | 50 | let id = head.get_id(8); 51 | let mut lineage = head.get_id(24); 52 | if lineage.as_str().trim() == "NONE" { 53 | lineage = GridId::root(); 54 | } 55 | 56 | let mut ll = Lp { 57 | lam: -head.get_f64(120), // W_LONG 58 | phi: head.get_f64(72), // S_LAT 59 | }; 60 | 61 | let mut ur = Lp { 62 | lam: -head.get_f64(104), // E_LONG 63 | phi: head.get_f64(88), // N_LAT 64 | }; 65 | 66 | let mut del = Lp { 67 | lam: head.get_f64(152), // longitude interval 68 | phi: head.get_f64(136), // latitude interval 69 | }; 70 | 71 | let lim = Lp { 72 | lam: (((ur.lam - ll.lam).abs() / del.lam + 0.5) + 1.).floor(), 73 | phi: (((ur.phi - ll.phi).abs() / del.phi + 0.5) + 1.).floor(), 74 | }; 75 | 76 | // units are in seconds of degree. 77 | ll.lam *= SEC_TO_RAD; 78 | ll.phi *= SEC_TO_RAD; 79 | ur.lam *= SEC_TO_RAD; 80 | ur.phi *= SEC_TO_RAD; 81 | del.lam *= SEC_TO_RAD; 82 | del.phi *= SEC_TO_RAD; 83 | 84 | // Read matrix data 85 | let nrows = lim.phi as usize; 86 | let rowsize = lim.lam as usize; 87 | 88 | let gs_count = head.get_u32(168) as usize; 89 | if gs_count != nrows * rowsize { 90 | return Err(Error::InvalidNtv2GridFormat(ERR_GSCOUNT_NOT_MATCHING)); 91 | } 92 | 93 | // Read grid data 94 | trace!( 95 | "Reading data for grid {}:{}:{}", 96 | key, 97 | id.as_str(), 98 | lineage.as_str() 99 | ); 100 | 101 | let mut buf = head.rebind::<16>(); 102 | let mut cvs: Vec = (0..gs_count) 103 | .map(|_| { 104 | buf.read(read)?; 105 | // NOTE: phi and lam are inverted 106 | Ok(Lp { 107 | phi: SEC_TO_RAD * (buf.get_f32(0) as f64), 108 | lam: -(SEC_TO_RAD * (buf.get_f32(4) as f64)), // NOTE: Compensate NT convention 109 | }) 110 | }) 111 | .collect::>>()?; 112 | 113 | // See https://geodesie.ign.fr/contenu/fichiers/documentation/algorithmes/notice/NT111_V1_HARMEL_TransfoNTF-RGF93_FormatGrilleNTV2.pdf 114 | 115 | // In proj4, rows are stored in reverse order 116 | for i in 0..nrows { 117 | let offs = i * rowsize; 118 | cvs[offs..(offs + rowsize)].reverse(); 119 | } 120 | 121 | let epsilon = (del.lam.abs() + del.phi.abs()) * REL_TOLERANCE_HGRIDSHIFT; 122 | 123 | catalog.add_grid( 124 | key.into(), 125 | Grid { 126 | id, 127 | lineage, 128 | ll, 129 | ur, 130 | del, 131 | lim, 132 | epsilon, 133 | cvs: cvs.into_boxed_slice(), 134 | }, 135 | ) 136 | } 137 | 138 | #[cfg(test)] 139 | mod tests { 140 | use super::*; 141 | use crate::nadgrids::Catalog; 142 | use crate::tests::setup; 143 | use std::env; 144 | use std::fs::File; 145 | use std::io::BufReader; 146 | use std::path::Path; 147 | 148 | macro_rules! fixture { 149 | ($name:expr) => { 150 | Path::new(&env::var("CARGO_MANIFEST_DIR").unwrap()) 151 | .join("fixtures") 152 | .as_path() 153 | .join($name) 154 | .as_path() 155 | }; 156 | } 157 | 158 | macro_rules! load_ntv2 { 159 | ($cat:expr, $name:expr) => { 160 | // Use a BufReader or efficiency 161 | let file = File::open(fixture!($name)).unwrap(); 162 | let mut read = BufReader::new(file); 163 | read_ntv2($cat, $name, &mut read).unwrap(); 164 | }; 165 | } 166 | 167 | #[test] 168 | fn ntv2_100800401_gsb() { 169 | setup(); 170 | 171 | let catalog = Catalog::default(); 172 | load_ntv2!(&catalog, "100800401.gsb"); 173 | 174 | let grids = catalog.find("100800401.gsb").unwrap().collect::>(); 175 | assert_eq!(grids.len(), 1); 176 | 177 | let grid = grids[0]; 178 | assert!(grid.is_root()); 179 | assert_eq!(grid.id.as_str(), "0INT2GRS"); 180 | assert_eq!(grid.cvs.len(), 1591); 181 | } 182 | 183 | #[test] 184 | #[cfg(feature = "local_tests")] 185 | fn ntv2_bwta2017_gsb() { 186 | setup(); 187 | 188 | let catalog = Catalog::default(); 189 | load_ntv2!(&catalog, "BWTA2017.gsb"); 190 | 191 | let grids = catalog.find("BWTA2017.gsb").unwrap().collect::>(); 192 | assert_eq!(grids.len(), 1); 193 | 194 | let grid = grids[0]; 195 | assert!(grid.is_root()); 196 | assert_eq!(grid.id.as_str(), "DHDN90 "); 197 | assert_eq!(grid.cvs.len(), 24514459); 198 | } 199 | } 200 | -------------------------------------------------------------------------------- /proj4rs/src/projections/merc.rs: -------------------------------------------------------------------------------- 1 | //! 2 | //! Pseudo Mercator 3 | //! 4 | //! merc: "Mercator" "\n\tCyl, Sph&Ell\n\tlat_ts=" 5 | //! webmerc: "Web Mercator / Pseudo Mercator" "\n\tCyl, Ell\n\t" 6 | //! 7 | 8 | // Projection stub 9 | super::projection! { merc, webmerc } 10 | 11 | use crate::errors::{Error, Result}; 12 | use crate::math::{ 13 | asinh, 14 | consts::{EPS_10, FRAC_PI_2}, 15 | msfn, phi2, 16 | }; 17 | use crate::parameters::ParamList; 18 | use crate::proj::ProjData; 19 | 20 | #[derive(Debug, Clone)] 21 | pub(crate) struct Projection { 22 | is_ellps: bool, 23 | k0: f64, 24 | e: f64, 25 | } 26 | 27 | impl Projection { 28 | pub fn merc(p: &mut ProjData, params: &ParamList) -> Result { 29 | let phits: Option = params.try_angular_value("lat_ts")?; 30 | if let Some(phits) = phits { 31 | if phits >= FRAC_PI_2 { 32 | return Err(Error::InvalidParameterValue( 33 | "lat_ts larger than 90 degrees", 34 | )); 35 | } 36 | } 37 | 38 | if p.ellps.is_ellipsoid() { 39 | if let Some(phits) = phits { 40 | p.k0 = msfn(phits.sin(), phits.cos(), p.ellps.es); 41 | } 42 | } else if let Some(phits) = phits { 43 | p.k0 = phits.cos(); 44 | } 45 | 46 | Ok(Self { 47 | is_ellps: p.ellps.is_ellipsoid(), 48 | k0: p.k0, 49 | e: p.ellps.e, 50 | }) 51 | } 52 | 53 | pub fn webmerc(p: &mut ProjData, _params: &ParamList) -> Result { 54 | p.k0 = 1.0; 55 | Ok(Self { 56 | is_ellps: false, 57 | k0: p.k0, 58 | e: p.ellps.e, 59 | }) 60 | } 61 | 62 | pub fn forward(&self, lam: f64, phi: f64, z: f64) -> Result<(f64, f64, f64)> { 63 | if (phi.abs() - FRAC_PI_2).abs() <= EPS_10 { 64 | return Err(Error::ToleranceConditionError); 65 | } 66 | if self.is_ellps { 67 | let (sphi, cphi) = phi.sin_cos(); 68 | Ok(( 69 | self.k0 * lam, 70 | self.k0 * (asinh(sphi / cphi) - self.e * (self.e * sphi).atanh()), 71 | z, 72 | )) 73 | } else { 74 | Ok((self.k0 * lam, self.k0 * asinh(phi.tan()), z)) 75 | } 76 | } 77 | 78 | pub fn inverse(&self, x: f64, y: f64, z: f64) -> Result<(f64, f64, f64)> { 79 | if self.is_ellps { 80 | Ok((x / self.k0, phi2((-y / self.k0).exp(), self.e)?, z)) 81 | } else { 82 | Ok((x / self.k0, (y / self.k0).sinh().atan(), z)) 83 | } 84 | } 85 | 86 | pub const fn has_inverse() -> bool { 87 | true 88 | } 89 | 90 | pub const fn has_forward() -> bool { 91 | true 92 | } 93 | } 94 | 95 | #[cfg(test)] 96 | mod tests { 97 | use crate::math::consts::EPS_10; 98 | use crate::proj::Proj; 99 | use crate::tests::utils::{test_proj_forward, test_proj_inverse}; 100 | 101 | #[test] 102 | fn proj_merc_merc_ellps() { 103 | let p = Proj::from_proj_string("+proj=merc +ellps=GRS80").unwrap(); 104 | 105 | println!("{:#?}", p.data()); 106 | println!("{:#?}", p.projection()); 107 | 108 | // XXX: this differs in y from Proj output: 110579.96521824962 109 | // because of asinh definition: using the same asinh definition 110 | // leads to same results up to 1e-11m. 111 | let inputs = [ 112 | ((2., 1., 0.), (222638.98158654713, 110579.96521825077, 0.)), 113 | ((2., -1., 0.), (222638.98158654713, -110579.96521825077, 0.)), 114 | ((-2., 1., 0.), (-222638.98158654713, 110579.96521825077, 0.)), 115 | ( 116 | (-2., -1., 0.), 117 | (-222638.98158654713, -110579.96521825077, 0.), 118 | ), 119 | ]; 120 | 121 | test_proj_forward(&p, &inputs, EPS_10); 122 | test_proj_inverse(&p, &inputs, EPS_10); 123 | } 124 | 125 | #[test] 126 | fn proj_merc_merc_sph() { 127 | let p = Proj::from_proj_string("+proj=merc +R=6400000").unwrap(); 128 | 129 | println!("{:#?}", p.projection()); 130 | 131 | let inputs = [ 132 | ((2., 1., 0.), (223402.14425527418, 111706.74357494547, 0.)), 133 | ((2., -1., 0.), (223402.14425527418, -111706.74357494547, 0.)), 134 | ((-2., 1., 0.), (-223402.14425527418, 111706.74357494547, 0.)), 135 | ( 136 | (-2., -1., 0.), 137 | (-223402.14425527418, -111706.74357494547, 0.), 138 | ), 139 | ]; 140 | 141 | test_proj_forward(&p, &inputs, EPS_10); 142 | test_proj_inverse(&p, &inputs, EPS_10); 143 | } 144 | 145 | #[test] 146 | fn proj_merc_webmerc_ellps() { 147 | let p = Proj::from_proj_string("+proj=webmerc +ellps=GRS80").unwrap(); 148 | 149 | println!("{:#?}", p.projection()); 150 | 151 | let inputs = [ 152 | ((2., 1., 0.), (222638.98158654713, 111325.14286638626, 0.)), 153 | ((2., -1., 0.), (222638.98158654713, -111325.14286638626, 0.)), 154 | ((-2., 1., 0.), (-222638.98158654713, 111325.14286638626, 0.)), 155 | ( 156 | (-2., -1., 0.), 157 | (-222638.98158654713, -111325.14286638626, 0.), 158 | ), 159 | ]; 160 | 161 | test_proj_forward(&p, &inputs, EPS_10); 162 | test_proj_inverse(&p, &inputs, EPS_10); 163 | } 164 | 165 | #[test] 166 | fn proj_merc_webmerc_sph() { 167 | let p = Proj::from_proj_string("+proj=webmerc +R=6400000").unwrap(); 168 | 169 | println!("{:#?}", p.projection()); 170 | 171 | let inputs = [ 172 | ((2., 1., 0.), (223402.14425527418, 111706.74357494547, 0.)), 173 | ((2., -1., 0.), (223402.14425527418, -111706.74357494547, 0.)), 174 | ((-2., 1., 0.), (-223402.14425527418, 111706.74357494547, 0.)), 175 | ( 176 | (-2., -1., 0.), 177 | (-223402.14425527418, -111706.74357494547, 0.), 178 | ), 179 | ]; 180 | 181 | test_proj_forward(&p, &inputs, EPS_10); 182 | test_proj_inverse(&p, &inputs, EPS_10); 183 | } 184 | } 185 | -------------------------------------------------------------------------------- /proj4rs/src/geocent.rs: -------------------------------------------------------------------------------- 1 | //! 2 | //! Geodetic to/from geocentrique conversion 3 | //! 4 | use crate::errors::{Error, Result}; 5 | use crate::math::consts::{FRAC_PI_2, PI, TAU}; 6 | 7 | const GENAU: f64 = 1.0e-12; 8 | const GENAU2: f64 = GENAU * GENAU; 9 | const MAXITER: usize = 30; 10 | const FRAC_PI_2_EPS: f64 = 1.001 * FRAC_PI_2; 11 | 12 | /// Convert geodetic coordinates to geocentric coordinatesa 13 | /// 14 | /// The function Convert_Geodetic_To_Geocentric converts geodetic coordinates 15 | /// (latitude, longitude, and height) to geocentric coordinates (X, Y, Z), 16 | /// according to the current ellipsoid parameters. 17 | /// 18 | /// Latitude : Geodetic latitude in radians (input) 19 | /// Longitude : Geodetic longitude in radians (input) 20 | /// Height : Geodetic height, in meters (input) 21 | /// X : Calculated Geocentric X coordinate, in meters (output) 22 | /// Y : Calculated Geocentric Y coordinate, in meters (output) 23 | /// Z : Calculated Geocentric Z coordinate, in meters (output) 24 | /// 25 | /// This conversion converts geodetic coordinate values (longitude, latitude, elevation above ellipsoid) 26 | /// to their geocentric (X, Y, Z) representation, where the first axis (X) points from the Earth centre 27 | /// to the point of longitude=0, latitude=0, the second axis (Y) points from the 28 | /// Earth centre to the point of longitude=90, latitude=0 and the third axis (Z) points to the North pole 29 | /// 30 | pub fn geodetic_to_geocentric(x: f64, y: f64, z: f64, a: f64, es: f64) -> Result<(f64, f64, f64)> { 31 | let mut lon = x; 32 | let mut lat = y; 33 | 34 | if lat < -FRAC_PI_2 && lat > -FRAC_PI_2_EPS { 35 | lat = -FRAC_PI_2 36 | } else if lat > FRAC_PI_2 && lat < FRAC_PI_2_EPS { 37 | lat = FRAC_PI_2 38 | } else if !(-FRAC_PI_2..=FRAC_PI_2).contains(&lat) { 39 | return Err(Error::LatitudeOutOfRange); 40 | }; 41 | 42 | if lon > PI { 43 | // TAU is 2PI 44 | lon -= TAU; 45 | } 46 | 47 | let (sin_lat, cos_lat) = lat.sin_cos(); 48 | // Earth radius at location 49 | let rn = a / (1. - es * (sin_lat * sin_lat)).sqrt(); 50 | Ok(( 51 | (rn + z) * cos_lat * lon.cos(), 52 | (rn + z) * cos_lat * lon.sin(), 53 | ((rn * (1. - es)) + z) * sin_lat, 54 | )) 55 | } 56 | 57 | /// Convert geocentric coordinates to geodetic coordinates 58 | /// 59 | /// ### Reference... 60 | /// 61 | /// Wenzel, H.-G.(1985): Hochauflösende Kugelfunktionsmodelle für 62 | /// das Gravitationspotential der Erde. Wiss. Arb. Univ. Hannover 63 | /// Nr. 137, p. 130-131. 64 | /// 65 | /// Programmed by GGA- Leibniz-Institute of Applied Geophysics 66 | /// Stilleweg 2 67 | /// D-30655 Hannover 68 | /// Federal Republic of Germany 69 | /// Internet: www.gga-hannover.de 70 | /// 71 | /// Hannover, March 1999, April 2004. 72 | /// see also: comments in statements 73 | /// 74 | /// remarks: 75 | /// Mathematically exact and because of symmetry of rotation-ellipsoid, 76 | /// each point (X,Y,Z) has at least two solutions (Latitude1,Longitude1,Height1) and 77 | /// (Latitude2,Longitude2,Height2). Is point=(0.,0.,Z) (P=0.), so you get even 78 | /// four solutions,» every two symmetrical to the semi-minor axis. 79 | /// Here Height1 and Height2 have at least a difference in order of 80 | /// radius of curvature (e.g. (0,0,b)=> (90.,0.,0.) or (-90.,0.,-2b); 81 | /// (a+100.)*(sqrt(2.)/2.,sqrt(2.)/2.,0.) => (0.,45.,100.) or 82 | /// (0.,225.,-(2a+100.))). 83 | /// The algorithm always computes (Latitude,Longitude) with smallest |Height|. 84 | /// For normal computations, that means |Height|<10000.m, algorithm normally 85 | /// converges after to 2-3 steps!!! 86 | /// But if |Height| has the amount of length of ellipsoid's axis 87 | /// (e.g. -6300000.m),» algorithm needs about 15 steps. 88 | pub fn geocentric_to_geodetic( 89 | x: f64, 90 | y: f64, 91 | z: f64, 92 | a: f64, 93 | es: f64, 94 | b: f64, 95 | ) -> Result<(f64, f64, f64)> { 96 | let d2 = (x * x) + (y * y); 97 | 98 | // distance between semi-minor axis and location 99 | let p = d2.sqrt(); 100 | // distance between center and location 101 | let rr = (d2 + z * z).sqrt(); 102 | 103 | // if (X,Y,Z)=(0.,0.,0.) then Height becomes semi-minor axis 104 | // of ellipsoid (=center of mass), Latitude becomes PI/2 105 | let lon = if p / a < GENAU { 106 | if rr / a < GENAU { 107 | return Ok((0., FRAC_PI_2, -b)); 108 | } 109 | 0. 110 | } else { 111 | y.atan2(x) 112 | }; 113 | 114 | //-------------------------------------------------------------- 115 | // Following iterative algorithm was developed by 116 | // Institut for Erdmessung", University of Hannover, July 1988. 117 | // Internet: www.ife.uni-hannover.de 118 | // Iterative computation of CPHI,SPHI and Height. 119 | // Iteration of CPHI and SPHI to 10**-12 radian resp. 120 | // 2*10**-7 arcsec. 121 | // -------------------------------------------------------------- 122 | let ct = z / rr; 123 | let st = p / rr; 124 | let mut rx = 1.0 / (1.0 - es * (2.0 - es) * st * st).sqrt(); 125 | let mut cphi0 = st * (1.0 - es) * rx; 126 | let mut sphi0 = ct * rx; 127 | let (mut rk, mut rn, mut cphi, mut sphi, mut sdphi, mut height); 128 | 129 | // loop to find sin(Latitude) resp. Latitude 130 | // until |sin(Latitude(iter)-Latitude(iter-1))| < genau 131 | 132 | // Note: using `for _ in 0..MAXITER { ... }` lead to compiler error 133 | // about unitialized variables 134 | let mut iter = 0; 135 | loop { 136 | iter += 1; 137 | rn = a / (1.0 - es * sphi0 * sphi0).sqrt(); 138 | // ellipsoidal (geodetic) height 139 | height = p * cphi0 + z * sphi0 - rn * (1.0 - es * sphi0 * sphi0); 140 | 141 | // avoid zero division 142 | if (rn + height) == 0. { 143 | return Ok((lon, 0., height)); 144 | } 145 | 146 | rk = es * rn / (rn + height); 147 | rx = 1.0 / (1.0 - rk * (2.0 - rk) * st * st).sqrt(); 148 | cphi = st * (1.0 - rk) * rx; 149 | sphi = ct * rx; 150 | sdphi = sphi * cphi0 - cphi * sphi0; 151 | cphi0 = cphi; 152 | sphi0 = sphi; 153 | 154 | if sdphi * sdphi <= GENAU2 { 155 | break; 156 | } 157 | 158 | if iter >= MAXITER { 159 | break; 160 | } 161 | } 162 | 163 | // ellipsoidal (geodetic) latitude 164 | Ok((lon, sphi.atan2(cphi.abs()), height)) 165 | } 166 | --------------------------------------------------------------------------------