crossbind
GitHub
← All libraries

GDAL

Raster and vector geospatial data processing.

Install

Runs in browsers; Node.js and edge runtimes use the same package. Built for wasm32, single-threaded and multi-threaded.

shell
npm install @crossbind/port-gdal-wasm@beta
crossbind.config.js
import gdalWasm from '@crossbind/port-gdal-wasm/crossbind.config.js';
 
export default {
dependencies: [gdalWasm],
paths: { config: import.meta.url },
};
main.js
import { initNative, GDALAllRegister } from '@crossbind/port-gdal/gdal.h';
 
await initNative();

crossbind itself comes with your bundler plugin; Browser has Vite, Webpack, Rspack and Rollup.

GDAL in your browser: twenty vector formats, terrain analysis and pixels to polygons

GDAL is the format library behind QGIS, PostGIS, MapServer and rasterio. These apps run GDAL 3.13.3, compiled by crossbind: they read twenty vector formats and write seventeen, with reprojection, from zipped Shapefiles to Esri File Geodatabases; they shade and contour a terrain and compute what is seen from a point; and they trace painted pixels into polygons. The page registers 25 of the port's 180 drivers, one by one. GDAL is MIT; the module also contains GEOS and libiconv, which are LGPL, and every app links their sources. The first run downloads 29 MB of WebAssembly once; every app on this page shares it, and nothing is uploaded.

APP 01

Open twenty vector formats and write seventeen, reprojected, in the page

Drop a Shapefile (its parts or a zip), a GeoPackage, KML, GPX, GeoJSON, FlatGeobuf, DXF, CSV, an Excel or OpenDocument sheet, MapInfo TAB, GML, PMTiles or an Esri File Geodatabase, and take it away as another of them, in another coordinate system, filtered by a SQL condition. GDAL does what its ogr2ogr tool does, on this device; the file is never uploaded.

Or drop them here. Give a Shapefile all its parts (.shp, .shx, .dbf, .prj) or a zip of them. The sample is a GeoPackage the module writes: eight Turkish cities, the lines between some of them and two regions.

GDAL is MIT; this module also links GEOS and libiconv, which are LGPL: license · GEOS source · libiconv source · build recipe
Open the sample or a file of your own to see its layers, then convert it.
SHOW THE CODE
src/native/vector_studio.h
// src/native/vector_studio.h (excerpt): ogr2ogr with the arguments the page chose
CPLStringList args(CSLTokenizeString2(arguments.c_str(), "\n", CSLT_ALLOWEMPTYTOKENS));
GDALVectorTranslateOptions* options = GDALVectorTranslateOptionsNew(args.List(), nullptr);
GDALDatasetH written = GDALVectorTranslate(output.c_str(), nullptr, 1, &source, options, nullptr);
GDALVectorTranslateOptionsFree(options);
 
// src/support/drivers.h: registered one by one, so only these drivers are linked
RegisterOGRGeoJSON(); RegisterOGRShape(); RegisterOGRGeoPackage(); RegisterOGRFlatGeobuf();
RegisterOGRKML(); RegisterOGRGPX(); RegisterOGRCSV(); RegisterOGRDXF(); RegisterOGROpenFileGDB();
RegisterOGRXLSX(); RegisterOGRODS(); RegisterOGRGML(); RegisterOGRTAB(); RegisterOGRPMTiles(); // ...
main.js
const m = await initNative();
await new m.VectorStudio();
const [mounted] = await m.autoMountFiles([file], await m.getRandomPath('/memfs'));
const input = await m.VectorStudio.resolve(mounted); // looks into zips and .gdb folders
const { layers } = JSON.parse(await m.VectorStudio.inspect(input)); // ogrinfo -json
 
await m.FS.mkdirTree('/memfs/out');
const args = ['-f', 'GPKG', '-t_srs', 'EPSG:3857'].join('\n');
const result = JSON.parse(await m.VectorStudio.convert(input, '/memfs/out/data.gpkg', args));
const bytes = await m.getFileBytes(result.download); // zipped when GDAL wrote several files
APP 02

A terrain studio: hillshade, slope, contour lines and what is seen from where you click

The module generates a 15 km landscape and GDAL analyses it in the page: the gdaldem products, contour lines at the interval you pick, and a viewshed from any point you click, with the Earth's curvature. Every product downloads as a georeferenced GeoTIFF.

PRODUCT

Height coloured from green lowlands to white peaks, then shaded by a hillshade. Click the map to place the observer. The terrain is gradient noise from seed 1, 512 × 512 cells of 30 m placed in UTM zone 35N; it is not a real place.

GDAL is MIT; this module also links GEOS and libiconv, which are LGPL: license · GEOS source · libiconv source · build recipe
Generate the terrain to see it shaded, measured and contoured.
SHOW THE CODE
src/native/terrain_studio.h
// src/native/terrain_studio.h (excerpt): gdaldem into memory, then coloured
GDALDEMProcessingOptions* options = GDALDEMProcessingOptionsNew(args.List(), nullptr); // -of MEM -compute_edges
GDALDatasetH values = GDALDEMProcessing("", source, "slope", nullptr, options, nullptr);
GDALDatasetH coloured = GDALDEMProcessing("", values, "color-relief", "/vsimem/colours.txt", alpha, nullptr);
 
// gdal_viewshed from a clicked pixel, with the Earth's curvature and refraction
GDALDatasetH seen = GDALViewshedGenerate(band, "MEM", "", nullptr, x, y, height, 0, 255, 0, 0, -1,
0.85714, GVM_Edge, 0, nullptr, nullptr, GVOT_NORMAL, nullptr);
main.js
const m = await initNative();
await new m.TerrainStudio();
await m.FS.mkdirTree('/memfs/terrain');
const dem = '/memfs/terrain/dem.tif';
const model = JSON.parse(await m.TerrainStudio.create(dem, 512, 1)); // 493.6 to 1752.7 m
 
const view = JSON.parse(await m.TerrainStudio.render(dem, 'slope', 315, '/memfs/terrain/slope.rgba'));
const rgba = await m.getFileBytes('/memfs/terrain/slope.rgba'); // 512 x 512 RGBA for a canvas
const lines = JSON.parse(await m.TerrainStudio.contours(dem, 100)); // GeoJSON, 346 lines
const seen = JSON.parse(await m.TerrainStudio.viewshed(dem, 256, 256, 10, '/memfs/terrain/seen.bin'));
// seen.visible: 3123 of 262144 cells
APP 03

Pixels to polygons: paint, and GDAL traces the shapes

Paint on the grid, or keep the sample. GDAL removes the specks below a size you pick, traces every region of ink into a polygon with its holes, simplifies the staircase edges and saves the result as GeoJSON, GeoPackage, Shapefile or DXF. The same calls turn classified satellite images and scanned maps into vector layers.

BRUSH
GDAL is MIT; this module also links GEOS and libiconv, which are LGPL: license · GEOS source · libiconv source · build recipe
Trace the polygons to see them drawn over the grid, with their areas.
SHOW THE CODE
src/native/vectorizer.h
// src/native/vectorizer.h (excerpt): specks out, then every region of ink as a polygon
if (sieve > 1) GDALSieveFilter(band, nullptr, band, sieve, 4, nullptr, nullptr, nullptr);
OGRLayerH layer = GDALDatasetCreateLayer(store, "shapes", nullptr, wkbPolygon, nullptr);
// the band is its own mask, so background pixels (0) make no polygon
GDALPolygonize(band, band, layer, 0, nullptr, nullptr, nullptr);
// then ogr2ogr -simplify <tolerance>, which keeps each polygon valid
main.js
const m = await initNative();
await new m.Vectorizer();
await m.FS.mkdirTree('/memfs/trace/out');
await m.FS.writeFile('/memfs/trace/mask.bin', mask); // 128 x 128 bytes, 0 = background
const traced = JSON.parse(await m.Vectorizer.trace('/memfs/trace/mask.bin', 128, 128, 5, 1.5));
// traced.polygons: 3, traced.holes: 1, traced.areas: [1500, 1400, 1257]
const saved = JSON.parse(await m.Vectorizer.save('/memfs/trace/mask.bin', 128, 128, 5, 1.5, 'GPKG', '/memfs/trace/out/shapes.gpkg'));

Examples

Each one is a small C++ header and the JavaScript that calls it, with what it printed when the site was built. The JavaScript only tab does the same with GDAL's own headers and no C++ file. The last is a whole C++ program, built for WASI.

Convert GeoJSON to GeoPackage and Shapefile, reprojected

Format conversion is what GDAL is used for most, as the ogr2ogr tool: GDALVectorTranslate takes the same arguments, here -f for the format and -t_srs for the coordinate system. The input is text handed over through /vsimem/, and a zipped Shapefile is one file.

src/native/vector_converter.h
#pragma once
 
#include <cpl_error.h>
#include <cpl_string.h>
#include <cpl_vsi.h>
#include <gdal.h>
#include <gdal_utils.h>
#include <ogr_srs_api.h>
#include <ogrsf_frmts.h>
 
#include <cmath>
#include <stdexcept>
#include <string>
 
// Converts GeoJSON text to another vector format, reprojected, and reports what was written.
// It registers only the drivers it uses, so GDAL opens and writes these formats and no others.
class VectorConverter {
public:
VectorConverter() {
RegisterOGRGeoJSON();
RegisterOGRGeoPackage();
RegisterOGRFlatGeobuf();
RegisterOGRShape();
}
 
std::string convert(const std::string& geojson, const std::string& format, const std::string& targetCrs,
const std::string& outputPath) {
const char* input = "/vsimem/input.geojson";
VSIFCloseL(VSIFileFromMemBuffer(input, reinterpret_cast<GByte*>(const_cast<char*>(geojson.data())), geojson.size(), FALSE));
GDALDatasetH source = GDALOpenEx(input, GDAL_OF_VECTOR, nullptr, nullptr, nullptr);
if (!source) fail(input);
 
CPLStringList args;
args.AddString("-f");
args.AddString(format.c_str());
args.AddString("-t_srs");
args.AddString(targetCrs.c_str());
GDALVectorTranslateOptions* options = GDALVectorTranslateOptionsNew(args.List(), nullptr);
VSIUnlink(outputPath.c_str()); // replace the output of an earlier call
GDALDatasetH written = GDALVectorTranslate(outputPath.c_str(), nullptr, 1, &source, options, nullptr);
GDALVectorTranslateOptionsFree(options);
GDALClose(source);
if (!written) fail(input);
 
OGRLayerH layer = GDALDatasetGetLayer(written, 0);
OGREnvelope extent;
OGR_L_GetExtent(layer, &extent, TRUE);
OGRSpatialReferenceH crs = OGR_L_GetSpatialRef(layer);
const char* authority = crs ? OSRGetAuthorityName(crs, nullptr) : nullptr;
const char* code = crs ? OSRGetAuthorityCode(crs, nullptr) : nullptr;
const std::string summary = format + ": " + std::to_string(OGR_L_GetFeatureCount(layer, TRUE)) + " features, " +
(authority && code ? std::string(authority) + ":" + code : std::string("no CRS")) + ", extent " +
std::to_string(std::llround(extent.MinX)) + " " + std::to_string(std::llround(extent.MinY)) + " " +
std::to_string(std::llround(extent.MaxX)) + " " + std::to_string(std::llround(extent.MaxY));
GDALClose(written);
VSIUnlink(input);
return summary;
}
 
private:
[[noreturn]] static void fail(const char* input) {
const std::string reason = CPLGetLastErrorMsg();
VSIUnlink(input);
throw std::runtime_error(reason.empty() ? "GDAL could not convert the data" : reason);
}
};
main.js
import { initNative, VectorConverter } from './native/vector_converter.h';
 
await initNative();
const converter = await new VectorConverter();
const cities = JSON.stringify({
type: 'FeatureCollection',
features: [
['Istanbul', 28.9784, 41.0082],
['Ankara', 32.8597, 39.9334],
['Izmir', 27.1428, 38.4237],
].map(([name, lon, lat]) => ({ type: 'Feature', properties: { name }, geometry: { type: 'Point', coordinates: [lon, lat] } })),
});
console.log(await converter.convert(cities, 'GPKG', 'EPSG:3857', '/vsimem/cities.gpkg'));
console.log(await converter.convert(cities, 'ESRI Shapefile', 'EPSG:32635', '/vsimem/cities.shp.zip'));
PRINTSfirst run downloads 29 MB
GPKG: 3 features, EPSG:3857, extent 3021523 4639455 3657925 5013551
ESRI Shapefile: 3 features, EPSG:32635, extent 512465 4252837 1000822 4541552

Write a GeoTIFF and read its georeferencing back

GDALCreate writes a raster with GDALSetGeoTransform and a CRS from OSRImportFromEPSG; GDALOpenEx opens it again as it opens any of the formats GDAL reads, and the size, band type, geotransform and CRS come back from the dataset. GDALInfo returns the report the gdalinfo tool prints.

src/native/raster_info.h
#pragma once
 
#include <cpl_error.h>
#include <cpl_string.h>
#include <gdal.h>
#include <gdal_frmts.h>
#include <gdal_utils.h>
#include <ogr_srs_api.h>
 
#include <cstdio>
#include <sstream>
#include <stdexcept>
#include <string>
#include <vector>
 
// Writes a GeoTIFF with a coordinate system, then opens it the way any raster is opened and reads
// what GDAL knows about it: the size, the bands, where the pixels lie (the geotransform) and in which CRS.
class RasterInfo {
public:
RasterInfo() { GDALRegister_GTiff(); }
 
// Elevations in metres that rise by 1 m a pixel to the east and 2 m a pixel to the south.
void create(const std::string& path, int width, int height, double west, double north, double pixelSize, int epsg) {
char** options = CSLSetNameValue(nullptr, "COMPRESS", "DEFLATE");
GDALDatasetH dataset = GDALCreate(GDALGetDriverByName("GTiff"), path.c_str(), width, height, 1, GDT_Float32, options);
CSLDestroy(options);
if (!dataset) fail();
double transform[6] = {west, pixelSize, 0, north, 0, -pixelSize};
GDALSetGeoTransform(dataset, transform);
OGRSpatialReferenceH crs = OSRNewSpatialReference(nullptr);
OSRImportFromEPSG(crs, epsg);
GDALSetSpatialRef(dataset, crs);
OSRDestroySpatialReference(crs);
GDALRasterBandH band = GDALGetRasterBand(dataset, 1);
std::vector<float> row(width);
for (int y = 0; y < height; ++y) {
for (int x = 0; x < width; ++x) row[x] = static_cast<float>(100 + x + 2 * y);
if (GDALRasterIO(band, GF_Write, 0, y, width, 1, row.data(), width, 1, GDT_Float32, 0, 0) != CE_None) {
GDALClose(dataset);
fail();
}
}
GDALClose(dataset);
}
 
std::string describe(const std::string& path) {
GDALDatasetH dataset = GDALOpenEx(path.c_str(), GDAL_OF_RASTER, nullptr, nullptr, nullptr);
if (!dataset) fail();
GDALRasterBandH band = GDALGetRasterBand(dataset, 1);
double t[6];
GDALGetGeoTransform(dataset, t);
OGRSpatialReferenceH crs = GDALGetSpatialRef(dataset);
double range[2];
GDALComputeRasterMinMax(band, FALSE, range);
 
char line[160];
std::string text = std::string(GDALGetDriverShortName(GDALGetDatasetDriver(dataset))) + ", " +
std::to_string(GDALGetRasterXSize(dataset)) + " x " + std::to_string(GDALGetRasterYSize(dataset)) + " pixels, " +
std::to_string(GDALGetRasterCount(dataset)) + " band of " + GDALGetDataTypeName(GDALGetRasterDataType(band)) + "\n";
std::snprintf(line, sizeof line, "origin %.0f, %.0f; pixel size %.0f x %.0f\n", t[0], t[3], t[1], t[5]);
text += line;
text += std::string(OSRGetName(crs)) + ", " + OSRGetAuthorityName(crs, nullptr) + ":" + OSRGetAuthorityCode(crs, nullptr) + "\n";
std::snprintf(line, sizeof line, "values %.0f to %.0f\n", range[0], range[1]);
text += line;
 
// GDALInfo returns the report the gdalinfo tool prints; keep its corner coordinates.
char* report = GDALInfo(dataset, nullptr);
std::istringstream lines(report ? report : "");
CPLFree(report);
GDALClose(dataset);
for (std::string entry; std::getline(lines, entry);) {
if (entry.rfind("Upper Left", 0) == 0 || entry.rfind("Lower Right", 0) == 0) text += entry + "\n";
}
return text.substr(0, text.size() - 1);
}
 
private:
[[noreturn]] static void fail() {
const std::string reason = CPLGetLastErrorMsg();
throw std::runtime_error(reason.empty() ? "GDAL could not read the raster" : reason);
}
};
main.js
import { initNative, RasterInfo } from './native/raster_info.h';
 
await initNative();
const raster = await new RasterInfo();
// 200 x 150 pixels of 30 m, the top left corner at 500000 E 4450000 N in UTM zone 35N
await raster.create('/vsimem/dem.tif', 200, 150, 500000, 4450000, 30, 32635);
for (const line of (await raster.describe('/vsimem/dem.tif')).split('\n')) console.log(line);
PRINTSfirst run downloads 29 MB
GTiff, 200 x 150 pixels, 1 band of Float32
origin 500000, 4450000; pixel size 30 x -30
WGS 84 / UTM zone 35N, EPSG:32635
values 100 to 597
Upper Left  (  500000.000, 4450000.000) ( 27d 0' 0.00"E, 40d12' 1.44"N)
Lower Right (  506000.000, 4445500.000) ( 27d 4'13.64"E, 40d 9'35.41"N)

Reproject a raster and write a Cloud-Optimized GeoTIFF

GDALWarp reprojects as the gdalwarp tool does, here into a virtual raster that is computed while it is read; GDALTranslate with -of COG then writes it tiled, compressed and with overviews, the layout web maps read with HTTP range requests.

src/native/cog_writer.h
#pragma once
 
#include <cpl_error.h>
#include <cpl_string.h>
#include <cpl_vsi.h>
#include <gdal.h>
#include <gdal_frmts.h>
#include <gdal_utils.h>
 
#include <cstdio>
#include <stdexcept>
#include <string>
 
// Reprojects a raster as the gdalwarp tool does, then writes it as gdal_translate -of COG does: a
// Cloud-Optimized GeoTIFF, tiled and compressed, with overviews laid out so that a client can read
// one area at one zoom level with a few HTTP range requests.
class CogWriter {
public:
CogWriter() {
GDALRegister_GTiff();
GDALRegister_COG();
GDALRegister_VRT();
}
 
std::string warpToCog(const std::string& source, const std::string& targetCrs, const std::string& output) {
GDALDatasetH input = GDALOpenEx(source.c_str(), GDAL_OF_RASTER, nullptr, nullptr, nullptr);
if (!input) fail();
 
// gdalwarp -of VRT: the reprojected raster stays virtual and is computed while the COG is written.
CPLStringList warpArgs;
for (const char* arg : {"-of", "VRT", "-r", "bilinear", "-t_srs"}) warpArgs.AddString(arg);
warpArgs.AddString(targetCrs.c_str());
GDALWarpAppOptions* warpOptions = GDALWarpAppOptionsNew(warpArgs.List(), nullptr);
GDALDatasetH warped = GDALWarp("", nullptr, 1, &input, warpOptions, nullptr);
GDALWarpAppOptionsFree(warpOptions);
if (!warped) {
GDALClose(input);
fail();
}
 
CPLStringList cogArgs;
for (const char* arg : {"-of", "COG", "-co", "COMPRESS=DEFLATE", "-co", "BLOCKSIZE=256"}) cogArgs.AddString(arg);
GDALTranslateOptions* cogOptions = GDALTranslateOptionsNew(cogArgs.List(), nullptr);
VSIUnlink(output.c_str());
GDALDatasetH cog = GDALTranslate(output.c_str(), warped, cogOptions, nullptr);
GDALTranslateOptionsFree(cogOptions);
GDALClose(warped);
GDALClose(input);
if (!cog) fail();
GDALClose(cog);
return describe(output);
}
 
private:
// What a reader of the file sees.
static std::string describe(const std::string& path) {
GDALDatasetH dataset = GDALOpenEx(path.c_str(), GDAL_OF_RASTER, nullptr, nullptr, nullptr);
if (!dataset) fail();
GDALRasterBandH band = GDALGetRasterBand(dataset, 1);
double t[6];
GDALGetGeoTransform(dataset, t);
int blockWidth = 0, blockHeight = 0;
GDALGetBlockSize(band, &blockWidth, &blockHeight);
const char* layout = GDALGetMetadataItem(dataset, "LAYOUT", "IMAGE_STRUCTURE");
const char* compression = GDALGetMetadataItem(dataset, "COMPRESSION", "IMAGE_STRUCTURE");
 
char line[200];
std::snprintf(line, sizeof line, "%d x %d pixels of %.6f x %.6f degrees\nLAYOUT=%s, COMPRESSION=%s, %d x %d blocks\noverviews",
GDALGetRasterXSize(dataset), GDALGetRasterYSize(dataset), t[1], -t[5], layout ? layout : "none",
compression ? compression : "none", blockWidth, blockHeight);
std::string text = line;
for (int i = 0; i < GDALGetOverviewCount(band); ++i) {
GDALRasterBandH overview = GDALGetOverview(band, i);
text += (i ? ", " : " ") + std::to_string(GDALGetRasterBandXSize(overview)) + " x " + std::to_string(GDALGetRasterBandYSize(overview));
}
GDALClose(dataset);
return text;
}
 
[[noreturn]] static void fail() {
const std::string reason = CPLGetLastErrorMsg();
throw std::runtime_error(reason.empty() ? "GDAL could not write the COG" : reason);
}
};
main.js
import { initNative, CogWriter } from './native/cog_writer.h';
import { RasterInfo } from './native/raster_info.h';
 
await initNative();
const raster = await new RasterInfo();
await raster.create('/vsimem/utm.tif', 1000, 800, 500000, 4450000, 30, 32635); // 30 x 24 km in UTM zone 35N
const writer = await new CogWriter();
const report = await writer.warpToCog('/vsimem/utm.tif', 'EPSG:4326', '/vsimem/cog.tif');
for (const line of report.split('\n')) console.log(line);
PRINTSfirst run downloads 29 MB
1093 x 672 pixels of 0.000322 x 0.000322 degrees
LAYOUT=COG, COMPRESSION=DEFLATE, 256 x 256 blocks
overviews 546 x 336, 273 x 168, 136 x 84

Hillshade, slope and contour lines from an elevation model

GDALDEMProcessing is the gdaldem tool: hillshade, slope, aspect, roughness, TRI and TPI by name. GDALContourGenerateEx draws the contour lines gdal_contour draws, into any vector layer; here an in-memory one, measured with OGR_G_Length.

src/native/dem_tools.h
#pragma once
 
#include <cpl_error.h>
#include <cpl_string.h>
#include <gdal.h>
#include <gdal_alg.h>
#include <gdal_frmts.h>
#include <gdal_utils.h>
#include <ogr_api.h>
#include <ogr_srs_api.h>
 
#include <algorithm>
#include <cstdio>
#include <stdexcept>
#include <string>
#include <vector>
 
// Terrain products from an elevation model: the gdaldem tool's hillshade and slope, and the contour
// lines gdal_contour draws.
class DemTools {
public:
DemTools() {
GDALRegister_GTiff();
GDALRegister_MEM();
}
 
// A round hill, 900 m at the centre of a 100 m plain, in whole metres.
void createHill(const std::string& path, int size, double pixelSize) {
GDALDatasetH dataset = GDALCreate(GDALGetDriverByName("GTiff"), path.c_str(), size, size, 1, GDT_Float32, nullptr);
if (!dataset) fail();
double transform[6] = {500000, pixelSize, 0, 4450000, 0, -pixelSize};
GDALSetGeoTransform(dataset, transform);
OGRSpatialReferenceH crs = OSRNewSpatialReference(nullptr);
OSRImportFromEPSG(crs, 32635);
GDALSetSpatialRef(dataset, crs);
OSRDestroySpatialReference(crs);
std::vector<float> row(size);
const int centre = size / 2;
for (int y = 0; y < size; ++y) {
for (int x = 0; x < size; ++x) {
const int dx = x - centre, dy = y - centre;
row[x] = static_cast<float>(std::max(100, 900 - (dx * dx + dy * dy) / 4));
}
if (GDALRasterIO(GDALGetRasterBand(dataset, 1), GF_Write, 0, y, size, 1, row.data(), size, 1, GDT_Float32, 0, 0) != CE_None) {
GDALClose(dataset);
fail();
}
}
GDALClose(dataset);
}
 
// gdaldem <processing>: "hillshade", "slope", "aspect", "roughness", "TRI" or "TPI". With
// -compute_edges the border pixels get values too.
std::string derive(const std::string& dem, const std::string& processing, const std::string& output) {
GDALDatasetH source = open(dem);
CPLStringList args;
args.AddString("-compute_edges");
GDALDEMProcessingOptions* options = GDALDEMProcessingOptionsNew(args.List(), nullptr);
GDALDatasetH result = GDALDEMProcessing(output.c_str(), source, processing.c_str(), nullptr, options, nullptr);
GDALDEMProcessingOptionsFree(options);
GDALClose(source);
if (!result) fail();
GDALRasterBandH band = GDALGetRasterBand(result, 1);
double min = 0, max = 0, mean = 0, deviation = 0;
GDALComputeRasterStatistics(band, FALSE, &min, &max, &mean, &deviation, nullptr, nullptr);
const int checksum = GDALChecksumImage(band, 0, 0, GDALGetRasterXSize(result), GDALGetRasterYSize(result));
GDALClose(result);
char line[160];
std::snprintf(line, sizeof line, "%s: %.2f to %.2f, mean %.2f, checksum %d", processing.c_str(), min, max, mean, checksum);
return line;
}
 
// gdal_contour -i <interval>: the lines go to an in-memory layer, one feature per line.
std::string contours(const std::string& dem, double interval) {
GDALDatasetH source = open(dem);
GDALDatasetH store = GDALCreate(GDALGetDriverByName("MEM"), "", 0, 0, 0, GDT_Unknown, nullptr);
OGRLayerH layer = GDALDatasetCreateLayer(store, "contours", GDALGetSpatialRef(source), wkbLineString, nullptr);
OGRFieldDefnH field = OGR_Fld_Create("elevation", OFTReal);
OGR_L_CreateField(layer, field, TRUE);
OGR_Fld_Destroy(field);
 
char value[32];
std::snprintf(value, sizeof value, "%g", interval);
CPLStringList options;
options.SetNameValue("LEVEL_INTERVAL", value);
options.SetNameValue("ELEV_FIELD", "0");
const CPLErr error = GDALContourGenerateEx(GDALGetRasterBand(source, 1), layer, options.List(), nullptr, nullptr);
GDALClose(source);
if (error != CE_None) {
GDALClose(store);
fail();
}
 
int lines = 0;
double lowest = 0, highest = 0, length = 0;
OGR_L_ResetReading(layer);
for (OGRFeatureH feature; (feature = OGR_L_GetNextFeature(layer)) != nullptr; OGR_F_Destroy(feature)) {
const double elevation = OGR_F_GetFieldAsDouble(feature, 0);
lowest = lines ? std::min(lowest, elevation) : elevation;
highest = lines ? std::max(highest, elevation) : elevation;
length += OGR_G_Length(OGR_F_GetGeometryRef(feature));
++lines;
}
GDALClose(store);
char line[160];
std::snprintf(line, sizeof line, "contours every %g m: %d lines from %g to %g m, %.1f km long", interval, lines, lowest, highest, length / 1000);
return line;
}
 
private:
static GDALDatasetH open(const std::string& path) {
GDALDatasetH dataset = GDALOpenEx(path.c_str(), GDAL_OF_RASTER, nullptr, nullptr, nullptr);
if (!dataset) fail();
return dataset;
}
 
[[noreturn]] static void fail() {
const std::string reason = CPLGetLastErrorMsg();
throw std::runtime_error(reason.empty() ? "GDAL could not process the elevation model" : reason);
}
};
main.js
import { initNative, DemTools } from './native/dem_tools.h';
 
await initNative();
const tools = await new DemTools();
await tools.createHill('/vsimem/hill.tif', 101, 30); // 101 x 101 pixels of 30 m
console.log(await tools.derive('/vsimem/hill.tif', 'hillshade', '/vsimem/hillshade.tif'));
console.log(await tools.derive('/vsimem/hill.tif', 'slope', '/vsimem/slope.tif'));
console.log(await tools.contours('/vsimem/hill.tif', 100));
PRINTSfirst run downloads 29 MB
hillshade: 12.00 to 255.00, mean 156.63, checksum 58516
slope: 0.00 to 42.97, mean 27.46, checksum 54054
contours every 100 m: 11 lines from 200 to 900 m, 46.9 km long

Read features and filter them by attribute and area

OGR reads every vector format through one API: the schema from OGR_L_GetLayerDefn, then OGR_L_GetNextFeature over the features that pass OGR_L_SetAttributeFilter, a SQL WHERE clause, and OGR_L_SetSpatialFilterRect. Open options turn the lon and lat columns of a CSV into points.

src/native/feature_query.h
#pragma once
 
#include <cpl_conv.h>
#include <cpl_error.h>
#include <cpl_vsi.h>
#include <gdal.h>
#include <ogr_api.h>
#include <ogrsf_frmts.h>
 
#include <stdexcept>
#include <string>
 
// Reads a layer with OGR: its schema, then the features that pass an attribute filter (a SQL WHERE
// clause) inside a rectangle, with their attributes and geometry.
class FeatureQuery {
public:
FeatureQuery() { RegisterOGRCSV(); }
 
// `csv` is text with lon and lat columns, which the CSV driver turns into point geometries.
std::string select(const std::string& csv, const std::string& where, double west, double south, double east, double north) {
const char* path = "/vsimem/sensors.csv";
VSIFCloseL(VSIFileFromMemBuffer(path, reinterpret_cast<GByte*>(const_cast<char*>(csv.data())), csv.size(), FALSE));
const char* const openOptions[] = {"X_POSSIBLE_NAMES=lon", "Y_POSSIBLE_NAMES=lat", "KEEP_GEOM_COLUMNS=NO", "AUTODETECT_TYPE=YES", nullptr};
GDALDatasetH dataset = GDALOpenEx(path, GDAL_OF_VECTOR, nullptr, openOptions, nullptr);
if (!dataset) fail(path);
OGRLayerH layer = GDALDatasetGetLayer(dataset, 0);
OGRFeatureDefnH schema = OGR_L_GetLayerDefn(layer);
 
std::string text = std::string(OGR_L_GetName(layer)) + ": " + std::to_string(OGR_L_GetFeatureCount(layer, TRUE)) + " features of " +
OGRGeometryTypeToName(OGR_L_GetGeomType(layer)) + ", fields";
for (int i = 0; i < OGR_FD_GetFieldCount(schema); ++i) {
OGRFieldDefnH field = OGR_FD_GetFieldDefn(schema, i);
text += std::string(i ? ", " : " ") + OGR_Fld_GetNameRef(field) + " " + OGR_GetFieldTypeName(OGR_Fld_GetType(field));
}
 
if (OGR_L_SetAttributeFilter(layer, where.c_str()) != OGRERR_NONE) {
GDALClose(dataset);
fail(path);
}
OGR_L_SetSpatialFilterRect(layer, west, south, east, north);
OGR_L_ResetReading(layer);
for (OGRFeatureH feature; (feature = OGR_L_GetNextFeature(layer)) != nullptr; OGR_F_Destroy(feature)) {
text += "\n";
for (int i = 0; i < OGR_F_GetFieldCount(feature); ++i) text += std::string(OGR_F_GetFieldAsString(feature, i)) + " ";
char* wkt = nullptr;
OGR_G_ExportToWkt(OGR_F_GetGeometryRef(feature), &wkt);
text += wkt ? wkt : "";
CPLFree(wkt);
}
GDALClose(dataset);
VSIUnlink(path);
return text;
}
 
private:
[[noreturn]] static void fail(const char* path) {
const std::string reason = CPLGetLastErrorMsg();
VSIUnlink(path);
throw std::runtime_error(reason.empty() ? "GDAL could not read the features" : reason);
}
};
main.js
import { initNative, FeatureQuery } from './native/feature_query.h';
 
await initNative();
const csv = [
'id,kind,reading,lon,lat',
'1,air,31.5,28.9784,41.0082',
'2,air,18.2,32.8597,39.9334',
'3,water,22.7,27.1428,38.4237',
'4,air,12.9,39.7168,41.0027',
'5,water,19.4,30.7133,36.8969',
'6,air,27.3,29.0610,40.1885',
].join('\n');
const query = await new FeatureQuery();
// readings above 20 between 26 and 31 degrees east, 36 and 42 degrees north
const found = await query.select(csv, 'reading > 20', 26, 36, 31, 42);
for (const line of found.split('\n')) console.log(line);
PRINTSfirst run downloads 29 MB
sensors: 6 features of Point, fields id Integer, kind String, reading Real
1 air 31.5 POINT (28.9784 41.0082)
3 water 22.7 POINT (27.1428 38.4237)
6 air 27.3 POINT (29.061 40.1885)

Setup for the JavaScript only examples

Imported straight from JavaScript, the headers need this configuration today; its comments say why.

crossbind.config.js
import gdalWasm from '@crossbind/port-gdal-wasm/crossbind.config.js';
 
export default {
general: { name: 'gdaldirect' },
dependencies: [gdalWasm],
paths: { config: import.meta.url },
targetSpecs: [
{
platform: 'wasm',
specs: {
// SpatiaLite is left out of the link (crossbind.overrides.js), and a
// single-threaded build has no threads for GeoPackage or for the Arrow
// path of ogr2ogr to use.
env: {
SPATIALITE_LOAD: 'FALSE',
OGR2OGR_USE_ARROW_API: 'NO',
OGR_GPKG_NUM_THREADS: '1',
},
// -Oz keeps the wasm, GEOS included, under the 25 MiB a file the site's host accepts.
binary: { emccFlags: ['-Oz'] },
},
},
],
};
crossbind.overrides.js
// Libraries left out of the link. Binding gdal.h links GDALAllRegister and so every
// driver GDAL was built with; leaving out what these examples never call keeps the wasm
// under the 25 MiB a file the site's host accepts. Calling into a library left out
// aborts with "missing function". zstd stays in: the COG driver calls it even when it
// writes Deflate. GEOS stays in too: a spatial filter makes OGR build a GEOS prepared
// geometry.
export default {
curl: { exclude: true },
openssl: { exclude: true },
spatialite: { exclude: true },
webp: { exclude: true },
Lerc: { exclude: true },
jpeg: { exclude: true },
expat: { exclude: true },
iconv: { exclude: true },
};

A GDAL command-line tool

WASI has no JavaScript bindings: the program is a main() that links the library, built into one .wasm and run with wasmtime. This one is a small ogrinfo and ogr2ogr. The build copies GDAL's and PROJ's data files to dist/data, and the GDAL_DATA and PROJ_DATA variables tell GDAL and PROJ where they are; PROJ reads its coordinate systems from proj.db there.

crossbind.config.js
import gdalWasi from '@crossbind/port-gdal-wasi/crossbind.config.js';
 
export default {
general: { name: 'gdal-tool' },
dependencies: [gdalWasi],
// The program and the GDAL and PROJ data it reads land in dist/.
paths: { config: import.meta.url, output: 'dist' },
};
src/native/main.cpp
// A small ogrinfo and ogr2ogr: `info <file>` prints what a vector file holds, one line per layer;
// `convert <input> <output> <format> <crs>` writes it in another format and coordinate system.
#include <cpl_error.h>
#include <cpl_string.h>
#include <gdal.h>
#include <gdal_utils.h>
#include <ogr_api.h>
#include <ogr_srs_api.h>
#include <ogrsf_frmts.h>
 
#include <cstdio>
#include <cstring>
#include <string>
 
namespace {
 
void registerDrivers() {
RegisterOGRGeoJSON();
RegisterOGRGeoPackage();
RegisterOGRShape();
RegisterOGRFlatGeobuf();
RegisterOGRCSV();
RegisterOGRKML();
}
 
int failed(const char* what) {
std::fprintf(stderr, "%s: %s\n", what, CPLGetLastErrorMsg());
return 1;
}
 
// name: count type, CRS, extent; degrees to 4 decimals, metres to whole numbers.
void describe(GDALDatasetH dataset) {
for (int i = 0; i < GDALDatasetGetLayerCount(dataset); ++i) {
OGRLayerH layer = GDALDatasetGetLayer(dataset, i);
OGREnvelope extent;
OGR_L_GetExtent(layer, &extent, TRUE);
OGRSpatialReferenceH crs = OGR_L_GetSpatialRef(layer);
const char* authority = crs ? OSRGetAuthorityName(crs, nullptr) : nullptr;
const char* code = crs ? OSRGetAuthorityCode(crs, nullptr) : nullptr;
const std::string crsName = authority && code ? std::string(authority) + ":" + code : "no CRS";
const int decimals = crs && OSRIsGeographic(crs) ? 4 : 0;
std::printf("%s: %lld %s, %s, extent %.*f %.*f %.*f %.*f\n", OGR_L_GetName(layer), static_cast<long long>(OGR_L_GetFeatureCount(layer, TRUE)),
OGRGeometryTypeToName(OGR_L_GetGeomType(layer)), crsName.c_str(), decimals, extent.MinX, decimals, extent.MinY, decimals, extent.MaxX,
decimals, extent.MaxY);
}
}
 
int info(const char* path) {
GDALDatasetH dataset = GDALOpenEx(path, GDAL_OF_VECTOR, nullptr, nullptr, nullptr);
if (!dataset) return failed(path);
describe(dataset);
GDALClose(dataset);
return 0;
}
 
int convert(const char* input, const char* output, const char* format, const char* crs) {
GDALDatasetH source = GDALOpenEx(input, GDAL_OF_VECTOR, nullptr, nullptr, nullptr);
if (!source) return failed(input);
CPLStringList args;
for (const char* arg : {"-f", format, "-t_srs", crs}) args.AddString(arg);
GDALVectorTranslateOptions* options = GDALVectorTranslateOptionsNew(args.List(), nullptr);
GDALDatasetH written = GDALVectorTranslate(output, nullptr, 1, &source, options, nullptr);
GDALVectorTranslateOptionsFree(options);
GDALClose(source);
if (!written) return failed(output);
std::printf("wrote %s as %s\n", output, format);
GDALClose(written);
return 0;
}
 
} // namespace
 
int main(int argc, char** argv) {
registerDrivers();
if (argc == 3 && std::strcmp(argv[1], "info") == 0) return info(argv[2]);
if (argc == 6 && std::strcmp(argv[1], "convert") == 0) return convert(argv[2], argv[3], argv[4], argv[5]);
std::fprintf(stderr, "usage: %s info <file>\n %s convert <input> <output> <format> <crs>\n", argv[0], argv[0]);
return 2;
}
shell
npx crossbind build -p wasi -e wasi -b release
wasmtime run --dir=. --env GDAL_DATA=dist/data/gdal --env PROJ_DATA=dist/data/proj dist/gdal-tool-wasi-wasm32-st-release.wasm info cities.geojson
wasmtime run --dir=. --env GDAL_DATA=dist/data/gdal --env PROJ_DATA=dist/data/proj dist/gdal-tool-wasi-wasm32-st-release.wasm convert cities.geojson cities.gpkg GPKG EPSG:3857
wasmtime run --dir=. --env GDAL_DATA=dist/data/gdal --env PROJ_DATA=dist/data/proj dist/gdal-tool-wasi-wasm32-st-release.wasm info cities.gpkg
output
cities: 3 Point, EPSG:4326, extent 27.1428 38.4237 32.8597 41.0082
wrote cities.gpkg as GPKG
cities: 3 Point, EPSG:3857, extent 3021523 4639455 3657925 5013551
Type to search every guide page and section.
↑↓ navigate↵ openesc close