crossbind
GitHub

GDAL

v3.13.3Geospatial

GDAL 3.13.3, raster and vector geospatial data processing, packaged by crossbind as @crossbind/port-gdal and one package per target. Only a variant that is actually on npm beta is listed as published.

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

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 28.8 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: 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: 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: 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'));

Usage

The calls most GDAL 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 GDAL's own headers from @crossbind/port-gdal directly. 4 of 5 work that way; the other says what stops it.

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',
},
},
},
],
};
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.
export default {
curl: { exclude: true },
openssl: { exclude: true },
spatialite: { exclude: true },
webp: { exclude: true },
Lerc: { exclude: true },
jpeg: { exclude: true },
expat: { exclude: true },
geos: { exclude: true },
iconv: { exclude: true },
};

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 28.8 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 28.8 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 28.8 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 28.8 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 28.8 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)

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-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 },
};

Platforms

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

Packages

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

Licence

  • npm license field of @crossbind/port-gdal: 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
cURLExpatGEOSGeoTIFFiconvLERClibjpeg-turbolibTIFFOpenSSLPROJSpatiaLiteSQLiteWebPzlibZstandard
Type to search every guide page and section.
↑↓ navigate↵ openesc close