geos.js
Version:
an easy-to-use JavaScript wrapper over WebAssembly build of GEOS
1,449 lines (1,438 loc) • 1.51 MB
JavaScript
const POINTER = Symbol('ptr');
const FINALIZATION = Symbol('finalization_registry');
const CLEANUP = Symbol('cleanup');
// PreparedGeometry specific
const P_POINTER = Symbol('prepared:ptr');
const P_FINALIZATION = Symbol('prepared:finalization_registry');
const P_CLEANUP = Symbol('prepared:cleanup');
class ReusableBuffer {
constructor(ptr, length) {
this[POINTER] = (ptr >>>= 0);
this.i4 = ptr / 4;
this.l = length;
this.l4 = length / 4;
}
freeIfTmp() {
if (this !== geos.buff) {
geos.free(this[POINTER]);
}
}
}
class ReusableU32 {
constructor(ptr) {
this[POINTER] = (ptr >>>= 0);
this.i = ptr / 4;
}
get() {
return geos.U32[this.i];
}
set(v) {
geos.U32[this.i] = v;
}
}
class ReusableF64 {
constructor(ptr) {
this[POINTER] = (ptr >>>= 0);
this.i = ptr / 8;
}
get() {
return geos.F64[this.i];
}
set(v) {
geos.F64[this.i] = v;
}
}
/**
* Base error class for all `geos.js` errors.
*
* Errors that originate from C/C++/Wasm code are thrown as instances of this class.
* More specific errors are thrown as instances of one of the subclasses of this class.
*/
class GEOSError extends Error {
/** @internal */
constructor(message) {
super(message);
this.name = 'GEOSError';
}
}
class GEOS {
updateMemory() {
const ab = this.memory.buffer;
this.U8 = new Uint8Array(ab);
this.U32 = new Uint32Array(ab);
this.F64 = new Float64Array(ab);
}
buffByL(l) {
let { buff } = this;
if (l > buff.l) {
const tmpBuffPtr = this.malloc(l);
buff = new ReusableBuffer(tmpBuffPtr, l);
}
return buff;
}
buffByL4(l4) {
let { buff } = this;
if (l4 > buff.l4) {
const tmpBuffLen = l4 * 4;
const tmpBuffPtr = this.malloc(tmpBuffLen);
buff = new ReusableBuffer(tmpBuffPtr, tmpBuffLen);
}
return buff;
}
encodeString(str) {
const strLen = str.length;
const buff = this.buffByL(strLen + 1);
const idx = buff[POINTER];
const dst = this.U8.subarray(idx, idx + strLen + 1);
const stats = this.te.encodeInto(str, dst);
if (stats.written !== strLen) {
// geos related strings are expected to be simple 1 byte utf8
throw new GEOSError('Unexpected string encoding result');
}
dst[strLen] = 0;
return buff;
}
decodeString(ptr) {
const startIdx = ptr >>> 0;
const src = this.U8;
let endIdx = startIdx;
while (src[endIdx])
endIdx++;
return this.td.decode(src.subarray(startIdx, endIdx));
}
addFunction(fn, sig) {
let fnIdx = this.functionsInTableMap.get(fn);
if (fnIdx) {
return fnIdx;
}
fnIdx = this.freeTableIndexes.length
? this.freeTableIndexes.pop()
: this.table.grow(1);
const asWasmFn = convertJsFunctionToWasm(fn, sig);
this.table.set(fnIdx, asWasmFn);
this.functionsInTableMap.set(fn, fnIdx);
return fnIdx;
}
;
removeFunction(fn) {
const fnIdx = this.functionsInTableMap.get(fn);
if (fnIdx) {
this.table.set(fnIdx, null);
this.functionsInTableMap.delete(this.table.get(fnIdx));
this.freeTableIndexes.push(fnIdx);
}
}
constructor(instance) {
/* WASM: strings */
this.td = new TextDecoder();
this.te = new TextEncoder();
this.functionsInTableMap = new Map();
this.freeTableIndexes = [];
/* GEOS */
this.t_r = {};
this.t_w = {};
this.b_r = {};
this.b_w = {};
this.b_p = {};
this.m_v = {};
this.onGEOSError = (messagePtr, _userdata) => {
const message = this.decodeString(messagePtr);
const error = new GEOSError(message);
const sepIdx = message.indexOf(': ');
if (sepIdx > 0) {
error.name = `${error.name}::${message.slice(0, sepIdx)}`;
error.message = message.slice(sepIdx + 2);
}
throw error;
};
const { memory, __indirect_function_table, ...exports } = instance.exports;
this.memory = memory;
this.updateMemory();
this.table = __indirect_function_table;
exports._initialize();
const ctx = exports.GEOS_init_r();
exports.GEOSContext_setErrorMessageHandler_r(ctx, this.addFunction(this.onGEOSError, 'vpp'), 0);
// bind ctx to all `_r` functions and remove `_r` from their name:
for (const fnName in exports) {
if (fnName.endsWith('_r')) {
// @ts-ignore
this[fnName.slice(0, -2)] = exports[fnName].bind(null, ctx);
}
else {
// @ts-ignore
this[fnName] = exports[fnName];
}
}
const buffLen = 4096; // 4KB
let ptr = exports.malloc(buffLen + // buff
2 * 4 + // u32s
4 * 8);
this.buff = new ReusableBuffer(ptr, buffLen);
this.u1 = new ReusableU32(ptr += buffLen);
this.u2 = new ReusableU32(ptr += 4);
this.f1 = new ReusableF64(ptr += 4);
this.f2 = new ReusableF64(ptr += 8);
this.f3 = new ReusableF64(ptr += 8);
this.f4 = new ReusableF64(ptr + 8);
}
}
const convertJsFunctionToWasm = (fn, sig) => {
const typeSectionBody = [1, 96];
const sigRet = sig.slice(0, 1);
const sigParam = sig.slice(1);
const typeCodes = {
i: 127, // i32
p: 127, // i32
j: 126, // i64
f: 125, // f32
d: 124, // f64
};
uleb128Encode(sigParam.length, typeSectionBody);
for (const paramType of sigParam) {
typeSectionBody.push(typeCodes[paramType]);
}
if (sigRet === 'v') {
typeSectionBody.push(0);
}
else {
typeSectionBody.push(1, typeCodes[sigRet]);
}
const bytes = [0, 97, 115, 109, 1, 0, 0, 0, 1];
uleb128Encode(typeSectionBody.length, bytes);
bytes.push(...typeSectionBody);
bytes.push(2, 7, 1, 1, 101, 1, 102, 0, 0, 7, 5, 1, 1, 102, 0, 0);
const module = new WebAssembly.Module(new Uint8Array(bytes));
const instance = new WebAssembly.Instance(module, { e: { f: fn } });
return instance.exports.f;
};
const uleb128Encode = (n, target) => {
if (n < 128) {
target.push(n);
}
else {
target.push((n % 128) | 128, n >> 7);
}
};
const imports = {
env: {
emscripten_notify_memory_growth() {
// memory growth linear step:
const growStep = 256; // 256 * 64KB = 16MB
const currentPageCount = geos.memory.buffer.byteLength / 65536;
const pagesToGrow = growStep - (currentPageCount % growStep);
geos.memory.grow(pagesToGrow);
geos.updateMemory();
},
},
wasi_snapshot_preview1: {
random_get(buffer, size) {
crypto.getRandomValues(geos.U8.subarray(buffer >>>= 0, buffer + size >>> 0));
return 0;
},
},
};
const geosPlaceholder = new Proxy({}, {
get(_, property) {
if (property.endsWith('destroy')) {
// silently ignore GEOS destroy calls after `terminate` call
return () => 0;
}
throw new GEOSError('GEOS.js not initialized');
},
});
let geos = geosPlaceholder;
async function instantiate(source) {
let module;
let instance;
if (source instanceof WebAssembly.Module) {
module = source;
instance = await WebAssembly.instantiate(source, imports);
}
else {
({ module, instance } = await WebAssembly.instantiateStreaming(source, imports));
}
geos = new GEOS(instance);
return module;
}
/**
* Terminates the initialized `geos.js` module and releases associated resources.
*
* @returns A Promise that resolves when the termination is complete
*
* @see {@link initializeFromBase64}
* @see {@link initialize}
*/
async function terminate() {
if (geos !== geosPlaceholder) {
geos = geosPlaceholder; // gc will do the rest
}
}
const jsonifyGeom = (s) => {
const { B, F } = s;
const header = B[s.b++];
const typeId = header & 15;
const isEmpty = header & 16;
const hasZ = header & 32;
const hasM = header & 64;
const skip = !hasZ + !!hasM;
if (isEmpty) {
if (typeId < 7) {
return { type: GEOSGeometryTypeDecoder[typeId], coordinates: [] };
}
return { type: GEOSGeometryTypeDecoder[typeId], geometries: [] };
}
switch (typeId) {
case 0: { // Point
const pt = hasZ
? [F[s.f++], F[s.f++], F[s.f++]]
: [F[s.f++], F[s.f++]];
return { type: GEOSGeometryTypeDecoder[typeId], coordinates: pt };
}
case 1: { // LineString
const ptsLength = B[s.b++];
const pts = Array(ptsLength);
let f = B[s.b++];
for (let i = 0; i < ptsLength; i++, f += skip) {
pts[i] = hasZ
? [F[f++], F[f++], F[f++]]
: [F[f++], F[f++]];
}
return { type: GEOSGeometryTypeDecoder[typeId], coordinates: pts };
}
case 4: { // MultiPoint
const ptsLength = B[s.b++];
const pts = Array(ptsLength);
for (let i = 0; i < ptsLength; i++) {
pts[i] = hasZ
? [F[s.f++], F[s.f++], F[s.f++]]
: [F[s.f++], F[s.f++]];
}
return { type: GEOSGeometryTypeDecoder[typeId], coordinates: pts };
}
case 3: // Polygon
case 5: { // MultiLineString
const pptsLength = B[s.b++];
const ppts = Array(pptsLength);
for (let j = 0; j < pptsLength; j++) {
const ptsLength = B[s.b++];
const pts = ppts[j] = Array(ptsLength);
let f = B[s.b++];
for (let i = 0; i < ptsLength; i++, f += skip) {
pts[i] = hasZ
? [F[f++], F[f++], F[f++]]
: [F[f++], F[f++]];
}
}
return { type: GEOSGeometryTypeDecoder[typeId], coordinates: ppts };
}
case 6: { // MultiPolygon
const ppptsLength = B[s.b++];
const pppts = Array(ppptsLength);
for (let k = 0; k < ppptsLength; k++) {
const pptsLength = B[s.b++];
const ppts = pppts[k] = Array(pptsLength);
for (let j = 0; j < pptsLength; j++) {
const ptsLength = B[s.b++];
const pts = ppts[j] = Array(ptsLength);
let f = B[s.b++];
for (let i = 0; i < ptsLength; i++, f += skip) {
pts[i] = hasZ
? [F[f++], F[f++], F[f++]]
: [F[f++], F[f++]];
}
}
}
return { type: GEOSGeometryTypeDecoder[typeId], coordinates: pppts };
}
case 7: { // GeometryCollection
const geomsLength = B[s.b++];
const geoms = Array(geomsLength);
for (let i = 0; i < geomsLength; i++) {
geoms[i] = jsonifyGeom(s);
}
return { type: GEOSGeometryTypeDecoder[typeId], geometries: geoms };
}
}
throw new GEOSError(`Unsupported geometry type ${GEOSGeometryTypeDecoder[typeId]}`);
};
/**
* Converts a geometry object to its GeoJSON representation.
*
* @param geometry - The geometry object to be converted
* @returns A GeoJSON representation of the geometry
* @throws {GEOSError} when called with an unsupported geometry type (not GeoJSON)
*
* @example
* const a = point([ 0, 0 ]);
* const b = lineString([ [ 0, 0 ], [ 1, 1 ] ]);
* const c = polygon([ [ [ 0, 0 ], [ 1, 1 ], [ 1, 0 ], [ 0, 0 ] ] ]);
* const a_json = jsonifyGeometry(a); // { type: 'Point', coordinates: [ 0, 0 ] };
* const b_json = jsonifyGeometry(b); // { type: 'LineString', coordinates: [ [ 0, 0 ], [ 1, 1 ] ] };
* const c_json = jsonifyGeometry(c); // { type: 'Polygon', coordinates: [ [ [ 0, 0 ], [ 1, 1 ], [ 1, 0 ], [ 0, 0 ] ] ] };
*/
function jsonifyGeometry(geometry) {
const buff = geos.buff;
let tmpOutBuffPtr;
try {
let B = geos.U32;
let b = buff.i4, b0 = b;
B[b++] = 0;
B[b++] = 1;
B[b++] = geometry[POINTER];
B[b] = buff.l4 - 3; // buffAvailableL4
geos.jsonify_geoms(buff[POINTER]);
B = geos.U32;
const s = { B, b, F: geos.F64, f: B[b0 + 1] }; // f = buff[1]
tmpOutBuffPtr = B[b0]; // buff[0]
if (tmpOutBuffPtr) {
s.b = tmpOutBuffPtr / 4;
}
return jsonifyGeom(s);
}
finally {
if (tmpOutBuffPtr) {
geos.free(tmpOutBuffPtr);
}
}
}
/**
* Converts an array of geometries to an array of GeoJSON `Feature` objects.
*
* @param geometries - Array of geometries to be converted
* @returns Array of GeoJSON `Feature` objects
* @throws {GEOSError} when called with an unsupported geometry type (not GeoJSON)
*
* @example
* const a = point([ 0, 0 ], { id: 1, properties: { name: 'A' } });
* const b = lineString([ [ 0, 0 ], [ 1, 1 ] ], { id: 2 });
* const c = polygon([ [ [ 0, 0 ], [ 1, 1 ], [ 1, 0 ], [ 0, 0 ] ] ]);
* const features = jsonifyFeatures([ a, b, c ]);
* // [
* // {
* // id: 1,
* // type: 'Feature',
* // geometry: { type: 'Point', coordinates: [ 0, 0 ] },
* // properties: { name: 'A' },
* // },
* // {
* // id: 2,
* // type: 'Feature',
* // geometry: { type: 'LineString', coordinates: [ [ 0, 0 ], [ 1, 1 ] ] },
* // properties: null,
* // },
* // {
* // type: 'Feature',
* // geometry: { type: 'Polygon', coordinates: [ [ [ 0, 0 ], [ 1, 1 ], [ 1, 0 ], [ 0, 0 ] ] ] },
* // properties: null,
* // },
* // ];
*/
function jsonifyFeatures(geometries) {
const geometriesLength = geometries.length;
const buffNeededL4 = geometriesLength + 3;
const buff = geos.buffByL4(buffNeededL4);
let tmpOutBuffPtr;
try {
let B = geos.U32;
let b = buff.i4, b0 = b;
B[b++] = 0;
B[b++] = geometriesLength;
for (const geometry of geometries) {
B[b++] = geometry[POINTER];
}
B[b] = buff.l4 - buffNeededL4; // buffAvailableL4
geos.jsonify_geoms(buff[POINTER]);
B = geos.U32;
const s = { B, b, F: geos.F64, f: B[b0 + 1] }; // f = buff[1]
tmpOutBuffPtr = B[b0]; // buff[0]
if (tmpOutBuffPtr) {
s.b = tmpOutBuffPtr / 4;
}
const features = Array(geometriesLength);
for (let i = 0; i < geometriesLength; i++) {
const geometry = geometries[i];
features[i] = {
id: geometry.id,
type: 'Feature',
geometry: jsonifyGeom(s),
properties: geometry.props ?? null,
};
}
return features;
}
finally {
buff.freeIfTmp();
if (tmpOutBuffPtr) {
geos.free(tmpOutBuffPtr);
}
}
}
var _a$1, _b;
const GEOSGeometryTypeDecoder = [
/* 0 */ 'Point',
/* 1 */ 'LineString',
/* 2 */ 'LinearRing', // Not GeoJSON, GEOS geometry type for polygon rings
/* 3 */ 'Polygon',
/* 4 */ 'MultiPoint',
/* 5 */ 'MultiLineString',
/* 6 */ 'MultiPolygon',
/* 7 */ 'GeometryCollection',
// Not GeoJSON types:
/* 8 */ 'CircularString',
/* 9 */ 'CompoundCurve',
/* 10 */ 'CurvePolygon',
/* 11 */ 'MultiCurve',
/* 12 */ 'MultiSurface',
];
/**
* Class representing a GEOS geometry that exists in the Wasm memory.
*
* @template P - The type of optional data assigned to a geometry instance.
* Similar to the type of GeoJSON `Feature` properties field.
*/
class GeometryRef {
/**
* Organizes the elements, rings, and coordinate order of geometries in a
* consistent way, so that geometries that represent the same object can
* be easily compared.
*
* Modifies the geometry in-place.
*
* Normalization ensures the following:
* - Lines are oriented to have smallest coordinate first (apart from duplicate endpoints)
* - Rings start with their smallest coordinate (using XY ordering)
* - Polygon **shell** rings are oriented **CW**, and **holes CCW**
* - Collection elements are sorted by their first coordinate
*
* Note the Polygon winding order, OGC standard uses the opposite convention
* and so does GeoJSON. Polygon ring orientation could be changed via {@link orientPolygons}.
*
* @returns The same geometry but normalized, modified in-place
*/
normalize() {
geos.GEOSNormalize(this[POINTER]);
return this;
}
/**
* Enforces a ring orientation on all polygonal elements in the input geometry.
* Polygon exterior ring can be oriented clockwise (CW) or counter-clockwise (CCW),
* interior rings (holes) are oriented in the opposite direction.
*
* Modifies the geometry in-place. Non-polygonal geometries will not be modified.
*
* @param [exterior='cw'] - Exterior ring orientation. Interior rings are
* always oriented in the opposite direction.
* @returns The same geometry but with oriented rings, modified in-place
*
* @example exterior ring CCW, holes CW (GeoJSON compliance)
* polygonal = // (Multi)Polygon or GeometryCollection with some (Multi)Polygons
* polygonal.orientPolygons('ccw');
*
* @example exterior ring CW, holes CCW
* polygonal.orientPolygons('cw');
*/
orientPolygons(exterior = 'cw') {
geos.GEOSOrientPolygons(this[POINTER], +(exterior === 'cw'));
return this;
}
/**
* Creates a deep copy of this geometry object.
*
* @returns A new geometry that is a copy of this geometry
*
* @example
* const original = point([ 0, 0 ]); // some geometry
* const copy = original.clone();
* // copy can be modified without affecting the original
*/
clone() {
const geomPtr = geos.GEOSGeom_clone(this[POINTER]);
const copy = new GeometryRef(geomPtr);
if (this.id != null) {
copy.id = this.id;
}
if (this.props != null) {
copy.props = this.props; // shallow copy
}
return copy;
}
/**
* Converts the geometry to a GeoJSON `Feature` object.
*
* This method allows the geometry to be serialized to JSON
* and is automatically called by `JSON.stringify()`.
*
* @returns A GeoJSON `Feature` representation of this geometry
*
* @see {@link toGeoJSON} converts geometry to a GeoJSON `Feature`
* or a GeoJSON `FeatureCollection` object.
*
* @example
* const geom = point([ 1, 2, 3 ]);
* const geojson = geom.toJSON();
* // {
* // type: 'Feature',
* // geometry: { type: 'Point', coordinates: [ 1, 2, 3 ] },
* // properties: null,
* // }
* const geojsonStr = JSON.stringify(geom);
* // '{"type":"Feature","geometry":{"type":"Point","coordinates":[1,2,3]},"properties":null}'
*/
toJSON() {
return {
id: this.id,
type: 'Feature',
geometry: jsonifyGeometry(this),
properties: this.props ?? null,
};
}
/**
* Frees the Wasm memory allocated for the GEOS geometry object.
*
* {@link GeometryRef} objects are automatically freed when they are out of scope.
* This mechanism is provided by the [`FinalizationRegistry`]{@link https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/FinalizationRegistry}
* that binds the lifetime of the Wasm resources to the lifetime of the JS objects.
*
* This method exists as a backup for those who find `FinalizationRegistry`
* unreliable and want a way to free the memory manually.
*
* Use with caution, as when the object is manually freed, the underlying
* Wasm resource becomes invalid and cannot be used anymore.
*
* @see {@link GeometryRef#detached}
*/
free() {
if (this[P_POINTER]) {
GeometryRef[P_FINALIZATION].unregister(this);
GeometryRef[P_CLEANUP](this[P_POINTER]);
}
GeometryRef[FINALIZATION].unregister(this);
GeometryRef[CLEANUP](this[POINTER]);
this.detached = true;
}
/** @internal */
constructor(ptr, type, extras) {
GeometryRef[FINALIZATION].register(this, ptr, this);
this[POINTER] = ptr;
this.type = type || GEOSGeometryTypeDecoder[geos.GEOSGeomTypeId(ptr)];
if (extras) {
if (extras.id != null) {
this.id = extras.id;
}
if (extras.properties != null) {
this.props = extras.properties;
}
}
}
/** @internal */
static [(_a$1 = FINALIZATION, _b = P_FINALIZATION, CLEANUP)](ptr) {
geos.GEOSGeom_destroy(ptr);
}
/** @internal */
static [P_CLEANUP](ptr) {
geos.GEOSPreparedGeom_destroy(ptr);
}
}
/** @internal */
GeometryRef[_a$1] = (new FinalizationRegistry(GeometryRef[CLEANUP]));
/** @internal */
GeometryRef[_b] = (new FinalizationRegistry(GeometryRef[P_CLEANUP]));
/**
* Prepares geometry to optimize the performance of repeated calls to specific
* geometric operations.
*
* The "prepared geometry" is conceptually similar to a database "prepared
* statement": by doing up-front work to create an optimized object, you reap
* a performance benefit when executing repeated function calls on that object.
*
* List of functions that benefit from geometry preparation:
* - {@link distance}
* - {@link nearestPoints}
* - {@link distanceWithin}
* - {@link intersects}
* - {@link disjoint}
* - {@link contains}
* - {@link containsProperly}
* - {@link within}
* - {@link covers}
* - {@link coveredBy}
* - {@link crosses}
* - {@link overlaps}
* - {@link touches}
* - {@link relate}
* - {@link relatePattern}
*
* Modifies the geometry in-place.
*
* @template G - The type of prepared geometry, for example, {@link Geometry}
* or more specific {@link Polygon}
* @param geometry - Geometry to prepare
* @returns Exactly the same geometry object, but with prepared internal
* spatial indexes
* @throws {GEOSError} on unsupported geometry types (curved)
*
* @see {@link unprepare} frees prepared indexes
* @see {@link isPrepared} checks whether a geometry is prepared
* @see {@link https://libgeos.org/usage/c_api/#prepared-geometry}
*
* @example lifecycle of the prepared geometry
* const regularPolygon = buffer(point([ 0, 0 ]), 10, { quadrantSegments: 1000 });
* const preparedPolygon = prepare(regularPolygon);
* const regularPolygonAgain = unprepare(preparedPolygon);
* // `regularPolygon`, `preparedPolygon` and `regularPolygonAgain` are exactly the same object
* // so if you do not care about TypeScript, the above can be simplified to:
* const p = buffer(point([ 0, 0 ]), 10, { quadrantSegments: 1000 });
* prepare(p);
* unprepare(p);
*
* @example to improve performance of repeated calls against a single geometry
* const a = buffer(point([ 0, 0 ]), 10, { quadrantSegments: 1000 });
* // `a` is a polygon with many vertices (4000 in this example)
* prepare(a);
* // the preparation of geometry `a` will improve the performance of repeated
* // supported functions (see list above) calls, but only those where `a` is
* // the first geometry
* const d1 = distance(a, point([ 10, 0 ]));
* const d2 = distance(a, point([ 10, 1 ]));
* const d3 = distance(point([ 10, 2 ]), a); // no benefit from prepared geometry
* const i1 = intersects(a, lineString([ [ 0, 22 ], [ 11, 0 ] ]));
* const i2 = intersects(a, lineString([ [ 0, 24 ], [ 11, 0 ] ]));
* const i3 = intersects(lineString([ [ 0, 26 ], [ 11, 0 ] ]), a); // no benefit
*/
function prepare(geometry) {
if (!geometry[P_POINTER]) {
const pPtr = geos.GEOSPrepare(geometry[POINTER]);
GeometryRef[P_FINALIZATION].register(geometry, pPtr, geometry);
geometry[P_POINTER] = pPtr;
}
return geometry;
}
/**
* Frees the prepared internal spatial indexes.
*
* Call this function when you no longer need a performance boost, but need
* the geometry itself and want to reclaim some memory.
*
* The prepared internal spatial indexes will be automatically freed alongside
* the geometry itself, either when released via [`free`]{@link GeometryRef#free}
* or when geometry goes out of scope.
*
* Modifies the geometry in-place.
*
* @template G - The type of prepared geometry, for example, {@link Geometry}
* or more specific {@link Polygon}
* @param geometry - Geometry to free its prepared indexes
* @returns Exactly the same geometry object, but without prepared internal
* spatial indexes
*
* @see {@link prepare} prepares geometry internal spatial indexes
* @see {@link isPrepared} checks whether a geometry is prepared
* @see {@link https://libgeos.org/usage/c_api/#prepared-geometry}
*
* @example lifecycle of the prepared geometry
* const regularPolygon = buffer(point([ 0, 0 ]), 10, { quadrantSegments: 1000 });
* const preparedPolygon = prepare(regularPolygon);
* const regularPolygonAgain = unprepare(preparedPolygon);
* // `regularPolygon`, `preparedPolygon` and `regularPolygonAgain` are exactly the same object
* // so if you do not care about TypeScript, the above can be simplified to:
* const p = buffer(point([ 0, 0 ]), 10, { quadrantSegments: 1000 });
* prepare(p);
* unprepare(p);
*/
function unprepare(geometry) {
if (geometry[P_POINTER]) {
GeometryRef[P_FINALIZATION].unregister(geometry);
GeometryRef[P_CLEANUP](geometry[P_POINTER]);
delete geometry[P_POINTER];
}
return geometry;
}
class InvalidGeoJSONError extends GEOSError {
/** @internal */
constructor(geom, invalidType) {
super(`Invalid ${invalidType ? 'GeoJSON geometry' : geom.type}: ${JSON.stringify(geom)}`);
this.name = 'InvalidGeoJSONError';
}
}
const geosifyMeasureAndValidateGeom = (geom, c) => {
switch (geom?.type) {
case 'Point': {
const pt = geom.coordinates;
const dim = pt.length > 2 ? 3 : 2;
c.f += dim;
c.d += 1; // [header]
return;
}
case 'MultiPoint': {
const pts = geom.coordinates;
const dim = pts[0]?.length > 2 ? 3 : 2;
c.f += pts.length * dim;
c.d += 2; // [header][numPoints]
return;
}
case 'LineString': {
const pts = geom.coordinates;
if (pts.length === 1) {
throw new InvalidGeoJSONError(geom);
}
c.s += 1; // [cs->data]
c.d += 2; // [header][cs->size/ptr]
return;
}
case 'Polygon': {
const ppts = geom.coordinates;
const pptsLength = ppts.length;
for (const pts of ppts) {
const ptsLength = pts.length;
const f = pts[0], l = pts[ptsLength - 1];
if (ptsLength && (ptsLength < 3 || f[0] !== l[0] || f[1] !== l[1])) {
throw new InvalidGeoJSONError(geom);
}
}
c.s += pptsLength; // [R1:cs->data]…[RN:cs->data]
c.d += 2 + pptsLength; // [header][numRings] [R1:cs->size/ptr]…[RN:cs->size/ptr]
return;
}
case 'MultiLineString': {
const ppts = geom.coordinates;
const pptsLength = ppts.length;
for (const pts of ppts) {
if (pts.length === 1) {
throw new InvalidGeoJSONError(geom);
}
}
c.s += pptsLength; // [L1:cs->data]…[LN:cs->data]
c.d += 2 + pptsLength; // [header][numLines] [L1:cs->size/ptr]…[LN:cs->size/ptr]
return;
}
case 'MultiPolygon': {
const pppts = geom.coordinates;
c.d += 2 + pppts.length; // [header][numPolygons] [P1:numRings]…[PN:numRings]
for (const ppts of pppts) {
const pptsLength = ppts.length;
for (const pts of ppts) {
const ptsLength = pts.length;
const f = pts[0], l = pts[ptsLength - 1];
if (ptsLength && (ptsLength < 3 || f[0] !== l[0] || f[1] !== l[1])) {
throw new InvalidGeoJSONError(geom);
}
}
c.s += pptsLength; // [R1:cs->data]…[RN:cs->data]
c.d += pptsLength; // [R1:cs->size/ptr]…[RN:cs->size/ptr]
}
return;
}
case 'GeometryCollection': {
const geoms = geom.geometries;
for (const g of geoms) {
geosifyMeasureAndValidateGeom(g, c);
}
c.d += 2; // [header][numGeometries]
return;
}
}
throw new InvalidGeoJSONError(geom, true);
};
const geosifyEncodeGeom = (geom, s) => {
const { B, F } = s;
let { d, f } = s;
switch (geom.type) {
case 'Point': {
const pt = geom.coordinates;
const dim = pt.length;
// B[ b++ ] = typeId | (isEmpty << 4) | (+hasZ << 5);
if (dim) {
F[f++] = pt[0];
F[f++] = pt[1];
if (dim > 2) {
F[f++] = pt[2];
B[d++] = 32; // typeId | (0 << 4) | (1 << 5)
}
else {
B[d++] = 0; // typeId | (0 << 4) | (0 << 5)
}
}
else {
B[d++] = 16; // typeId | (1 << 4) | (0 << 5)
}
break;
}
case 'MultiPoint': {
const pts = geom.coordinates;
const hasZ = pts[0]?.length > 2;
B[d++] = hasZ ? 36 : 4; // typeId | (+hasZ << 5);
B[d++] = pts.length;
for (const pt of pts) {
F[f++] = pt[0];
F[f++] = pt[1];
if (hasZ) {
F[f++] = pt[2];
}
}
break;
}
case 'LineString': {
const pts = geom.coordinates;
const hasZ = pts[0]?.length > 2;
B[d++] = hasZ ? 33 : 1; // typeId | (+hasZ << 5);
B[d++] = pts.length;
break;
}
case 'Polygon': {
const ppts = geom.coordinates;
const hasZ = ppts[0]?.[0]?.length > 2;
B[d++] = hasZ ? 35 : 3; // typeId | (+hasZ << 5);
B[d++] = ppts.length;
for (const pts of ppts) {
B[d++] = pts.length;
}
break;
}
case 'MultiLineString': {
const ppts = geom.coordinates;
const hasZ = ppts[0]?.[0]?.length > 2;
B[d++] = hasZ ? 37 : 5; // typeId | (+hasZ << 5);
B[d++] = ppts.length;
for (const pts of ppts) {
B[d++] = pts.length;
}
break;
}
case 'MultiPolygon': {
const pppts = geom.coordinates;
const hasZ = pppts[0]?.[0]?.[0]?.length > 2;
B[d++] = hasZ ? 38 : 6; // typeId | (+hasZ << 5);
B[d++] = pppts.length;
for (const ppts of pppts) {
B[d++] = ppts.length;
for (const pts of ppts) {
B[d++] = pts.length;
}
}
break;
}
case 'GeometryCollection': {
const geoms = geom.geometries;
B[s.d++] = 7;
B[s.d++] = geoms.length;
for (const g of geoms) {
geosifyEncodeGeom(g, s);
}
return;
}
}
s.f = f;
s.d = d;
};
const geosifyPopulateGeom = (geom, s) => {
const { B, F } = s;
switch (geom.type) {
// Point & MultiPoint - skip
case 'LineString': {
const pts = geom.coordinates;
let f = B[s.s++];
for (const pt of pts) {
F[f++] = pt[0];
F[f++] = pt[1];
F[f++] = pt.length > 2 ? pt[2] : NaN;
}
break;
}
case 'Polygon':
case 'MultiLineString': {
const ppts = geom.coordinates;
for (const pts of ppts) {
let f = B[s.s++];
for (const pt of pts) {
F[f++] = pt[0];
F[f++] = pt[1];
F[f++] = pt.length > 2 ? pt[2] : NaN;
}
}
break;
}
case 'MultiPolygon': {
const pppts = geom.coordinates;
for (const ppts of pppts) {
for (const pts of ppts) {
let f = B[s.s++];
for (const pt of pts) {
F[f++] = pt[0];
F[f++] = pt[1];
F[f++] = pt.length > 2 ? pt[2] : NaN;
}
}
}
break;
}
case 'GeometryCollection': {
const geoms = geom.geometries;
for (const g of geoms) {
geosifyPopulateGeom(g, s);
}
}
}
};
/**
* Creates a {@link Geometry} from GeoJSON geometry object.
*
* @param geojson - GeoJSON geometry object
* @param extras - Optional geometry extras
* @returns A new geometry
* @throws {InvalidGeoJSONError} on invalid GeoJSON geometry
*
* @example
* const pt = geosifyGeometry({ type: 'Point', coordinates: [ 1, 1 ] });
* const line = geosifyGeometry({ type: 'LineString', coordinates: [ [ 0, 0 ], [ 1, 1 ] ] });
* const collection = geosifyGeometry({
* type: 'GeometryCollection',
* geometries: [
* { type: 'Point', coordinates: [ 1, 1 ] },
* { type: 'LineString', coordinates: [ [ 0, 0 ], [ 1, 1 ] ] },
* ],
* });
* pt.type; // 'Point'
* line.type; // 'LineString'
* collection.type; // 'GeometryCollection'
*/
function geosifyGeometry(geojson, extras) {
const c = { d: 0, s: 0, f: 0 };
geosifyMeasureAndValidateGeom(geojson, c);
const buff = geos.buffByL4(3 + c.d + c.s + c.f * 2);
try {
let B = geos.U32;
let d = buff.i4, s, f;
B[d++] = c.d;
B[d++] = c.s;
s = d + c.d;
f = Math.ceil((s + c.s) / 2);
const es = { B, d, F: geos.F64, f };
geosifyEncodeGeom(geojson, es);
if (c.s) {
geos.geosify_geomsCoords(buff[POINTER]);
const ps = { B: geos.U32, s, F: geos.F64 };
geosifyPopulateGeom(geojson, ps);
}
geos.geosify_geoms(buff[POINTER]);
B = geos.U32;
return new GeometryRef(B[d], geojson.type, extras);
}
finally {
buff.freeIfTmp();
}
}
/**
* Creates an array of {@link GeometryRef} from an array of GeoJSON feature objects.
*
* @param geojsons - Array of GeoJSON feature objects
* @returns An array of new geometries
* @throws {InvalidGeoJSONError} on GeoJSON feature without geometry
* @throws {InvalidGeoJSONError} on invalid GeoJSON geometry
*
* @example
* const [ pt, line, collection ] = geosifyFeatures([
* {
* type: 'Feature',
* geometry: { type: 'Point', coordinates: [ 1, 1 ] },
* properties: null,
* },
* {
* type: 'Feature',
* geometry: { type: 'LineString', coordinates: [ [ 0, 0 ], [ 1, 1 ] ] },
* properties: null,
* },
* {
* type: 'Feature',
* geometry: {
* type: 'GeometryCollection',
* geometries: [
* { type: 'Point', coordinates: [ 1, 1 ] },
* { type: 'LineString', coordinates: [ [ 0, 0 ], [ 1, 1 ] ] },
* ],
* },
* properties: null,
* },
* ]);
* pt.type; // 'Point'
* line.type; // 'LineString'
* collection.type; // 'GeometryCollection'
*/
function geosifyFeatures(geojsons) {
const c = { d: 0, s: 0, f: 0 };
for (const geom of geojsons) {
geosifyMeasureAndValidateGeom(geom.geometry, c);
}
const buff = geos.buffByL4(3 + c.d + c.s + c.f * 2);
try {
let B = geos.U32;
let d = buff.i4, s, f;
B[d++] = c.d;
B[d++] = c.s;
s = d + c.d;
f = Math.ceil((s + c.s) / 2);
const es = { B, d, F: geos.F64, f };
for (const geom of geojsons) {
geosifyEncodeGeom(geom.geometry, es);
}
if (c.s) {
geos.geosify_geomsCoords(buff[POINTER]);
const ps = { B: geos.U32, s, F: geos.F64 };
for (const geom of geojsons) {
geosifyPopulateGeom(geom.geometry, ps);
}
}
geos.geosify_geoms(buff[POINTER]);
B = geos.U32;
const geometriesLength = geojsons.length;
const geosGeometries = Array(geometriesLength);
for (let i = 0; i < geometriesLength; i++) {
const feature = geojsons[i];
geosGeometries[i] = new GeometryRef(B[d++], feature.geometry.type, feature);
}
return geosGeometries;
}
finally {
buff.freeIfTmp();
}
}
/**
* Creates a {@link Point} geometry from a position.
*
* @param pt - Point coordinates
* @param options - Optional geometry options
* @returns A new point Geometry object
*
* @example
* const a = point([ 0, 0 ]);
* const b = point([ 2, 0 ], { properties: { name: 'B' } });
* const wkt = toWKT(a); // 'POINT (0 0)'
*/
function point(pt, options) {
return geosifyGeometry({ type: 'Point', coordinates: pt }, options);
}
/**
* Creates a {@link LineString} geometry from an array of positions.
*
* Line string must contain at least 2 positions.
* Empty line strings with 0 positions are allowed.
*
* @param pts - LineString coordinates
* @param options - Optional geometry options
* @returns A new Geometry object
* @throws {InvalidGeoJSONError} on line with 1 position
*
* @example
* const a = lineString([ [ 0, 0 ], [ 2, 1 ], [ 0, 2 ] ]);
* const b = lineString([ [ 2, 0 ], [ 4, 0 ] ], { properties: { name: 'B' } });
* const wkt = toWKT(a); // 'LINESTRING (0 0, 2 1, 0 2)'
*/
function lineString(pts, options) {
return geosifyGeometry({ type: 'LineString', coordinates: pts }, options);
}
/**
* Creates a {@link Polygon} geometry from an array of linear rings coordinates.
*
* The first ring represents the exterior ring (shell), subsequent rings
* represent interior rings (holes). Each ring must be a closed line string
* with first and last positions identical and contain at least 3 positions.
* Empty polygons without any rings are allowed.
*
* @param ppts - Polygon coordinates
* @param options - Optional geometry options
* @returns A new Geometry object
* @throws {InvalidGeoJSONError} if any ring is invalid (not closed or with 1 or 2 positions)
*
* @example
* const a = polygon([ [ [ 4, 3 ], [ 5, 4 ], [ 5, 3 ], [ 4, 3 ] ] ]);
* const b = polygon([
* [ [ 0, 0 ], [ 0, 8 ], [ 8, 8 ], [ 8, 0 ], [ 0, 0 ] ],
* [ [ 2, 2 ], [ 6, 6 ], [ 6, 2 ], [ 2, 2 ] ],
* ], { properties: { name: 'B' } });
* const wkt = toWKT(a); // 'POLYGON ((4 3, 5 4, 5 3, 4 3))'
*/
function polygon(ppts, options) {
return geosifyGeometry({ type: 'Polygon', coordinates: ppts }, options);
}
/**
* Creates a {@link MultiPoint} geometry from an array of positions.
*
* @param pts - MultiPoint coordinates
* @param options - Optional geometry options
* @returns A new Geometry object
*
* @example
* const a = multiPoint([ [ 0, 0 ], [ 2, 0 ], [ 4, 0 ] ]);
* const b = multiPoint([ [ 1, 0 ], [ 3, 0 ] ], { properties: { name: 'B' } });
* const wkt = toWKT(a); // 'MULTIPOINT ((0 0), (2 0), (4 0))'
*/
function multiPoint(pts, options) {
return geosifyGeometry({ type: 'MultiPoint', coordinates: pts }, options);
}
/**
* Creates a {@link MultiLineString} geometry from an array of line strings coordinates.
*
* Each line string must contain at least 2 positions.
* Empty line strings with 0 positions are allowed.
*
* @param ppts - MultiLineString coordinates
* @param options - Optional geometry options
* @returns A new Geometry object
* @throws {InvalidGeoJSONError} on line with 1 position
*
* @example
* const a = multiLineString([
* [ [ -10, 3 ], [ 5, 4 ] ],
* [ [ -10, 7 ], [ 5, 6 ] ],
* ]);
* const b = multiLineString([
* [ [ 0, 0 ], [ 10, 5 ], [ 0, 10 ] ],
* [ [ 1, 0 ], [ 12, 5 ], [ 1, 10 ] ],
* ], { properties: { name: 'B' } });
* const wkt = toWKT(a); // 'MULTILINESTRING ((-10 3, 5 4), (-10 7, 5 6))'
*/
function multiLineString(ppts, options) {
return geosifyGeometry({ type: 'MultiLineString', coordinates: ppts }, options);
}
/**
* Creates a {@link MultiPolygon} geometry from an array of polygon coordinates.
*
* Each polygon must consist of an array of linear rings coordinates.
* The first ring represents the exterior ring (shell), subsequent rings
* represent interior rings (holes). Each ring must be a closed line string
* with first and last positions identical and contain at least 3 positions.
* Empty polygons without any rings are allowed.
*
* @param pppts - MultiPolygon coordinates
* @param options - Optional geometry options
* @returns A new Geometry object
* @throws {InvalidGeoJSONError} if any ring is invalid (not closed or with 1 or 2 positions)
*
* @example
* const a = multiPolygon([
* [ [ [ 1, 0 ], [ 0, 1 ], [ 1, 1 ], [ 1, 0 ] ] ],
* [ [ [ 1, 1 ], [ 1, 2 ], [ 2, 1 ], [ 1, 1 ] ] ],
* ]);
* const b = multiPolygon([
* [ [ [ 0, 1 ], [ 1, 2 ], [ 1, 1 ], [ 0, 1 ] ] ],
* [ [ [ 1, 0 ], [ 1, 1 ], [ 2, 1 ], [ 1, 0 ] ] ],
* ], { properties: { name: 'B' } });
* const wkt = toWKT(a); // 'MULTIPOLYGON (((1 0, 0 1, 1 1, 1 0)), ((1 1, 1 2, 2 1, 1 1)))'
*/
function multiPolygon(pppts, options) {
return geosifyGeometry({ type: 'MultiPolygon', coordinates: pppts }, options);
}
/**
* Creates a {@link GeometryCollection} geometry from an array of geometries.
*
* The collection consumes the input geometries - after creating
* the collection, the input geometries become [detached]{@link GeometryRef#detached},
* are no longer valid and should **not** be used.
*
* @param geometries - Array of geometry objects to be included in the collection
* @param options - Optional geometry options
* @returns A new GeometryCollection containing all input geometries
*
* @example
* const a = polygon([ [ [ 4, 1 ], [ 4, 3 ], [ 8, 2 ], [ 4, 1 ] ] ]);
* const b = lineString([ [ 0, 2 ], [ 6, 2 ] ]);
* const c = geometryCollection([ a, b ]);
* const wkt = toWKT(c); // 'GEOMETRYCOLLECTION (POLYGON ((4 1, 4 3, 8 2, 4 1)), LINESTRING (0 2, 6 2))'
*/
function geometryCollection(geometries, options) {
const geometriesLength = geometries.length;
const buff = geos.buffByL4(geometriesLength);
try {
let B = geos.U32, b = buff.i4;
for (const geometry of geometries) {
B[b++] = geometry[POINTER];
}
const geomPtr = geos.GEOSGeom_createCollection(7, buff[POINTER], geometriesLength);
for (const geometry of geometries) {
GeometryRef[FINALIZATION].unregister(geometry);
geometry.detached = true;
}
return new GeometryRef(geomPtr, 'GeometryCollection', options);
}
finally {
buff.freeIfTmp();
}
}
/**
* Creates a rectangular {@link Polygon} geometry from bounding box coordinates.
*
* Polygon is oriented clockwise.
*
* @param bbox - Array of four numbers `[ xMin, yMin, xMax, yMax ]`
* @param options - Optional geometry options
* @returns A new Polygon object
* @throws {GEOSError} when box is degenerated: width or height is `0`
*
* @see {@link bounds} calculates bounding box of an existing geometry
*
* @example
* const b1 = box([ 0, 0, 4, 4 ]); // <POLYGON ((0 0, 0 4, 4 4, 4 0, 0 0))>
* const b2 = box([ 5, 0, 8, 1 ]); // <POLYGON ((5 0, 5 1, 8 1, 8 0, 5 0))>
*/
function box(bbox, options) {
const [xMin, yMin, xMax, yMax] = bbox;
if (xMin === xMax || yMin === yMax) {
throw new GEOSError('Degenerate box'); // point or line
}
return polygon([[[xMin, yMin], [xMin, yMax], [xMax, yMax], [xMax, yMin], [xMin, yMin]]], options);
}
function fromGeoJSON(geojson) {
switch (geojson.type) {
case 'FeatureCollection': {
return geosifyFeatures(geojson.features);
}
case 'Feature': {
return geosifyGeometry(geojson.geometry, geojson);
}
}
return geosifyGeometry(geojson);
}
function toGeoJSON(geometryies) {
if (Array.isArray(geometryies)) {
return { type: 'FeatureCollection', features: jsonifyFeatures(geometryies) };
}
return geometryies.toJSON();
}
/**
* Creates a {@link Geometry} from Well-Known Text (WKT) representation.
*
* @param wkt - String containing WKT representation of the geometry
* @param options - Optional WKT input configuration
* @returns A new geometry object created from the WKT string
* @throws {GEOSError} on invalid WKT string
*
* @see {@link https://libgeos.org/specifications/wkt}
*
* @example
* const pt = fromWKT('POINT(0 2)');
* const line = fromWKT('LINESTRING(1 2, 2 2, 2 0)');
* const poly = fromWKT('POLYGON((0 0, 0 1, 1 1, 1 0, 0 0))');
*
* @example will fix unclosed ring
* const poly = fromWKT('POLYGON((0 0, 1 0, 1 1))', { fix: true });
*/
function fromWKT(wkt, options) {
const cache = geos.t_r;
const key = options
? [options.fix].join()
: '';
let readerPtr = cache[key];
if (!readerPtr) {
const ptr = geos.GEOSWKTReader_create();
if (options) {
const { fix } = options;
if (fix != null) {
geos.GEOSWKTReader_setFixStructure(ptr, +fix);
}
}
readerPtr = cache[key] = ptr;
}
const buff = geos.encodeString(wkt);
try {
const geomPtr = geos.GEOSWKTReader_read(readerPtr, buff[POINTER]);
return new GeometryRef(geomPtr);
}
finally {
buff.freeIfTmp();
}
}
/**
* Converts a geometry object to its Well-Known Text (WKT) representation.
*
* @param geometry - The geometry object to be converted to WKT
* @param options - Optional WKT output configuration
* @returns String with WKT representation of the geometry
*
* @see {@link https://libgeos.org/specifications/wkt}
*
* @example
* const pt = point([ 1.1234, 1.9876, 10 ]);
* const wkt1 = toWKT(pt); // 'POINT Z (1.1234 1.9876 10)'
* const wkt2 = toWKT(pt, { dim: 2 }); // 'POINT (1.1234 1.9876)'
* const wkt3 = toWKT(pt, { precision: 2 }); // 'POINT Z (1.12 1.99 10)'
*/
function toWKT(geometry, options) {
const cache = geos.t_w;
const key = options
? [options.dim, options.precision, options.trim].join()
: '';
let writerPtr = cache[key];
if (!writerPtr) {
const ptr = geos.GEOSWKTWriter_create();
if (options) {
const { dim, precision, trim } = options;
if (dim != null) {
geos.GEOSWKTWriter_setOutputDimension(ptr, dim);
}
if (precision != null) {
geos.GEOSWKTWriter_setRoundingPrecision(ptr, precision);
}
if (trim != null) {
geos.GEOSWKTWriter_setTrim(ptr, +trim);
}
}
writerPtr = cache[key] = ptr;
}
const strPtr = geos.GEOSWKTWriter_write(writerPtr, geometry[POINTER]);
const str = geos.decodeString(strPtr);
geos.free(strPtr);
return str;
}
/**
* Creates a {@link Geometry} from Well-Known Binary (WKB) representation.
*
* @param wkb - Binary data containing WKB representation of the geometry
* @param options - Optional WKB input configuration
* @returns A new geometry object created from the WKB data
* @throws {GEOSError} on invalid WKB data
*
* @see {@link https://libgeos.org/specifications/wkb}
*
* @example
* const wkb = new Uint8Array([
* 1, // 1 - LE
* 1, 0, 0, 0, // 1 - point
* 105, 87, 20, 139, 10, 191, 5, 64, // Math.E - x
* 24, 45, 68, 84, 251, 33, 9, 64, // Math.PI - y
* ]);
* const pt = fromWKB(wkb); // point([ Math.E, Math.PI ]);
*/
function fromWKB(wkb, options) {
const cache = geos.b_r;
const key = options
? [options.fix].join()
: '';
let readerPtr = cache[key];
if (!readerPtr) {
const ptr = geos.GEOSWKBReader_create();
if (options) {
const { fix } = options;
if (fix != null) {
geos.GEOSWKBReader_setFixStructure(ptr, +fix);
}
}
readerPtr = cache[key] = ptr;
}
const wkbLen = wkb.length;
const buff = geos.buffByL(wkbLen);
try {
geos.U8.set(wkb, buff[POINTER]);
const geomPtr = geos.GEOSWKBReader_read(readerPtr, buff[POINTER], wkbLen);
return new GeometryRef(geomPtr);
}
finally {
buff.freeIfTmp();
}
}
/**
* Converts a geometry object to its Well-Known Binary (WKB) representation.
*
* @param geometry - The geometry object to be converted to WKB
* @param options - Optional WKB output configuration
* @returns A Uint8Array containing the WKB representation of the geometry
*
* @see {@link https://libgeos.org/speci