Update: since Oct 24 2024 I am homeless and living in my van. I lost access to most of my computer hardware. The eviction from my home has been timed for maximum effect as I was not present when it happened. Please, if you use my software, consider asking everyone in the geospatial community if they are taking part in this extortion and why.
This is proj.js - PROJ bindings for JavaScript with a native port for Node.js and WASM port for the browser using SWIG JSE.
This project is completely separate from proj4js which is a separate (and partial) reimplementation in JavaScript.
Alpha quality. Most basic functions work as intended. Mostly tested for leaks on the nominal code paths.
Keep in mind that I am only a very occasional user of a very small fraction of PROJ and my main interest is JavaScript bindings - Node.js and browser - for C/C++ projects. If you find methods that are not usable in the current version and submit unit tests for it, I will make them work.
The npm package has precompiled binaries for Linux x64, Windows x64 and macOS x64/arm64. The code generated by SWIG is included in the package.
npm install proj.jsWhen checking out from git, the code must be generated by SWIG JSE which is now distributed for most platforms as part of the hadron build system:
# Checkout from git
git clone https://github.com/mmomtchev/proj.js.git
# Install all the npm dependencies
cd proj.js
npm install
npx xpm install
# Generate the wrappers
npx xpm run generate
# alias npm run swig
# Eventually, set build options
export npm_config_disable_tiff=true
export npm_config_enable_inline_projdb=true
# Build the native version (requires a working C++ compiler)
npx xpm run prepare --config native && npx xpm run build --config native
# alias npm run build:native
# Build the WASM version (requires emsdk in path)
npx xpm run prepare --config wasm && npx xpm run build --config wasm
# alias npm run build:wasm
# Run the quickstart
node test/shared/quickstart.debug.js
# Run the tests (Node.js and browser)
npm test
# Run the web demo (should work on all OS if you have the WASM version)
cd test/browser && npx webpack serve --mode=production
# then open http://localhost:8030/Enable ASAN or any other meson option and create a debug build:
npx xpm run prepare --config native-debug
npx xpm run configure --config native-debug -- -Db_sanitize=address
npx xpm run build --config native-debugThe VS Code launch.json contains the command that must be launched in order to use the debugger.
This package is a magickwand.js-style npm package with an automatic import that resolves to either the native module or the WASM module depending on the environment.
The following code will import the module:
import qPROJ from 'proj.js';
const PROJ = await qPROJ;
console.log(`proj.db is inlined: ${PROJ.proj_js_inline_projdb}`);
if (!PROJ.proj_js_inline_projdb) {
const proj_db = new Uint8Array(await (await fetch(proj_db_url)).arrayBuffer());
PROJ.loadDatabase(proj_db);
}Node.js will pick up the native binary, while a modern bundler such as webpack or rollup with support for Node.js 16 exports will pick up the WASM module.
This requires ES6, Node.js 16 and a recent webpack or rollup. If using TypeScript, you will have to transpile to ES6. Most major web components were updated with those features in 2022.
If using only the Node.js native module, there is an alternative import that is fully synchronous:
import PROJ from 'proj.js/native';
console.log(`proj.db is inlined: ${PROJ.proj_js_inline_projdb}`);As proj.js is an ES6-only project, using require from CJS works only with very recent Node.js versions (refer to require(esm) in Node.js)
When using the native module, proj.db is always external and automatically loaded from import.meta.resolve('proj.js/proj.db').
It is also possible to always load the WASM module even if running in Node.js:
import qPROJ from 'proj.js/wasm';
const PROJ: PROJ = await qPROJ;If using TypeScript, you will need to explicitly import the types in the PROJ namespace because PROJ is a variable:
import qPROJ from 'proj.js';
import type * as PROJ from 'proj.js';
const PROJ: PROJ = await qPROJ;
console.log(`proj.db is inlined: ${PROJ.proj_js_inline_projdb}`);
if (!PROJ.proj_js_inline_projdb) {
const proj_db = new Uint8Array(await (await fetch(proj_db_url)).arrayBuffer());
PROJ.loadDatabase(proj_db);
}When using the WASM module in a browser, proj.db can be either inlined in the WASM bundle - which considerably increases its size - or downloaded separately by the user code and loaded into the module.
You can check test/browser/index.ts for an example that uses webpack inline asset modules to bundle and load a proj.db that has not been inlined. Or you can download your own custom proj.db from your own custom URL.
At the moment the default prebuilt WASM binaries do not have proj.db inlined as this is considered the more versatile solution at the cost of a few extra lines of code when loading the module.
The old PROJ C API is available as a separate module, proj.js/capi. This module is slightly smaller and the setup of the API is slightly faster.
The C API has many caveats and it is more prone to errors when interfacing to JS.
Using it for anything but basic coordinate transformation is not tested at the moment.
Keep in mind that if you load both modules, you will end with two completely separate and independent PROJ instances that cannot talk to each other.
When using WASM, proj.db can either be inlined in the WASM bundle or it can be loaded from an Uint8Array before use.
Currently, the bundle size remains an issue.
| Component | raw | brotli |
|---|---|---|
proj.wasm w/ TIFF w/o proj.db |
8593K | 1735K |
proj.wasm w/o TIFF w/o proj.db |
7082K | 1302K |
proj.db |
9240K | 1320K |
Only proj.a w/ TIFF without bindings |
1629K | 1092K |
| Only the C++ API bindings | 6964K | 643K |
| Only the C API bindings | 2352K | 238K |
It should be noted that while using -Os in emscripten can lead to a two-fold reduction of the raw size, the size of the compressed build will always remain the same. Sames goes for optimizing with binaryen - despite the very significant raw size gain, the compressed size gain is relatively insignificant.
curl support is enabled only in the native build - there is no simple solution to networking for the WASM build.
Linking with my own sqlite-wasm-http project to access a remote proj.db, using SQL over HTTP, is a very significant project that will further increase the bundle size to the point nullifying the gains from proj.db. It does not seem to be a logical option at the moment.
Currently the biggest contributor to raw code size is SWIG JSE which produces large amounts of identical code for each function. This may me improved in a future version, but bear in mind that SWIG-generated code has a very good compression ratio. It is also worth investigating what can be gained from modularization of the SWIG wrappers and if it is really necessary to wrap separately all derived classes.
In the compressed bundle, the main contributor is libproj.a.
Here is a quick rundown of the cost of each wrapper in the C++ bindings:
Initial crude benchmarks, tested on i7 9700K @ 3.6 GHz with the C++ quickstart:
| Test | Native | WASM in V8 |
|---|---|---|
DatabaseContext.create() |
0.171ms | 16.316ms |
AuthorityFactory.create('string') |
0.071ms | 0.44ms |
CoordinateOperationContext.create() |
0.052ms | 0.397ms |
AuthorityFactory.create('EPSG') |
0.011ms | 0.274ms |
createFromUserInput() |
0.283ms | 0.617ms |
CoordinateOperationFactory.create().createOperations() |
0.588ms | 1.885ms |
coordinateTransformer() |
0.29ms | 19.117ms |
transform() |
0.014ms | 0.234ms |
Globally, the first impression is that the library is usable both on the backend and in the browser in fully synchronous mode. The only real hurdle at the moment remains the WASM bundle size.
This project supports the hadron method of setting the download URL for the prebuilt binaries to a custom location.
Environment variable:
export npm_config_proj_js_binary_host=https://overriden-host.com/overriden-pathnpm install option:
npm install proj.js --proj_js_binary_host=https://overriden-host.com/overriden-path.npmrc option:
proj_js_binary_host=https://overriden-host.com/overriden-pathCurrently, the size of the WASM bundle renders it impractical for a normal website on the Internet. There are a number of
PROJcan be enriched with conditional compilation exported to CMake options which will be exported tonpmoptions throughhadron. When generating the bindings, SWIG won't generate any code if the function is disabled by conditional compilation. It can accept the usual-D...CLI option. Typemaps do not increase the code size unless they are actually used to wrap a function. This is the best approach that will lead to the best results.- The project's two entry points -
proj.iandproj_capi.i- can also be enriched with%ignoredirectives controlled by-D...macros. This will result savings only from the SWIG bindings themselves without reducing thePROJ.aarchive. - Alternatively, a third entry point, for a specially trimmed ultra-light WASM bundle, can be added. In this case, I will consider adding an automatic build and distribution point for this bundle.
- As a last option,
emscriptenhas a very interesting feature that allows to split the bundle in two pieces, a main bundle and a loadable addon to be downloaded and parsed only if this part of the code is invoked. The split of the module is determined by the code coverage of a user routine. This means you write some JavaScript code that usesproj.js, you launch it, and all the called functions become part of the main module. My first tests show that this can lead to up to 60% reduction of the size of the main bundle if calling only the C API quickstart. This works without sacrificing any features - but at the cost of a very significant one-time latency when calling a new function for the first time.
As proj.js works both in Node.js (as native) code and the browser (as a WASM bundle), it is compatible with frameworks that render partially on the server and in the client - with Next.js being the best example.
However this support comes with one important caveat - proj.js binary objects are not compatible between the native code and the WASM bundle and cannot be directly hydrated on the client.
This means that a web application that uses both builds of proj.js at the same time will have to manually serialize all coordinates to JavaScript arrays when transferring them to the client before recreating the PJ objects in the browser.
Further, while it is possible to use the WASM bundle even on the backend - at the price of a considerable loss of performance - this won't solve this problem as currently WASM objects cannot be serialized at all.
