Skip to content

Instantly share code, notes, and snippets.

@rcoup
Last active October 17, 2023 15:14
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save rcoup/6c458154a2e5368b04e48b26252c767b to your computer and use it in GitHub Desktop.
Save rcoup/6c458154a2e5368b04e48b26252c767b to your computer and use it in GitHub Desktop.
PROJ in a browser demo via Enscriptem+WASM
/proj
/proj-build
/sqlite
/sqlite-amalgamation-*.zip
/one-native
/one-native.dSYM
/one.data
/one.html
/one.js
/one.wasm
/fsroot
/.vscode

PROJ in a Browser

  • Still has SQLite, but the DB is completely empty. Not sure whether we could #ifdef it away altogether by cutting down the API.
  • No support for network grids/etc yet, but should be able to use Empscripten's Fetch API in proj/src/networkfilemanager.cpp in place of CURL.
  • No other resource files.
  • module-pre-js.js sets environment variables for PROJ.

Because the SQLite DB is empty, you need to pass in full transformation info, not just a source & dest CRS. For example, to convert from EPSG:2193 (NZTM) to EPSG:3857 (Web Mercator), get the WKT via projinfo -s EPSG:2193 -t EPSG:3857. You can use either the "PROJ string" or the "WKT2:2019 string" to pass to proj_create().

one.c is the conversion code.

Building

  • install emscripten, cmake, sqlite3, ccache
# clone this gist
$ git clone https://gist.github.com/6c458154a2e5368b04e48b26252c767b.git proj-wasm
$ cd proj-wasm

$ wget https://www.sqlite.org/2022/sqlite-amalgamation-3390400.zip  # or newer
$ git clone https://github.com/osgeo/proj.git

$ make

Running in Node

  • To run in node: make run

Running In Browser

You can't use file:// urls because browsers won't let you open other files.

make run-browser will start a python http server and open your browser. Logging/output appears in the JS console.

Native (no Enscriptem)

  • To run a native/host version of the C example: make run-native
#export EMCC_CFLAGS := -O0 -g -sDISABLE_EXCEPTION_CATCHING=0 #-sSAFE_HEAP=1 -sSTACK_OVERFLOW_CHECK=1
export EMCC_CFLAGS := -Os -sDISABLE_EXCEPTION_CATCHING=0
export EM_COMPILER_WRAPPER := ccache
all: one.html one-native
.PHONY: all
sqlite/sqlite3.c: sqlite-amalgamation-3390400.zip
$(RM) -r sqlite
unzip -j -d sqlite sqlite-amalgamation-3390400.zip
touch sqlite/*
SQLITE_WASM_LIB := sqlite/wasm/libsqlite3.a
SQLITE_NATIVE_LIB := sqlite/native/libsqlite3.a
$(SQLITE_WASM_LIB): sqlite/sqlite3.c
mkdir -p sqlite/wasm
emcc -c -o sqlite/wasm/sqlite3.o $^
emar rcs $@ sqlite/wasm/sqlite3.o
$(SQLITE_NATIVE_LIB): sqlite/sqlite3.c
mkdir -p sqlite/native
CFLAGS=-Os cc -c -o sqlite/native/sqlite3.o $^
ar rcs $@ sqlite/native/sqlite3.o
CMAKE_FLAGS := \
-DCMAKE_BUILD_TYPE=Release \
-DBUILD_SHARED_LIBS=NO \
-DBUILD_APPS=OFF \
-DBUILD_TESTING=OFF \
-DENABLE_CURL=OFF \
-DENABLE_TIFF=OFF \
-DSQLITE3_INCLUDE_DIR=sqlite \
-DUSE_CCACHE=ON
PROJ_WASM_LIB := proj-build/wasm-install/lib/libproj.a
PROJ_NATIVE_LIB := proj-build/native-install/lib/libproj.a
$(PROJ_WASM_LIB): proj/CMakeLists.txt $(SQLITE_WASM_LIB)
mkdir -p proj-build/wasm-install
emcmake cmake \
-B proj-build/wasm -S proj \
-DCMAKE_INSTALL_PREFIX=proj-build/wasm-install \
-DSQLITE3_LIBRARY=$(SQLITE_WASM_LIB) \
$(CMAKE_FLAGS) \
-DCMAKE_CXX_FLAGS_RELEASE="$(EMCC_CFLAGS)"
cmake --build proj-build/wasm --target install -j
touch -c $@
$(PROJ_NATIVE_LIB): proj/CMakeLists.txt $(SQLITE_NATIVE_LIB)
mkdir -p proj-build/native-install
cmake cmake \
-B proj-build/native -S proj \
-DCMAKE_INSTALL_PREFIX=proj-build/native-install \
-DSQLITE3_LIBRARY=$(SQLITE_NATIVE_LIB) \
-DPROJ_DB_CACHE_DIR=proj-build/wasm-install/share/proj \
$(CMAKE_FLAGS)
cmake --build proj-build/native --target install -j
touch -c $@
PROJ_INSTALL_SHARE := proj-build/wasm-install/share/proj
fsroot/proj/proj.ini: $(PROJ_WASM_LIB)
mkdir -p fsroot/proj
cp $(PROJ_INSTALL_SHARE)/proj.ini $@
fsroot/proj/proj.db: $(PROJ_WASM_LIB)
mkdir -p fsroot/proj
$(RM) $@
touch $@
sqlite3 $(PROJ_INSTALL_SHARE)/proj.db ".schema metadata" | sqlite3 $@
# sqlite3 $(PROJ_INSTALL_SHARE)/proj.db ".schema --nosys" \
# | sed '/^CREATE TRIGGER/,/^END;/d;' \
# | sed '/^CREATE INDEX.*;$$/d' \
# | sed 's/^CONSTRAINT/--CONSTRAINT/g' \
# | sqlite3 $@
sqlite3 $(PROJ_INSTALL_SHARE)/proj.db ".mode insert metadata" "select * from metadata;" | sqlite3 $@
FS := fsroot/proj/proj.ini fsroot/proj/proj.db
one.html: one.c module.pre-js.js $(PROJ_WASM_LIB) $(FS)
emcc \
-o one.html \
--pre-js module.pre-js.js \
-Iproj-build/wasm-install/include \
one.c \
-Lproj-build/wasm-install/lib \
-Lsqlite/wasm \
-lsqlite3 \
-lproj \
--preload-file fsroot@
run: one.html
node --trace-uncaught one.js
.PHONY: run
one-native: one.c $(PROJ_NATIVE_LIB) $(FS)
MACOSX_DEPLOYMENT_TARGET=13.6 \
cc -Os -o $@ \
-Iproj-build/native-install/include \
one.c \
-Lproj-build/native-install/lib \
-Lsqlite/native \
-lsqlite3 \
-lproj \
-lc++
run-native: one-native $(FS)
PROJ_DEBUG=3 PROJ_DATA=$(CURDIR)/fsroot/proj ./one-native
.PHONY: run-native
run-browser: one.html
# technically in the wrong order, but it works
open http://localhost:9009/one.html &
python3 -m http.server 9009
.PHONY: run-browser
clean:
$(RM) -r \
sqlite/wasm sqlite/native \
proj-build/wasm-install proj-build/native-install \
proj-build/wasm/CMakeCache.txt proj-build/native/CMakeCache.txt \
one.html one.js one.wasm one.data \
fsroot
.PHONY: clean
Module.preRun = [];
Module.preRun.push(function() {
ENV.PROJ_DEBUG = "3"; // 0-3
ENV.PROJ_DATA = "/proj";
ENV.PROJ_NETWORK = "OFF";
console.log("preRun ENV", ENV);
});
Module.logReadFiles = true;
#include <stdio.h>
#include <proj.h>
#include "transform_wkt.h"
void log_proj_error(PJ_CONTEXT *C, char *action) {
int errno;
const char *errstr;
errno = proj_context_errno(C);
if (errno == 0) {
/* This should be impossible. */
fprintf(stderr, ">> Failed to %s, reason unknown.\n", action);
} else {
errstr = proj_context_errno_string(C, errno);
fprintf(stderr, ">> Failed to %s: %s.\n", action, errstr);
}
}
int main (void) {
PJ_CONTEXT *C;
PJ *P;
PJ *norm;
PJ_COORD a, b;
// printf("Transform WKT:\n%s\n", TRANSFORM_NZTM_WEBMERC);
/* or you may set C=PJ_DEFAULT_CTX if you are sure you will */
/* use PJ objects from only one thread */
fprintf(stderr, ">> Creating context...\n");
C = proj_context_create();
fprintf(stderr, ">> C=%p\n", (void*)C);
fprintf(stderr, ">> Creating transform...\n");
P = proj_create(C, TRANSFORM_NZTM_WEBMERC);
fprintf(stderr, ">> P=%p\n", (void*)P);
if (0 == P) {
log_proj_error(C, "Creating NZTM>WebMercator transform");
proj_context_destroy(C);
return 1;
}
/* This will ensure that the order of coordinates for the input CRS */
/* will be longitude, latitude, whereas EPSG:4326 mandates latitude, */
/* longitude */
fprintf(stderr, ">> Normalising transform...\n");
norm = proj_normalize_for_visualization(C, P);
fprintf(stderr, ">> norm=%p\n", (void*)norm);
if (0 == norm) {
log_proj_error(C, "normalize transformation");
proj_context_destroy(C);
return 1;
}
proj_destroy(P);
P = norm;
/* a coordinate union representing Auckland: 37d S, 174d E */
/* Given that we have used proj_normalize_for_visualization(), the order of */
/* coordinates is longitude, latitude, and values are expressed in degrees. */
a = proj_coord(5904660.31, 1688977.32, 0, 0);
/* transform to Web Mercator */
fprintf(stderr, ">> Transforming ...\n");
b = proj_trans(P, PJ_FWD, a);
fprintf(stderr, ">> Mercator easting: %.3f, northing: %.3f\n", b.enu.e, b.enu.n);
/* Clean up */
fprintf(stderr, ">> Cleaning up ...\n");
proj_destroy(P);
proj_context_destroy(C); /* may be omitted in the single threaded case */
fprintf(stderr, ">> Bye ...\n");
return 0;
}
const char TRANSFORM_NZTM_WEBMERC[] =
"CONCATENATEDOPERATION[\"Inverse of New Zealand Transverse Mercator 2000 + NZGD2000 to WGS 84 (1) + Popular Visualisation Pseudo-Mercator\","
"SOURCECRS["
"PROJCRS[\"NZGD2000 / New Zealand Transverse Mercator 2000\","
"BASEGEOGCRS[\"NZGD2000\","
"DATUM[\"New Zealand Geodetic Datum 2000\","
"ELLIPSOID[\"GRS 1980\",6378137,298.257222101,"
"LENGTHUNIT[\"metre\",1]]],"
"PRIMEM[\"Greenwich\",0,"
"ANGLEUNIT[\"degree\",0.0174532925199433]],"
"ID[\"EPSG\",4167]],"
"CONVERSION[\"New Zealand Transverse Mercator 2000\","
"METHOD[\"Transverse Mercator\","
"ID[\"EPSG\",9807]],"
"PARAMETER[\"Latitude of natural origin\",0,"
"ANGLEUNIT[\"degree\",0.0174532925199433],"
"ID[\"EPSG\",8801]],"
"PARAMETER[\"Longitude of natural origin\",173,"
"ANGLEUNIT[\"degree\",0.0174532925199433],"
"ID[\"EPSG\",8802]],"
"PARAMETER[\"Scale factor at natural origin\",0.9996,"
"SCALEUNIT[\"unity\",1],"
"ID[\"EPSG\",8805]],"
"PARAMETER[\"False easting\",1600000,"
"LENGTHUNIT[\"metre\",1],"
"ID[\"EPSG\",8806]],"
"PARAMETER[\"False northing\",10000000,"
"LENGTHUNIT[\"metre\",1],"
"ID[\"EPSG\",8807]]],"
"CS[Cartesian,2],"
"AXIS[\"northing (N)\",north,"
"ORDER[1],"
"LENGTHUNIT[\"metre\",1]],"
"AXIS[\"easting (E)\",east,"
"ORDER[2],"
"LENGTHUNIT[\"metre\",1]],"
"ID[\"EPSG\",2193]]],"
"TARGETCRS["
"PROJCRS[\"WGS 84 / Pseudo-Mercator\","
"BASEGEOGCRS[\"WGS 84\","
"ENSEMBLE[\"World Geodetic System 1984 ensemble\","
"MEMBER[\"World Geodetic System 1984 (Transit)\"],"
"MEMBER[\"World Geodetic System 1984 (G730)\"],"
"MEMBER[\"World Geodetic System 1984 (G873)\"],"
"MEMBER[\"World Geodetic System 1984 (G1150)\"],"
"MEMBER[\"World Geodetic System 1984 (G1674)\"],"
"MEMBER[\"World Geodetic System 1984 (G1762)\"],"
"MEMBER[\"World Geodetic System 1984 (G2139)\"],"
"ELLIPSOID[\"WGS 84\",6378137,298.257223563,"
"LENGTHUNIT[\"metre\",1]],"
"ENSEMBLEACCURACY[2.0]],"
"PRIMEM[\"Greenwich\",0,"
"ANGLEUNIT[\"degree\",0.0174532925199433]],"
"ID[\"EPSG\",4326]],"
"CONVERSION[\"Popular Visualisation Pseudo-Mercator\","
"METHOD[\"Popular Visualisation Pseudo Mercator\","
"ID[\"EPSG\",1024]],"
"PARAMETER[\"Latitude of natural origin\",0,"
"ANGLEUNIT[\"degree\",0.0174532925199433],"
"ID[\"EPSG\",8801]],"
"PARAMETER[\"Longitude of natural origin\",0,"
"ANGLEUNIT[\"degree\",0.0174532925199433],"
"ID[\"EPSG\",8802]],"
"PARAMETER[\"False easting\",0,"
"LENGTHUNIT[\"metre\",1],"
"ID[\"EPSG\",8806]],"
"PARAMETER[\"False northing\",0,"
"LENGTHUNIT[\"metre\",1],"
"ID[\"EPSG\",8807]]],"
"CS[Cartesian,2],"
"AXIS[\"easting (X)\",east,"
"ORDER[1],"
"LENGTHUNIT[\"metre\",1]],"
"AXIS[\"northing (Y)\",north,"
"ORDER[2],"
"LENGTHUNIT[\"metre\",1]],"
"ID[\"EPSG\",3857]]],"
"STEP["
"CONVERSION[\"Inverse of New Zealand Transverse Mercator 2000\","
"METHOD[\"Inverse of Transverse Mercator\","
"ID[\"INVERSE(EPSG)\",9807]],"
"PARAMETER[\"Latitude of natural origin\",0,"
"ANGLEUNIT[\"degree\",0.0174532925199433],"
"ID[\"EPSG\",8801]],"
"PARAMETER[\"Longitude of natural origin\",173,"
"ANGLEUNIT[\"degree\",0.0174532925199433],"
"ID[\"EPSG\",8802]],"
"PARAMETER[\"Scale factor at natural origin\",0.9996,"
"SCALEUNIT[\"unity\",1],"
"ID[\"EPSG\",8805]],"
"PARAMETER[\"False easting\",1600000,"
"LENGTHUNIT[\"metre\",1],"
"ID[\"EPSG\",8806]],"
"PARAMETER[\"False northing\",10000000,"
"LENGTHUNIT[\"metre\",1],"
"ID[\"EPSG\",8807]],"
"ID[\"INVERSE(EPSG)\",19971]]],"
"STEP["
"COORDINATEOPERATION[\"NZGD2000 to WGS 84 (1)\","
"VERSION[\"OSG-Nzl\"],"
"SOURCECRS["
"GEOGCRS[\"NZGD2000\","
"DATUM[\"New Zealand Geodetic Datum 2000\","
"ELLIPSOID[\"GRS 1980\",6378137,298.257222101,"
"LENGTHUNIT[\"metre\",1]]],"
"PRIMEM[\"Greenwich\",0,"
"ANGLEUNIT[\"degree\",0.0174532925199433]],"
"CS[ellipsoidal,2],"
"AXIS[\"geodetic latitude (Lat)\",north,"
"ORDER[1],"
"ANGLEUNIT[\"degree\",0.0174532925199433]],"
"AXIS[\"geodetic longitude (Lon)\",east,"
"ORDER[2],"
"ANGLEUNIT[\"degree\",0.0174532925199433]],"
"ID[\"EPSG\",4167]]],"
"TARGETCRS["
"GEOGCRS[\"WGS 84\","
"ENSEMBLE[\"World Geodetic System 1984 ensemble\","
"MEMBER[\"World Geodetic System 1984 (Transit)\"],"
"MEMBER[\"World Geodetic System 1984 (G730)\"],"
"MEMBER[\"World Geodetic System 1984 (G873)\"],"
"MEMBER[\"World Geodetic System 1984 (G1150)\"],"
"MEMBER[\"World Geodetic System 1984 (G1674)\"],"
"MEMBER[\"World Geodetic System 1984 (G1762)\"],"
"MEMBER[\"World Geodetic System 1984 (G2139)\"],"
"ELLIPSOID[\"WGS 84\",6378137,298.257223563,"
"LENGTHUNIT[\"metre\",1]],"
"ENSEMBLEACCURACY[2.0]],"
"PRIMEM[\"Greenwich\",0,"
"ANGLEUNIT[\"degree\",0.0174532925199433]],"
"CS[ellipsoidal,2],"
"AXIS[\"geodetic latitude (Lat)\",north,"
"ORDER[1],"
"ANGLEUNIT[\"degree\",0.0174532925199433]],"
"AXIS[\"geodetic longitude (Lon)\",east,"
"ORDER[2],"
"ANGLEUNIT[\"degree\",0.0174532925199433]],"
"ID[\"EPSG\",4326]]],"
"METHOD[\"Geocentric translations (geog2D domain)\","
"ID[\"EPSG\",9603]],"
"PARAMETER[\"X-axis translation\",0,"
"LENGTHUNIT[\"metre\",1],"
"ID[\"EPSG\",8605]],"
"PARAMETER[\"Y-axis translation\",0,"
"LENGTHUNIT[\"metre\",1],"
"ID[\"EPSG\",8606]],"
"PARAMETER[\"Z-axis translation\",0,"
"LENGTHUNIT[\"metre\",1],"
"ID[\"EPSG\",8607]],"
"OPERATIONACCURACY[1.0],"
"ID[\"EPSG\",1565]]],"
"STEP["
"CONVERSION[\"Popular Visualisation Pseudo-Mercator\","
"METHOD[\"Popular Visualisation Pseudo Mercator\","
"ID[\"EPSG\",1024]],"
"PARAMETER[\"Latitude of natural origin\",0,"
"ANGLEUNIT[\"degree\",0.0174532925199433],"
"ID[\"EPSG\",8801]],"
"PARAMETER[\"Longitude of natural origin\",0,"
"ANGLEUNIT[\"degree\",0.0174532925199433],"
"ID[\"EPSG\",8802]],"
"PARAMETER[\"False easting\",0,"
"LENGTHUNIT[\"metre\",1],"
"ID[\"EPSG\",8806]],"
"PARAMETER[\"False northing\",0,"
"LENGTHUNIT[\"metre\",1],"
"ID[\"EPSG\",8807]],"
"ID[\"EPSG\",3856]]],"
"USAGE["
"SCOPE[\"unknown\"],"
"AREA[\"New Zealand - onshore and offshore. Includes Antipodes Islands, Auckland Islands, Bounty Islands, Chatham Islands, Cambell Island, Kermadec Islands, Raoul Island and Snares Islands.\"],"
"BBOX[-55.95,160.6,-25.88,-171.2]]]"
;
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment