crossbind
GitHub

PROJ

v9.9.0Geospatial

PROJ 9.9.0, coordinate reference-system transformations, packaged by crossbind as @crossbind/port-proj and one package per target. Only a variant that is actually on npm beta is listed as published.

npm install @crossbind/port-proj-wasm@beta
LIVE · 3 APPS · RUNS IN THIS TAB

The coordinate engine behind QGIS and PostGIS, in your browser

PROJ is the library GDAL, QGIS, PostGIS and pyproj call to convert coordinates between reference systems. These apps run PROJ 9.9.0, compiled by crossbind, with the EPSG registry v13.102 in its proj.db: any projected CRS drawn with what it distorts, a .prj traced to its EPSG code and to every transformation path PROJ knows, and the shortest flight route next to the one that holds a compass heading. proj.db, PROJ's 10-megabyte database, is most of the download. The first run downloads 16.1 MB of WebAssembly once; every app on this page shares it, and nothing is uploaded.

APP 01

See what a map projection stretches

Pick a world map, or any projected CRS by its EPSG or ESRI code, WKT or PROJ string. PROJ draws its graticule and a grid of circles that are all the same size on the ground, over the area the CRS is defined for, so the shapes the circles take show what the projection stretches. Click the map to measure the distortion at that point.

The circles are geodesic circles, every point the same distance from the centre on the ellipsoid, from PROJ's geodesic.h. A code brings its area of use from the EPSG or ESRI registry; a PROJ string carries none, so the whole world is drawn.

Runs PROJ 9.9.0, which is MIT-licensed: source · build recipe
Draw a projection to see its graticule and circles.
SHOW THE CODE
src/support/map_projection.h
// src/support/map_projection.h (excerpt)
// From the CRS's own geographic CRS to its map coordinates, so drawing never
// shifts datums, then longitude and easting first whatever the axis order.
const Object geographic = proj.own(proj_crs_get_geodetic_crs(proj.get(), crs.get()), "...");
const Object operation = proj.own(proj_create_crs_to_crs_from_pj(proj.get(), geographic.get(),
crs.get(), nullptr, nullptr), "...");
pipeline = proj.own(proj_normalize_for_visualization(proj.get(), operation.get()), "...");
 
// Scale factors at a point, from PROJ's derivatives of the projection.
out = proj_factors(crs.get(), proj_coord(lon * kRadiansPerDegree, lat * kRadiansPerDegree, 0, 0));
main.js
const m = await initNative();
const atlas = await new m.ProjectionAtlas();
const map = JSON.parse(await atlas.draw('EPSG:8857')); // Equal Earth
// map.graticule, map.outline, map.circles: runs of x, y in metres
const [x, y] = JSON.parse(await atlas.project(30, 40)); // 2549249.49, 4921020.06
 
await atlas.draw('EPSG:3395'); // World Mercator
const at60 = JSON.parse(await atlas.distortion(0, 60));
// at60.areal: 3.97992, so areas at 60°N are drawn four times too big
APP 02

Find out what a .prj is, and every way PROJ can transform it

A shapefile's .prj names no EPSG code. Paste one, or any WKT, PROJJSON, PROJ string or code, and PROJ matches it against its copy of the EPSG registry: the code, where the CRS applies, its axis order, the same CRS in four formats, and every transformation path to a second CRS with its accuracy. proj4js, the JavaScript port, knows only the CRSs you define with proj4.defs() and the few it predefines, so it has no registry to match a .prj against.

OR SEARCH THE REGISTRY BY NAME OR CODE
Runs PROJ 9.9.0, which is MIT-licensed: source · build recipe
Identify a CRS to see its code, where it applies, its axes and every path to another CRS.
SHOW THE CODE
src/native/crs_detective.h
// src/native/crs_detective.h (excerpt)
PJ_OPERATION_FACTORY_CONTEXT* factory = proj_create_operation_factory_context(proj.get(), nullptr);
proj_operation_factory_context_set_spatial_criterion(proj.get(), factory,
PROJ_SPATIAL_CRITERION_PARTIAL_INTERSECTION);
// IGNORED lists every path as if all grids were installed;
// DISCARD_OPERATION_IF_MISSING_GRID lists what this build can run.
proj_operation_factory_context_set_grid_availability_use(proj.get(), factory, grids);
PJ_OBJ_LIST* found = proj_create_operations(proj.get(), from, to, factory);
 
// A .prj names no code: proj_identify matches it against the registry.
PJ_OBJ_LIST* matches = proj_identify(proj.get(), crs, nullptr, nullptr, &confidence);
main.js
const m = await initNative();
const detective = await new m.CrsDetective();
const crs = JSON.parse(await detective.inspect(prjText));
// crs.identified[0]: EPSG:32635, confidence 100
// crs.area, from EPSG:32635: 24°E to 30°E, 0° to 84°N
 
const paths = JSON.parse(await detective.operations('EPSG:4267', 'EPSG:4269'));
// paths.known: 10 operations, 9 of them need a grid this build does not ship
// paths.chosen.name: 'NAD27 to WGS 84 (6) + Inverse of NAD83 to WGS 84 (1)'
APP 03

Why flights curve on a map

The shortest route between two cities is a geodesic on the ellipsoid. On Mercator it bends toward the pole, while the straight line there is the route that keeps one compass heading, and that one is longer. Pick two cities: PROJ measures both routes on the WGS 84 ellipsoid and draws them on three maps.

The shortest route comes from geod_inverse, Charles Karney's algorithm, which PROJ carries as geodesic.h. The constant heading, a rhumb line, is the straight line between the two cities on ellipsoidal Mercator, which PROJ unprojects point by point.

Runs PROJ 9.9.0, which is MIT-licensed: source · build recipe
Pick two cities to draw the shortest route and the constant heading.
SHOW THE CODE
src/native/flight_paths.h
// src/native/flight_paths.h and src/support/map_projection.h (excerpts)
// The shortest route: points along the geodesic, from geodesic.h.
geod_inverseline(&route, &ellipsoid, lat1, lon1, lat2, lon2, 0);
geod_position(&route, route.s13 * index / points, &lat, &lon, nullptr);
 
// The constant heading: a straight line on ellipsoidal Mercator (EPSG:3395),
// which is conformal, unprojected back to longitude and latitude.
x2 = x1 + std::remainder(x2 - x1, world); // the short way round
heading = std::fmod(std::atan2(x2 - x1, y2 - y1) / projapp::kRadiansPerDegree + 360, 360.0);
mercator.unproject(x1 + t * (x2 - x1), y1 + t * (y2 - y1), lon, lat);
main.js
const m = await initNative();
const flights = await new m.FlightPaths();
const route = JSON.parse(await flights.route(41.0082, 28.9784, 40.6413, -73.7781));
// Istanbul to New York JFK: route.geodesic.meters 8080310.14,
// route.rhumb.meters 8668353.46 at a heading of 269.73°
 
const map = JSON.parse(await flights.draw('EPSG:3395', 41.0082, 28.9784, 40.6413, -73.7781));
// map.geodesic and map.rhumb: runs of x, y on World Mercator

Usage

The calls most PROJ code makes, each a small C++ header crossbind binds and the JavaScript that uses it. Every example runs here in WebAssembly and prints what the site build checked; the same headers and calls work on Android and iOS.

Each example also has a JavaScript only tab: the same task with no C++ file, calling PROJ's own headers from @crossbind/port-proj directly. All 5 work that way.

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));
PRINTSfirst run downloads 16.1 MB
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(' '));
PRINTSfirst run downloads 16.1 MB
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}`);
}
PRINTSfirst run downloads 16.1 MB
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`);
PRINTSfirst run downloads 16.1 MB
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}`);
}
PRINTSfirst run downloads 16.1 MB
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

Add it to your project

One package per platform: install the ones you build for and list each in crossbind.config.js; crossbind compiles only the one that matches the build target. Your C++ goes in src/native, next to the headers it binds. Libraries explains the whole flow.

shell
npm install @crossbind/port-proj-wasm@beta
crossbind.config.js
import projWasm from '@crossbind/port-proj-wasm/crossbind.config.js';
 
export default {
dependencies: [projWasm],
paths: { config: import.meta.url },
};

Platforms

PlatformRuns inBuildsPage
WebAssemblybrowsers, Node.js and edge runtimeswasm32, single-threaded and multi-threadedPROJ for WebAssembly
AndroidReact Native apps on Androidarm64-v8a devices and the x86_64 emulatorPROJ for Android
iOSReact Native apps on iOSarm64 devices and simulatorsPROJ for iOS
macOSnative Node.js addons and Electron on macOSarm64 and x64, macOS 11 or laterPROJ for macOS
Linuxnative Node.js addons on Linuxx64 and arm64, glibc 2.28 or laterPROJ for Linux
Windowsnative Node.js addons on Windowsx64 and arm64, Windows 10 or laterPROJ for Windows
WASIcommand-line programs under wasmtimewasm32-wasip3, single-threadedPROJ for WASI

Packages

TargetPackagenpm `beta`
Meta package@crossbind/port-proj2.0.0-beta.62
Web and Node.js@crossbind/port-proj-wasm2.0.0-beta.62
WASI library@crossbind/port-proj-wasi2.0.0-beta.62
WASI commands@crossbind/port-proj-standalone-wasinot published
Android@crossbind/port-proj-android2.0.0-beta.62
iOS@crossbind/port-proj-ios2.0.0-beta.62
macOS@crossbind/port-proj-darwin2.0.0-beta.62
Linux@crossbind/port-proj-linux2.0.0-beta.62
Linux (musl)@crossbind/port-proj-linuxmuslnot published
Windows@crossbind/port-proj-win322.0.0-beta.62

Licence

  • npm license field of @crossbind/port-proj: MIT.
  • The licence files that ship with the package, and the port recipe, are in the port directory.

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