crossbind
GitHub

GEOS for iOS

v3.15.0iOS

GEOS 3.15.0 for React Native apps on iOS, precompiled for arm64 devices and simulators as @crossbind/port-geos-ios.

npm install @crossbind/port-geos-ios@beta

Install

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

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

Usage

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

Intersect, unite and subtract two polygons

Overlay is the most used part of GEOS: GEOSIntersection_r, GEOSUnion_r and GEOSDifference_r on shapes read from WKT, with GEOSArea_r to measure the result.

src/native/overlay.h
#pragma once
 
#include <geos_c.h>
 
#include <memory>
#include <stdexcept>
#include <string>
 
// Overlay of two geometries written as WKT. Every result is normalised, so the same shape always
// prints the same WKT whatever order its vertices came in.
class Overlay {
public:
static std::string version() { return GEOSversion(); }
 
static std::string intersection(const std::string& a, const std::string& b) { return apply(GEOSIntersection_r, a, b); }
static std::string unite(const std::string& a, const std::string& b) { return apply(GEOSUnion_r, a, b); }
static std::string difference(const std::string& a, const std::string& b) { return apply(GEOSDifference_r, a, b); }
 
static double area(const std::string& wkt) {
Session geos;
double value = 0;
if (!GEOSArea_r(geos.context, geos.read(wkt).get(), &value)) geos.fail();
return value;
}
 
private:
using Operation = GEOSGeometry* (*)(GEOSContextHandle_t, const GEOSGeometry*, const GEOSGeometry*);
 
static std::string apply(Operation operation, const std::string& a, const std::string& b) {
Session geos;
const auto left = geos.read(a);
const auto right = geos.read(b);
return geos.result(operation(geos.context, left.get(), right.get()));
}
 
// GEOS's reentrant C API: each call gets its own context, which also holds the last error message.
struct Session {
struct Destroy {
GEOSContextHandle_t context;
void operator()(GEOSGeometry* geometry) const { GEOSGeom_destroy_r(context, geometry); }
};
using Geometry = std::unique_ptr<GEOSGeometry, Destroy>;
 
GEOSContextHandle_t context = GEOS_init_r();
std::string error;
 
Session() { GEOSContext_setErrorMessageHandler_r(context, remember, &error); }
~Session() { GEOS_finish_r(context); }
Session(const Session&) = delete;
Session& operator=(const Session&) = delete;
 
Geometry own(GEOSGeometry* geometry) {
if (!geometry) fail();
return Geometry(geometry, Destroy{context});
}
 
Geometry read(const std::string& wkt) {
GEOSWKTReader* reader = GEOSWKTReader_create_r(context);
GEOSGeometry* geometry = GEOSWKTReader_read_r(context, reader, wkt.c_str());
GEOSWKTReader_destroy_r(context, reader);
return own(geometry);
}
 
std::string write(const GEOSGeometry* geometry) {
GEOSWKTWriter* writer = GEOSWKTWriter_create_r(context);
char* text = GEOSWKTWriter_write_r(context, writer, geometry);
GEOSWKTWriter_destroy_r(context, writer);
if (!text) fail();
const std::string wkt = text;
GEOSFree_r(context, text);
return wkt;
}
 
// Takes ownership of a result and writes it normalised.
std::string result(GEOSGeometry* geometry) {
const Geometry owned = own(geometry);
if (GEOSNormalize_r(context, owned.get()) != 0) fail();
return write(owned.get());
}
 
[[noreturn]] void fail() const { throw std::runtime_error(error.empty() ? "GEOS operation failed" : error); }
 
static void remember(const char* message, void* error) { *static_cast<std::string*>(error) = message; }
};
};
main.js
import { initNative, Overlay } from './native/overlay.h';
 
await initNative();
const parcel = 'POLYGON ((0 0, 10 0, 10 10, 0 10, 0 0))';
const floodZone = 'POLYGON ((5 5, 15 5, 15 15, 5 15, 5 5))';
const flooded = await Overlay.intersection(parcel, floodZone);
console.log(await Overlay.version());
console.log(flooded, await Overlay.area(flooded));
console.log(await Overlay.area(await Overlay.unite(parcel, floodZone)), await Overlay.area(await Overlay.difference(parcel, floodZone)));
PRINTS
3.15.0-CAPI-1.21.0
POLYGON ((5 5, 5 10, 10 10, 10 5, 5 5)) 25
175 75

Test points against a prepared polygon

Point in polygon is the most common spatial question. GEOSPrepare_r indexes the polygon once; GEOSPreparedContainsXY_r and GEOSPreparedIntersectsXY_r then answer per point and differ only on the boundary, and GEOSPreparedRelate_r gives the full DE-9IM matrix.

src/native/zone.h
#pragma once
 
#include <geos_c.h>
 
#include <memory>
#include <stdexcept>
#include <string>
 
// A polygon prepared once and then asked about many points: GEOSPrepare_r indexes its edges on the
// first question, so the later ones do not walk every edge again.
class Zone {
public:
explicit Zone(const std::string& wkt) : shape(geos.read(wkt)), prepared(GEOSPrepare_r(geos.context, shape.get())) {
if (!prepared) geos.fail();
}
~Zone() { GEOSPreparedGeom_destroy_r(geos.context, prepared); }
Zone(const Zone&) = delete;
Zone& operator=(const Zone&) = delete;
 
// Strictly inside: a point on the boundary is not contained.
bool contains(double x, double y) { return answer(GEOSPreparedContainsXY_r(geos.context, prepared, x, y)); }
 
// Inside or on the boundary.
bool intersects(double x, double y) { return answer(GEOSPreparedIntersectsXY_r(geos.context, prepared, x, y)); }
 
// The DE-9IM matrix: the dimension where the interior, boundary and exterior of this shape meet
// those of the other, row by row.
std::string relate(const std::string& wkt) {
char* matrix = GEOSPreparedRelate_r(geos.context, prepared, geos.read(wkt).get());
if (!matrix) geos.fail();
const std::string text = matrix;
GEOSFree_r(geos.context, matrix);
return text;
}
 
private:
bool answer(char result) {
if (result == 2) geos.fail();
return result == 1;
}
 
// GEOS's reentrant C API: each call gets its own context, which also holds the last error message.
struct Session {
struct Destroy {
GEOSContextHandle_t context;
void operator()(GEOSGeometry* geometry) const { GEOSGeom_destroy_r(context, geometry); }
};
using Geometry = std::unique_ptr<GEOSGeometry, Destroy>;
 
GEOSContextHandle_t context = GEOS_init_r();
std::string error;
 
Session() { GEOSContext_setErrorMessageHandler_r(context, remember, &error); }
~Session() { GEOS_finish_r(context); }
Session(const Session&) = delete;
Session& operator=(const Session&) = delete;
 
Geometry own(GEOSGeometry* geometry) {
if (!geometry) fail();
return Geometry(geometry, Destroy{context});
}
 
Geometry read(const std::string& wkt) {
GEOSWKTReader* reader = GEOSWKTReader_create_r(context);
GEOSGeometry* geometry = GEOSWKTReader_read_r(context, reader, wkt.c_str());
GEOSWKTReader_destroy_r(context, reader);
return own(geometry);
}
 
std::string write(const GEOSGeometry* geometry) {
GEOSWKTWriter* writer = GEOSWKTWriter_create_r(context);
char* text = GEOSWKTWriter_write_r(context, writer, geometry);
GEOSWKTWriter_destroy_r(context, writer);
if (!text) fail();
const std::string wkt = text;
GEOSFree_r(context, text);
return wkt;
}
 
// Takes ownership of a result and writes it normalised.
std::string result(GEOSGeometry* geometry) {
const Geometry owned = own(geometry);
if (GEOSNormalize_r(context, owned.get()) != 0) fail();
return write(owned.get());
}
 
[[noreturn]] void fail() const { throw std::runtime_error(error.empty() ? "GEOS operation failed" : error); }
 
static void remember(const char* message, void* error) { *static_cast<std::string*>(error) = message; }
};
 
Session geos;
Session::Geometry shape;
const GEOSPreparedGeometry* prepared;
};
main.js
import { initNative, Zone } from './native/zone.h';
 
await initNative();
const zone = await new Zone('POLYGON ((0 0, 10 0, 10 10, 0 10, 0 0), (4 4, 6 4, 6 6, 4 6, 4 4))');
const points = [[2, 2], [5, 5], [10, 5], [12, 5]]; // inside, in the hole, on the edge, outside
for (const [x, y] of points) {
console.log(`${x} ${y}: contains ${await zone.contains(x, y)}, intersects ${await zone.intersects(x, y)}`);
}
console.log(await zone.relate('POLYGON ((5 5, 15 5, 15 15, 5 15, 5 5))'));
PRINTS
2 2: contains true, intersects true
5 5: contains false, intersects false
10 5: contains false, intersects true
12 5: contains false, intersects false
212101212

Measure area, length and distance

GEOSArea_r, GEOSLength_r and GEOSDistance_r in the units of the coordinates, GEOSNearestPoints_r for where two shapes come closest, and GEOSGetCentroid_r. GEOS works in the plane, so project longitude and latitude before measuring.

src/native/measure.h
#pragma once
 
#include <geos_c.h>
 
#include <memory>
#include <stdexcept>
#include <string>
 
// Measurements in the plane, in the units of the coordinates: metres for most projected data.
// GEOS does not measure on the ellipsoid, so longitude and latitude need projecting first.
class Measure {
public:
static double area(const std::string& wkt) { return measure(GEOSArea_r, wkt); }
 
// A line's length or a polygon's perimeter. Not named length: every JavaScript function, a
// bound class included, already has a length property, and a static method cannot replace it.
static double lengthOf(const std::string& wkt) { return measure(GEOSLength_r, wkt); }
 
static double distance(const std::string& a, const std::string& b) {
Session geos;
const auto left = geos.read(a);
const auto right = geos.read(b);
double value = 0;
if (!GEOSDistance_r(geos.context, left.get(), right.get(), &value)) geos.fail();
return value;
}
 
// The shortest line between the two shapes, from its end on a to its end on b.
static std::string nearestPoints(const std::string& a, const std::string& b) {
Session geos;
const auto left = geos.read(a);
const auto right = geos.read(b);
GEOSCoordSequence* ends = GEOSNearestPoints_r(geos.context, left.get(), right.get());
if (!ends) geos.fail();
return geos.write(geos.own(GEOSGeom_createLineString_r(geos.context, ends)).get());
}
 
static std::string centroid(const std::string& wkt) {
Session geos;
return geos.result(GEOSGetCentroid_r(geos.context, geos.read(wkt).get()));
}
 
private:
using Metric = int (*)(GEOSContextHandle_t, const GEOSGeometry*, double*);
 
static double measure(Metric metric, const std::string& wkt) {
Session geos;
double value = 0;
if (!metric(geos.context, geos.read(wkt).get(), &value)) geos.fail();
return value;
}
 
// GEOS's reentrant C API: each call gets its own context, which also holds the last error message.
struct Session {
struct Destroy {
GEOSContextHandle_t context;
void operator()(GEOSGeometry* geometry) const { GEOSGeom_destroy_r(context, geometry); }
};
using Geometry = std::unique_ptr<GEOSGeometry, Destroy>;
 
GEOSContextHandle_t context = GEOS_init_r();
std::string error;
 
Session() { GEOSContext_setErrorMessageHandler_r(context, remember, &error); }
~Session() { GEOS_finish_r(context); }
Session(const Session&) = delete;
Session& operator=(const Session&) = delete;
 
Geometry own(GEOSGeometry* geometry) {
if (!geometry) fail();
return Geometry(geometry, Destroy{context});
}
 
Geometry read(const std::string& wkt) {
GEOSWKTReader* reader = GEOSWKTReader_create_r(context);
GEOSGeometry* geometry = GEOSWKTReader_read_r(context, reader, wkt.c_str());
GEOSWKTReader_destroy_r(context, reader);
return own(geometry);
}
 
std::string write(const GEOSGeometry* geometry) {
GEOSWKTWriter* writer = GEOSWKTWriter_create_r(context);
char* text = GEOSWKTWriter_write_r(context, writer, geometry);
GEOSWKTWriter_destroy_r(context, writer);
if (!text) fail();
const std::string wkt = text;
GEOSFree_r(context, text);
return wkt;
}
 
// Takes ownership of a result and writes it normalised.
std::string result(GEOSGeometry* geometry) {
const Geometry owned = own(geometry);
if (GEOSNormalize_r(context, owned.get()) != 0) fail();
return write(owned.get());
}
 
[[noreturn]] void fail() const { throw std::runtime_error(error.empty() ? "GEOS operation failed" : error); }
 
static void remember(const char* message, void* error) { *static_cast<std::string*>(error) = message; }
};
};
main.js
import { initNative, Measure } from './native/measure.h';
 
await initNative();
const field = 'POLYGON ((0 0, 40 0, 40 30, 0 30, 0 0))';
const well = 'POINT (52 39)';
const track = 'LINESTRING (0 0, 30 40, 30 50)';
console.log(await Measure.area(field), await Measure.lengthOf(field), await Measure.lengthOf(track));
console.log(await Measure.distance(field, well), await Measure.nearestPoints(field, well));
console.log(await Measure.centroid(field));
PRINTS
1200 140 60
15 LINESTRING (40 30, 52 39)
POINT (20 15)

Buffer points, lines and polygons

GEOSBuffer_r turns a point into a circle of straight segments, GEOSBufferWithStyle_r sets the caps and joins of a line's corridor, a negative distance shrinks a polygon, and GEOSOffsetCurve_r draws a parallel line.

src/native/buffer.h
#pragma once
 
#include <geos_c.h>
 
#include <memory>
#include <stdexcept>
#include <string>
 
// Buffers grow a shape by a distance, or shrink it with a negative one; an offset curve runs parallel
// to a line. Curves become straight segments: quadrantSegments of them for each quarter circle.
class BufferOp {
public:
static std::string around(const std::string& wkt, double distance, int quadrantSegments) {
Session geos;
return geos.result(GEOSBuffer_r(geos.context, geos.read(wkt).get(), distance, quadrantSegments));
}
 
// cap: "round", "flat" or "square"; join: "round", "mitre" or "bevel".
static std::string withStyle(const std::string& wkt, double distance, const std::string& cap, const std::string& join) {
Session geos;
const auto geometry = geos.read(wkt);
return geos.result(GEOSBufferWithStyle_r(geos.context, geometry.get(), distance, 8, capStyle(cap), joinStyle(join), 5.0));
}
 
// A positive distance offsets to the left of the line's direction, a negative one to the right.
static std::string offsetCurve(const std::string& wkt, double distance) {
Session geos;
return geos.result(GEOSOffsetCurve_r(geos.context, geos.read(wkt).get(), distance, 8, GEOSBUF_JOIN_ROUND, 5.0));
}
 
private:
static int capStyle(const std::string& name) {
if (name == "round") return GEOSBUF_CAP_ROUND;
if (name == "flat") return GEOSBUF_CAP_FLAT;
if (name == "square") return GEOSBUF_CAP_SQUARE;
throw std::invalid_argument("cap must be round, flat or square");
}
 
static int joinStyle(const std::string& name) {
if (name == "round") return GEOSBUF_JOIN_ROUND;
if (name == "mitre") return GEOSBUF_JOIN_MITRE;
if (name == "bevel") return GEOSBUF_JOIN_BEVEL;
throw std::invalid_argument("join must be round, mitre or bevel");
}
 
// GEOS's reentrant C API: each call gets its own context, which also holds the last error message.
struct Session {
struct Destroy {
GEOSContextHandle_t context;
void operator()(GEOSGeometry* geometry) const { GEOSGeom_destroy_r(context, geometry); }
};
using Geometry = std::unique_ptr<GEOSGeometry, Destroy>;
 
GEOSContextHandle_t context = GEOS_init_r();
std::string error;
 
Session() { GEOSContext_setErrorMessageHandler_r(context, remember, &error); }
~Session() { GEOS_finish_r(context); }
Session(const Session&) = delete;
Session& operator=(const Session&) = delete;
 
Geometry own(GEOSGeometry* geometry) {
if (!geometry) fail();
return Geometry(geometry, Destroy{context});
}
 
Geometry read(const std::string& wkt) {
GEOSWKTReader* reader = GEOSWKTReader_create_r(context);
GEOSGeometry* geometry = GEOSWKTReader_read_r(context, reader, wkt.c_str());
GEOSWKTReader_destroy_r(context, reader);
return own(geometry);
}
 
std::string write(const GEOSGeometry* geometry) {
GEOSWKTWriter* writer = GEOSWKTWriter_create_r(context);
char* text = GEOSWKTWriter_write_r(context, writer, geometry);
GEOSWKTWriter_destroy_r(context, writer);
if (!text) fail();
const std::string wkt = text;
GEOSFree_r(context, text);
return wkt;
}
 
// Takes ownership of a result and writes it normalised.
std::string result(GEOSGeometry* geometry) {
const Geometry owned = own(geometry);
if (GEOSNormalize_r(context, owned.get()) != 0) fail();
return write(owned.get());
}
 
[[noreturn]] void fail() const { throw std::runtime_error(error.empty() ? "GEOS operation failed" : error); }
 
static void remember(const char* message, void* error) { *static_cast<std::string*>(error) = message; }
};
};
main.js
import { initNative, BufferOp } from './native/buffer.h';
import { Measure } from './native/measure.h';
 
await initNative();
const circle = await BufferOp.around('POINT (0 0)', 10, 8); // 8 segments per quarter circle
console.log((await Measure.area(circle)).toFixed(4));
const road = 'LINESTRING (0 0, 100 0)';
const flat = await BufferOp.withStyle(road, 5, 'flat', 'round');
const round = await BufferOp.withStyle(road, 5, 'round', 'round');
console.log(await Measure.area(flat), (await Measure.area(round)).toFixed(4));
const setback = await BufferOp.around('POLYGON ((0 0, 20 0, 20 20, 0 20, 0 0))', -2, 8);
console.log(setback, await Measure.area(setback));
console.log(await BufferOp.offsetCurve(road, 5));
PRINTS
312.1445
1000 1078.0361
POLYGON ((2 2, 2 18, 18 18, 18 2, 2 2)) 256
LINESTRING (0 5, 100 5)

Find why a polygon is invalid and repair it

GEOSisValid_r and GEOSisValidReason_r say whether a shape breaks the OGC rules, what is wrong and where. GEOSMakeValidWithParams_r repairs it two ways, which agree on a bowtie and differ on a hole that leaks out of its shell.

src/native/validity.h
#pragma once
 
#include <geos_c.h>
 
#include <memory>
#include <stdexcept>
#include <string>
 
// OGC validity: whether a shape is valid, what is wrong and where if it is not, and a repair.
class Validity {
public:
static bool isValid(const std::string& wkt) {
Session geos;
const char valid = GEOSisValid_r(geos.context, geos.read(wkt).get());
if (valid == 2) geos.fail();
return valid == 1;
}
 
// "Valid Geometry", or the problem and its location, such as "Self-intersection[5 5]".
static std::string reason(const std::string& wkt) {
Session geos;
char* text = GEOSisValidReason_r(geos.context, geos.read(wkt).get());
if (!text) geos.fail();
const std::string reason = text;
GEOSFree_r(geos.context, text);
return reason;
}
 
// "linework" rebuilds the shape from all of its noded edges; "structure" repairs each ring,
// then keeps shells as shells and subtracts the holes from them.
static std::string makeValid(const std::string& wkt, const std::string& method) {
if (method != "linework" && method != "structure") throw std::invalid_argument("method must be linework or structure");
Session geos;
const auto geometry = geos.read(wkt);
GEOSMakeValidParams* params = GEOSMakeValidParams_create_r(geos.context);
GEOSMakeValidParams_setMethod_r(geos.context, params, method == "structure" ? GEOS_MAKE_VALID_STRUCTURE : GEOS_MAKE_VALID_LINEWORK);
GEOSGeometry* repaired = GEOSMakeValidWithParams_r(geos.context, geometry.get(), params);
GEOSMakeValidParams_destroy_r(geos.context, params);
return geos.result(repaired);
}
 
private:
// GEOS's reentrant C API: each call gets its own context, which also holds the last error message.
struct Session {
struct Destroy {
GEOSContextHandle_t context;
void operator()(GEOSGeometry* geometry) const { GEOSGeom_destroy_r(context, geometry); }
};
using Geometry = std::unique_ptr<GEOSGeometry, Destroy>;
 
GEOSContextHandle_t context = GEOS_init_r();
std::string error;
 
Session() { GEOSContext_setErrorMessageHandler_r(context, remember, &error); }
~Session() { GEOS_finish_r(context); }
Session(const Session&) = delete;
Session& operator=(const Session&) = delete;
 
Geometry own(GEOSGeometry* geometry) {
if (!geometry) fail();
return Geometry(geometry, Destroy{context});
}
 
Geometry read(const std::string& wkt) {
GEOSWKTReader* reader = GEOSWKTReader_create_r(context);
GEOSGeometry* geometry = GEOSWKTReader_read_r(context, reader, wkt.c_str());
GEOSWKTReader_destroy_r(context, reader);
return own(geometry);
}
 
std::string write(const GEOSGeometry* geometry) {
GEOSWKTWriter* writer = GEOSWKTWriter_create_r(context);
char* text = GEOSWKTWriter_write_r(context, writer, geometry);
GEOSWKTWriter_destroy_r(context, writer);
if (!text) fail();
const std::string wkt = text;
GEOSFree_r(context, text);
return wkt;
}
 
// Takes ownership of a result and writes it normalised.
std::string result(GEOSGeometry* geometry) {
const Geometry owned = own(geometry);
if (GEOSNormalize_r(context, owned.get()) != 0) fail();
return write(owned.get());
}
 
[[noreturn]] void fail() const { throw std::runtime_error(error.empty() ? "GEOS operation failed" : error); }
 
static void remember(const char* message, void* error) { *static_cast<std::string*>(error) = message; }
};
};
main.js
import { initNative, Validity } from './native/validity.h';
import { Measure } from './native/measure.h';
 
await initNative();
const bowtie = 'POLYGON ((0 0, 10 10, 10 0, 0 10, 0 0))';
console.log(await Validity.isValid(bowtie), await Validity.reason(bowtie));
const repaired = await Validity.makeValid(bowtie, 'linework');
console.log(repaired, await Validity.isValid(repaired), await Measure.area(repaired));
const leaky = 'POLYGON ((0 0, 10 0, 10 10, 0 10, 0 0), (5 5, 15 5, 15 15, 5 15, 5 5))';
console.log(await Validity.reason(leaky));
for (const method of ['linework', 'structure']) {
console.log(method, await Measure.area(await Validity.makeValid(leaky, method)));
}
PRINTS
false Self-intersection[5 5]
MULTIPOLYGON (((5 5, 10 10, 10 0, 5 5)), ((0 0, 0 10, 5 5, 0 0))) true 50
Self-intersection[5 10]
linework 150
structure 75

What is different on iOS

  • pod install compiles your headers with the library through the plugin's podspec, and the app build links the result.
  • Named imports from ./native/<header>.h work as on the web: await initNative() once, then call the classes.
  • There is no m.FS and no /memfs: files live in the app's own storage, and your C++ takes their paths.
  • No Worker and no COOP or COEP: runtime: 'mt' uses pthreads directly.

Other platforms

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

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