crossbind
GitHub

PROJ for Android

v9.9.0Android

PROJ 9.9.0 for React Native apps on Android, precompiled for arm64-v8a devices and the x86_64 emulator as @crossbind/port-proj-android.

npm install @crossbind/port-proj-android@beta

Install

shell
npm install @crossbind/plugin-react-native@beta @crossbind/plugin-react-native-ios-helper@beta @crossbind/port-proj-android@beta
npm install --save-dev @crossbind/plugin-metro@beta
crossbind.config.mjs
import projAndroid from '@crossbind/port-proj-android/crossbind.config.js';
 
export default {
dependencies: [projAndroid],
paths: { config: import.meta.url },
};
metro.config.js
const { getDefaultConfig, mergeConfig } = require('@react-native/metro-config');
const CrossbindMetroPlugin = require('@crossbind/plugin-metro');
 
const defaultConfig = getDefaultConfig(__dirname);
 
const config = {
...CrossbindMetroPlugin(defaultConfig),
};
 
module.exports = mergeConfig(defaultConfig, config);

The whole flow, including Expo, is in the React Native playbook.

Usage

The examples the WebAssembly page runs, as Android compiles them: the same headers and the same calls. They are checked on the WebAssembly build.

Transform a coordinate between two CRSs

The most used part of PROJ: proj_create_crs_to_crs picks the operation between two coordinate reference systems, proj_normalize_for_visualization puts longitude first, and proj_trans runs it forward or back.

src/native/transformer.h
#pragma once
 
#include <proj.h>
 
#include <cmath>
#include <cstdio>
#include <stdexcept>
#include <string>
 
// Converts coordinates between two coordinate reference systems given as EPSG or ESRI codes, WKT,
// PROJJSON or PROJ strings. Axis order is normalised to longitude or easting first, so EPSG:4326
// takes (longitude, latitude) although EPSG defines it latitude first.
class Transformer {
public:
Transformer(const std::string& source, const std::string& target) : context(proj_context_create()) {
proj_log_func(context, &error, remember);
PJ* chosen = proj_create_crs_to_crs(context, source.c_str(), target.c_str(), nullptr);
if (chosen) {
name = proj_get_name(chosen);
transformation = proj_normalize_for_visualization(context, chosen);
proj_destroy(chosen);
}
if (!transformation) {
const std::string reason = error.empty() ? proj_context_errno_string(context, proj_context_errno(context)) : error;
proj_context_destroy(context);
throw std::runtime_error(reason);
}
}
 
~Transformer() {
proj_destroy(transformation);
proj_context_destroy(context);
}
 
Transformer(const Transformer&) = delete;
Transformer& operator=(const Transformer&) = delete;
 
// Both return the point as a JSON array, [x, y].
std::string forward(double x, double y) { return apply(PJ_FWD, x, y); }
std::string inverse(double x, double y) { return apply(PJ_INV, x, y); }
 
// The operation PROJ picked, e.g. "UTM zone 35N".
std::string operation() const { return name; }
 
private:
std::string apply(PJ_DIRECTION direction, double x, double y) {
proj_errno_reset(transformation);
const PJ_COORD out = proj_trans(transformation, direction, proj_coord(x, y, 0, HUGE_VAL));
if (out.xy.x == HUGE_VAL) throw std::runtime_error(proj_context_errno_string(context, proj_errno(transformation)));
char json[64];
std::snprintf(json, sizeof json, "[%.17g,%.17g]", out.xy.x, out.xy.y);
return json;
}
 
// PROJ reports why a definition failed through its log, e.g. "crs not found: EPSG:99999".
static void remember(void* error, int level, const char* message) {
if (level == PJ_LOG_ERROR) *static_cast<std::string*>(error) = message;
}
 
PJ_CONTEXT* context;
PJ* transformation = nullptr;
std::string name;
std::string error;
};
main.js
import { initNative, Transformer } from './native/transformer.h';
 
await initNative();
const toUtm = await new Transformer('EPSG:4326', 'EPSG:32635'); // WGS 84 to UTM zone 35N
console.log(await toUtm.operation());
const [easting, northing] = JSON.parse(await toUtm.forward(28.9784, 41.0082)); // Istanbul, longitude first
console.log(easting.toFixed(2), northing.toFixed(2));
const [longitude, latitude] = JSON.parse(await toUtm.inverse(easting, northing));
console.log(longitude.toFixed(6), latitude.toFixed(6));
const toWebMap = await new Transformer('EPSG:4326', 'EPSG:3857'); // WGS 84 to Web Mercator
console.log(await toWebMap.operation());
const [x, y] = JSON.parse(await toWebMap.forward(28.9784, 41.0082));
console.log(x.toFixed(2), y.toFixed(2));
PRINTS
UTM zone 35N
666370.51 4541552.49
28.978400 41.008200
Popular Visualisation Pseudo-Mercator
3225860.73 5013551.24

Write a CRS as WKT, a .prj, PROJJSON or a PROJ string

proj_create reads a CRS from an EPSG code or any definition; proj_as_wkt writes WKT2 or the ESRI WKT a shapefile's .prj holds, proj_as_projjson writes PROJJSON, and proj_as_proj_string the short PROJ string, which drops names and the area of use.

src/native/crs_formats.h
#pragma once
 
#include <proj.h>
 
#include <stdexcept>
#include <string>
 
// Reads a CRS from any definition PROJ accepts and writes it out in the formats other software
// reads: WKT2 (ISO 19162, what GDAL and QGIS write), WKT1 the way ESRI writes it (the text of a
// shapefile's .prj), PROJJSON, and the PROJ string.
class CrsFormats {
public:
explicit CrsFormats(const std::string& definition) : context(proj_context_create()) {
proj_log_func(context, &error, remember);
crs = proj_create(context, definition.c_str());
if (!crs || !proj_is_crs(crs)) {
const std::string reason = crs ? definition + " is not a CRS" : error.empty() ? "PROJ cannot read " + definition : error;
proj_destroy(crs);
proj_context_destroy(context);
throw std::runtime_error(reason);
}
}
 
~CrsFormats() {
proj_destroy(crs);
proj_context_destroy(context);
}
 
CrsFormats(const CrsFormats&) = delete;
CrsFormats& operator=(const CrsFormats&) = delete;
 
// "EPSG:32635 WGS 84 / UTM zone 35N"; just the name when the definition carries no code.
std::string label() const {
const char* authority = proj_get_id_auth_name(crs, 0);
const char* code = proj_get_id_code(crs, 0);
const std::string name = proj_get_name(crs);
return authority && code ? std::string(authority) + ":" + code + " " + name : name;
}
 
std::string wkt() { return text(proj_as_wkt(context, crs, PJ_WKT2_2019, nullptr)); }
std::string esriWkt() { return text(proj_as_wkt(context, crs, PJ_WKT1_ESRI, nullptr)); }
std::string projJson() { return text(proj_as_projjson(context, crs, nullptr)); }
// A PROJ string keeps the maths and drops the metadata, such as the name and the area of use.
std::string projString() { return text(proj_as_proj_string(context, crs, PJ_PROJ_5, nullptr)); }
 
private:
// The exporters return text owned by the CRS object, so it is copied at once.
std::string text(const char* exported) const {
if (!exported) throw std::runtime_error(error.empty() ? "PROJ cannot write this CRS in that format" : error);
return exported;
}
 
static void remember(void* error, int level, const char* message) {
if (level == PJ_LOG_ERROR) *static_cast<std::string*>(error) = message;
}
 
PJ_CONTEXT* context;
PJ* crs = nullptr;
std::string error;
};
main.js
import { initNative, CrsFormats } from './native/crs_formats.h';
 
await initNative();
const utm = await new CrsFormats('EPSG:32635');
console.log(await utm.label());
console.log(await utm.projString());
console.log((await utm.wkt()).split('\n')[0]); // the first line of WKT2
console.log(await utm.esriWkt()); // the text of a shapefile's .prj
const json = JSON.parse(await utm.projJson());
console.log(json.type, json.conversion.method.name, json.conversion.parameters.map((parameter) => parameter.value).join(' '));
PRINTS
EPSG:32635 WGS 84 / UTM zone 35N
+proj=utm +zone=35 +datum=WGS84 +units=m +no_defs +type=crs
PROJCRS["WGS 84 / UTM zone 35N",
PROJCS["WGS_1984_UTM_Zone_35N",GEOGCS["GCS_WGS_1984",DATUM["D_WGS_1984",SPHEROID["WGS_1984",6378137.0,298.257223563]],PRIMEM["Greenwich",0.0],UNIT["Degree",0.0174532925199433]],PROJECTION["Transverse_Mercator"],PARAMETER["False_Easting",500000.0],PARAMETER["False_Northing",0.0],PARAMETER["Central_Meridian",27.0],PARAMETER["Scale_Factor",0.9996],PARAMETER["Latitude_Of_Origin",0.0],UNIT["Meter",1.0]]
ProjectedCRS Transverse Mercator 0 27 0.9996 500000 0

Read where a CRS applies and the order of its axes

proj_get_area_of_use gives the region EPSG defines a CRS for, as a bounding box in degrees and a description. proj_crs_get_coordinate_system and proj_cs_get_axis_info give the axis order, which is latitude first for EPSG:4326 and northing first for Poland's grid.

src/native/crs_usage.h
#pragma once
 
#include <proj.h>
 
#include <cstdio>
#include <stdexcept>
#include <string>
 
// Where a CRS may be used and the order of its axes, both from PROJ's copy of the EPSG registry.
// Axis order is the classic trap: EPSG:4326 is latitude first, and so are many national grids.
class CrsUsage {
public:
explicit CrsUsage(const std::string& definition) : context(proj_context_create()) {
proj_log_func(context, &error, remember);
crs = proj_create(context, definition.c_str());
if (!crs || !proj_is_crs(crs)) {
const std::string reason = crs ? definition + " is not a CRS" : error.empty() ? "PROJ cannot read " + definition : error;
proj_destroy(crs);
proj_context_destroy(context);
throw std::runtime_error(reason);
}
}
 
~CrsUsage() {
proj_destroy(crs);
proj_context_destroy(context);
}
 
CrsUsage(const CrsUsage&) = delete;
CrsUsage& operator=(const CrsUsage&) = delete;
 
// [west, south, east, north, "description"], in degrees.
std::string areaOfUse() {
double west, south, east, north;
const char* name = nullptr;
if (!proj_get_area_of_use(context, crs, &west, &south, &east, &north, &name)) throw std::runtime_error("this CRS has no area of use");
char box[128];
std::snprintf(box, sizeof box, "[%.10g,%.10g,%.10g,%.10g,", west, south, east, north);
return box + quote(name) + "]";
}
 
// [["Easting","E","east","metre"], ...]: name, abbreviation, direction and unit of each axis,
// in the order coordinates are written.
std::string axes() {
PJ* system = proj_crs_get_coordinate_system(context, crs);
if (!system) throw std::runtime_error("this CRS has no coordinate system");
std::string json = "[";
for (int index = 0; index < proj_cs_get_axis_count(context, system); index += 1) {
const char* name = nullptr;
const char* abbreviation = nullptr;
const char* direction = nullptr;
const char* unit = nullptr;
proj_cs_get_axis_info(context, system, index, &name, &abbreviation, &direction, nullptr, &unit, nullptr, nullptr);
json += std::string(index ? ",[" : "[") + quote(name) + "," + quote(abbreviation) + "," + quote(direction) + "," + quote(unit) + "]";
}
proj_destroy(system);
return json + "]";
}
 
private:
static std::string quote(const char* text) {
std::string out = "\"";
for (const char* at = text ? text : ""; *at; at += 1) {
if (*at == '"' || *at == '\\') out += '\\';
out += *at;
}
return out + "\"";
}
 
static void remember(void* error, int level, const char* message) {
if (level == PJ_LOG_ERROR) *static_cast<std::string*>(error) = message;
}
 
PJ_CONTEXT* context;
PJ* crs = nullptr;
std::string error;
};
main.js
import { initNative, CrsUsage } from './native/crs_usage.h';
 
await initNative();
for (const code of ['EPSG:4326', 'EPSG:2180', 'EPSG:2056']) {
const crs = await new CrsUsage(code);
const axes = JSON.parse(await crs.axes()).map(([, abbreviation, direction, unit]) => `${abbreviation} ${direction} (${unit})`);
const [west, south, east, north, description] = JSON.parse(await crs.areaOfUse());
console.log(`${code}: ${axes.join(', ')} | ${description} ${west} ${south} ${east} ${north}`);
}
PRINTS
EPSG:4326: Lat north (degree), Lon east (degree) | World. -180 -90 180 90
EPSG:2180: x north (metre), y east (metre) | Poland - onshore and offshore. 14.14 49 24.15 55.93
EPSG:2056: E east (metre), N north (metre) | Liechtenstein; Switzerland. 5.95 45.81 10.5 47.81

Measure distances, headings and areas on the ellipsoid

geod_inverse gives the shortest route between two points on the WGS 84 ellipsoid and its headings, geod_direct where a heading and a distance lead, and geod_polygonarea the area of a polygon. This is GeographicLib's algorithm, part of PROJ; it needs no CRS and no proj.db.

src/native/geodesy.h
#pragma once
 
#include <geodesic.h>
 
#include <cstdio>
#include <cstdlib>
#include <stdexcept>
#include <string>
#include <vector>
 
// Distances, headings and areas on the WGS 84 ellipsoid with geodesic.h, the C version of Charles
// Karney's GeographicLib that ships inside PROJ. Angles are in degrees, headings clockwise from north.
class Geodesy {
public:
// [meters, azimuth1, azimuth2]: the length of the shortest route between two points, and the
// heading at its start and at its end.
static std::string inverse(double lat1, double lon1, double lat2, double lon2) {
double meters, azimuth1, azimuth2;
geod_inverse(&wgs84(), lat1, lon1, lat2, lon2, &meters, &azimuth1, &azimuth2);
return json(meters, azimuth1, azimuth2);
}
 
// [lat, lon, azimuth]: where you arrive after `meters` from a point on a heading, and the
// heading you arrive with.
static std::string direct(double lat, double lon, double azimuth, double meters) {
double lat2, lon2, azimuth2;
geod_direct(&wgs84(), lat, lon, azimuth, meters, &lat2, &lon2, &azimuth2);
return json(lat2, lon2, azimuth2);
}
 
// [m2, perimeter] of a polygon whose corners are latitude, longitude pairs in any text, such
// as JSON: [[lat, lon], [lat, lon], ...]. Counter-clockwise corners give a positive area.
static std::string area(const std::string& corners) {
const std::vector<double> numbers = numbersIn(corners);
if (numbers.size() < 6 || numbers.size() % 2) throw std::invalid_argument("a polygon needs at least three latitude, longitude pairs");
std::vector<double> lats, lons;
for (size_t index = 0; index < numbers.size(); index += 2) {
lats.push_back(numbers[index]);
lons.push_back(numbers[index + 1]);
}
double m2, perimeter;
geod_polygonarea(&wgs84(), lats.data(), lons.data(), static_cast<int>(lats.size()), &m2, &perimeter);
char text[64];
std::snprintf(text, sizeof text, "[%.17g,%.17g]", m2, perimeter);
return text;
}
 
private:
static const geod_geodesic& wgs84() {
static const geod_geodesic ellipsoid = [] {
geod_geodesic g;
geod_init(&g, 6378137, 1 / 298.257223563);
return g;
}();
return ellipsoid;
}
 
static std::string json(double a, double b, double c) {
char text[96];
std::snprintf(text, sizeof text, "[%.17g,%.17g,%.17g]", a, b, c);
return text;
}
 
static std::vector<double> numbersIn(const std::string& text) {
std::vector<double> numbers;
const char* at = text.c_str();
while (*at) {
char* end = nullptr;
const double value = std::strtod(at, &end);
if (end == at) {
at += 1;
} else {
numbers.push_back(value);
at = end;
}
}
return numbers;
}
};
main.js
import { initNative, Geodesy } from './native/geodesy.h';
 
await initNative();
const compass = (azimuth) => ((azimuth + 360) % 360).toFixed(2);
const [meters, departure, arrival] = JSON.parse(await Geodesy.inverse(41.0082, 28.9784, 40.6413, -73.7781)); // Istanbul to New York JFK
console.log(`${(meters / 1000).toFixed(3)} km, leaving on ${compass(departure)}°, arriving on ${compass(arrival)}°`);
const [lat, lon] = JSON.parse(await Geodesy.direct(41.0082, 28.9784, departure, meters / 2));
console.log(`halfway at ${lat.toFixed(4)}, ${lon.toFixed(4)}`); // far north of both cities
const triangle = [[25.7617, -80.1918], [18.4655, -66.1057], [32.3078, -64.7505]]; // Miami, San Juan, Bermuda
const [m2, perimeter] = JSON.parse(await Geodesy.area(JSON.stringify(triangle)));
console.log(`${(m2 / 1e6).toFixed(1)} km², perimeter ${(perimeter / 1000).toFixed(1)} km`);
PRINTS
8080.310 km, leaving on 309.12°, arriving on 230.50°
halfway at 54.1948, -22.5976
1145170.4 km², perimeter 4868.1 km

Find the EPSG code of a .prj, and the UTM zone of a point

A shapefile's .prj names no EPSG code. proj_identify matches it against the registry, with a confidence that drops when names are missing, as in a PROJ string. proj_get_crs_info_list_from_database lists the CRSs whose area of use contains a point.

src/native/crs_lookup.h
#pragma once
 
#include <proj.h>
 
#include <stdexcept>
#include <string>
 
// Finds CRSs in PROJ's copy of the EPSG registry: the code that a .prj file or any WKT describes,
// and the projected CRSs that may be used at a point.
class CrsLookup {
public:
// [["EPSG:32635","WGS 84 / UTM zone 35N",100], ...]: code, name and confidence, best match
// first. The confidence is 100 when the definition matches an EPSG entry exactly, names
// included, and lower as details are missing or differ.
static std::string identify(const std::string& definition) {
Session proj;
PJ* crs = proj.create(definition);
int* confidence = nullptr;
PJ_OBJ_LIST* matches = proj_identify(proj.context, crs, "EPSG", nullptr, &confidence);
std::string json = "[";
for (int index = 0; matches && index < proj_list_get_count(matches); index += 1) {
PJ* match = proj_list_get(proj.context, matches, index);
json += std::string(index ? ",[" : "[") + entry(proj_get_id_auth_name(match, 0), proj_get_id_code(match, 0), proj_get_name(match)) + "," +
std::to_string(confidence[index]) + "]";
proj_destroy(match);
}
proj_int_list_destroy(confidence);
proj_list_destroy(matches);
proj_destroy(crs);
return json + "]";
}
 
// [["EPSG:32635","WGS 84 / UTM zone 35N"], ...]: the projected CRSs whose area of use
// contains the point and whose name contains `words`, deprecated ones left out.
static std::string projectedAt(double lat, double lon, const std::string& words) {
Session proj;
PROJ_CRS_LIST_PARAMETERS* filter = proj_get_crs_list_parameters_create();
const PJ_TYPE projected = PJ_TYPE_PROJECTED_CRS;
filter->types = &projected;
filter->typesCount = 1;
filter->bbox_valid = 1;
filter->west_lon_degree = filter->east_lon_degree = lon;
filter->south_lat_degree = filter->north_lat_degree = lat;
filter->crs_area_of_use_contains_bbox = 1;
int count = 0;
PROJ_CRS_INFO** found = proj_get_crs_info_list_from_database(proj.context, "EPSG", filter, &count);
std::string json = "[";
for (int index = 0; index < count; index += 1) {
const PROJ_CRS_INFO* crs = found[index];
if (std::string(crs->name).find(words) == std::string::npos) continue;
json += std::string(json.size() > 1 ? ",[" : "[") + entry(crs->auth_name, crs->code, crs->name) + "]";
}
proj_crs_info_list_destroy(found);
proj_get_crs_list_parameters_destroy(filter);
return json + "]";
}
 
private:
// One PROJ context per call; it also keeps the last error PROJ logged.
struct Session {
PJ_CONTEXT* context = proj_context_create();
std::string error;
 
Session() { proj_log_func(context, &error, remember); }
~Session() { proj_context_destroy(context); }
Session(const Session&) = delete;
Session& operator=(const Session&) = delete;
 
PJ* create(const std::string& definition) {
PJ* crs = proj_create(context, definition.c_str());
if (crs && proj_is_crs(crs)) return crs;
proj_destroy(crs);
throw std::runtime_error(error.empty() ? "PROJ cannot read this CRS definition" : error);
}
 
static void remember(void* error, int level, const char* message) {
if (level == PJ_LOG_ERROR) *static_cast<std::string*>(error) = message;
}
};
 
// "EPSG:32635","WGS 84 / UTM zone 35N"
static std::string entry(const char* authority, const char* code, const char* name) {
const std::string id = authority && code ? std::string(authority) + ":" + code : "";
return quote(id.c_str()) + "," + quote(name);
}
 
static std::string quote(const char* text) {
std::string out = "\"";
for (const char* at = text ? text : ""; *at; at += 1) {
if (*at == '"' || *at == '\\') out += '\\';
out += *at;
}
return out + "\"";
}
};
main.js
import { initNative, CrsLookup } from './native/crs_lookup.h';
 
await initNative();
const prj =
'PROJCS["WGS_1984_UTM_Zone_35N",GEOGCS["GCS_WGS_1984",DATUM["D_WGS_1984",SPHEROID["WGS_1984",6378137.0,298.257223563]],PRIMEM["Greenwich",0.0],UNIT["Degree",0.0174532925199433]],PROJECTION["Transverse_Mercator"],PARAMETER["False_Easting",500000.0],PARAMETER["False_Northing",0.0],PARAMETER["Central_Meridian",27.0],PARAMETER["Scale_Factor",0.9996],PARAMETER["Latitude_Of_Origin",0.0],UNIT["Meter",1.0]]';
for (const definition of [prj, '+proj=utm +zone=35 +datum=WGS84 +units=m +no_defs +type=crs']) {
const [[code, name, confidence]] = JSON.parse(await CrsLookup.identify(definition)); // the best match
console.log(`${code} ${name}, confidence ${confidence}`);
}
for (const [place, lat, lon] of [['Istanbul', 41.0082, 28.9784], ['Sydney', -33.8688, 151.2093]]) {
const [[code, name]] = JSON.parse(await CrsLookup.projectedAt(lat, lon, 'WGS 84 / UTM zone'));
console.log(`${place}: ${code} ${name}`);
}
PRINTS
EPSG:32635 WGS 84 / UTM zone 35N, confidence 100
EPSG:32635 WGS 84 / UTM zone 35N, confidence 70
Istanbul: EPSG:32635 WGS 84 / UTM zone 35N
Sydney: EPSG:32756 WGS 84 / UTM zone 56S

What is different on Android

  • The React Native plugin compiles your headers with the library inside Gradle's native build, so npm run android builds everything.
  • Named imports from ./native/<header>.h work as on the web: await initNative() once, then call the classes.
  • There is no m.FS and no /memfs: files live in the app's own storage, and your C++ takes their paths.
  • No Worker and no COOP or COEP: runtime: 'mt' uses pthreads directly.

Other platforms

Facts on this page come from the port manifests in the repository and from what npm served on beta when the site was built. See the Libraries guide for the full consumer flow.

MORE LIBRARIES
cURLExpatGDALGEOSGeoTIFFiconvLERClibjpeg-turbolibTIFFOpenSSLSpatiaLiteSQLiteWebPzlibZstandard
Type to search every guide page and section.
↑↓ navigate↵ openesc close