geos.js
Version:
an easy-to-use JavaScript wrapper over WebAssembly build of GEOS
1,435 lines (1,427 loc) • 1.56 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 CoordsOptionsMap$1 = {
XY: {
P: (F, f) => ([F[f], F[f + 1]]),
C: (F, l, f, _hasZ, hasM) => {
const stride = 3 + hasM;
const pts = Array(l);
for (let i = 0; i < l; i++, f += stride) {
pts[i] = [F[f], F[f + 1]];
}
return pts;
},
},
XYZ: {
P: (F, f, hasZ) => (hasZ
? [F[f], F[f + 1], F[f + 2]]
: [F[f], F[f + 1]]),
C: (F, l, f, hasZ, hasM) => {
const stride = 3 + hasM;
const pts = Array(l);
for (let i = 0; i < l; i++, f += stride) {
pts[i] = hasZ
? [F[f], F[f + 1], F[f + 2]]
: [F[f], F[f + 1]];
}
return pts;
},
},
XYZM: {
P: (F, f, hasZ, hasM) => (hasM
? [F[f], F[f + 1], F[f + 2], F[f + 3]]
: hasZ
? [F[f], F[f + 1], F[f + 2]]
: [F[f], F[f + 1]]),
C: (F, l, f, hasZ, hasM) => {
const stride = 3 + hasM;
const pts = Array(l);
for (let i = 0; i < l; i++, f += stride) {
pts[i] = hasM
? [F[f], F[f + 1], F[f + 2], F[f + 3]]
: hasZ
? [F[f], F[f + 1], F[f + 2]]
: [F[f], F[f + 1]];
}
return pts;
},
},
XYM: {
P: (F, f, _hasZ, hasM) => (hasM
? [F[f], F[f + 1], F[f + 3]]
: [F[f], F[f + 1]]),
C: (F, l, f, _hasZ, hasM) => {
const stride = 3 + hasM;
const pts = Array(l);
for (let i = 0; i < l; i++, f += stride) {
pts[i] = hasM
? [F[f], F[f + 1], F[f + 3]]
: [F[f], F[f + 1]];
}
return pts;
},
},
};
const jsonifyGeom = (s, o, extended) => {
const { B, F } = s;
const header = B[s.b++];
const typeId = header & 15;
const isEmpty = header & 16;
const hasZ = (header >> 5 & 1);
const hasM = (header >> 6 & 1);
if (extended || typeId < 8) {
if (isEmpty && typeId < 9 && typeId !== 7) {
return {
type: GEOSGeometryTypeDecoder[typeId === 2 ? 1 : typeId],
coordinates: [],
};
}
switch (typeId) {
case 0: { // Point
const pt = o.P(F, s.f, hasZ, hasM);
s.f += hasM ? 4 : hasZ ? 3 : 2;
return { type: GEOSGeometryTypeDecoder[typeId], coordinates: pt };
}
case 4: { // MultiPoint
const ptsLength = B[s.b++];
const pts = Array(ptsLength);
const step = hasM ? 4 : hasZ ? 3 : 2;
for (let i = 0; i < ptsLength; i++, s.f += step) {
pts[i] = o.P(F, s.f, hasZ, hasM);
}
return { type: GEOSGeometryTypeDecoder[typeId], coordinates: pts };
}
case 1: // LineString
case 2: // LinearRing
case 8: { // CircularString
const pts = o.C(F, B[s.b++], B[s.b++], hasZ, hasM);
return { type: GEOSGeometryTypeDecoder[typeId === 2 ? 1 : 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++) {
ppts[j] = o.C(F, B[s.b++], B[s.b++], hasZ, hasM);
}
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++) {
ppts[j] = o.C(F, B[s.b++], B[s.b++], hasZ, hasM);
}
}
return { type: GEOSGeometryTypeDecoder[typeId], coordinates: pppts };
}
case 7: // GeometryCollection
case 9: // CompoundCurve
case 10: // CurvePolygon
case 11: // MultiCurve
case 12: { // MultiSurface
const geomsLength = isEmpty ? 0 : B[s.b++];
const geoms = Array(geomsLength);
for (let i = 0; i < geomsLength; i++) {
geoms[i] = jsonifyGeom(s, o, extended);
}
const type = GEOSGeometryTypeDecoder[typeId], key = CollectionElementsKeyMap[type];
return { type, [key]: geoms };
}
}
}
throw new GEOSError(`${GEOSGeometryTypeDecoder[typeId]} is not standard GeoJSON geometry. Use 'extended' flavor to jsonify all geometry types.`);
};
/**
* Converts a geometry object to its GeoJSON representation.
*
* @param geometry - The geometry object to be converted
* @param layout - Output geometry coordinate layout
* @param extended - Whether to allow all geometry types 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, layout, extended) {
const o = CoordsOptionsMap$1[layout || 'XYZ'];
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, o, extended);
}
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
* @param layout - Output geometry coordinate layout
* @param extended - Whether to allow all geometry types 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, layout, extended) {
const o = CoordsOptionsMap$1[layout || 'XYZ'];
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] = feature(geometry, jsonifyGeom(s, o, extended));
}
return features;
}
finally {
buff.freeIfTmp();
if (tmpOutBuffPtr) {
geos.free(tmpOutBuffPtr);
}
}
}
function feature(f, g) {
return { id: f.id, type: 'Feature', geometry: g, properties: (f.props ?? null) };
}
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',
];
const GEOSGeomTypeIdMap = GEOSGeometryTypeDecoder.reduce((a, t, i) => (a[t] = i, a), {});
const CollectionElementsKeyMap = {
[GEOSGeometryTypeDecoder[7]]: 'geometries',
[GEOSGeometryTypeDecoder[9]]: 'segments',
[GEOSGeometryTypeDecoder[10]]: 'rings',
[GEOSGeometryTypeDecoder[11]]: 'curves',
[GEOSGeometryTypeDecoder[12]]: 'surfaces',
};
/**
* 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()`.
*
* `geom.toJSON()` is equivalent of calling
* `toGeoJSON(geom, { flavor: 'extended', layout: 'XYZM' })`
*
* @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 feature(this, jsonifyGeometry(this, 'XYZM', true));
}
/**
* 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;
}
const CoordsOptionsMap = {
XY: {
L: () => 2,
H: (t) => GEOSGeomTypeIdMap[t],
P: (pt, F, f) => {
F[f++] = pt[0];
F[f++] = pt[1];
return f;
},
C: (pts, F, f) => {
for (const pt of pts) {
F[f++] = pt[0];
F[f++] = pt[1];
F[f++] = NaN;
}
},
},
XYZ: {
L: (l) => l > 2 ? 3 : 2,
H: (t, pt) => {
const l = pt?.length;
return GEOSGeomTypeIdMap[t] | (l > 2 ? 32 : 0);
},
P: (pt, F, f, l) => {
F[f++] = pt[0];
F[f++] = pt[1];
if (l > 2) {
F[f++] = pt[2];
}
return f;
},
C: (pts, F, f, l) => {
const hasZ = l > 2;
for (const pt of pts) {
F[f++] = pt[0];
F[f++] = pt[1];
F[f++] = hasZ ? pt[2] : NaN;
}
},
},
XYZM: {
L: (l) => l > 2 ? l > 3 ? 4 : 3 : 2,
H: (t, pt) => {
const l = pt?.length;
return GEOSGeomTypeIdMap[t] | (l > 2 ? 32 : 0) | (l > 3 ? 64 : 0);
},
P: (pt, F, f, l) => {
F[f++] = pt[0];
F[f++] = pt[1];
if (l > 2) {
F[f++] = pt[2];
if (l > 3) {
F[f++] = pt[3];
}
}
return f;
},
C: (pts, F, f, l) => {
const hasZ = l > 2;
const hasM = l > 3;
for (const pt of pts) {
F[f++] = pt[0];
F[f++] = pt[1];
F[f++] = hasZ ? pt[2] : NaN;
if (hasM) {
F[f++] = pt[3];
}
}
},
},
XYM: {
L: (l) => l > 2 ? 4 : 2,
H: (t, pt) => {
const l = pt?.length;
return GEOSGeomTypeIdMap[t] | (l > 2 ? 64 : 0);
},
P: (pt, F, f, l) => {
F[f++] = pt[0];
F[f++] = pt[1];
if (l > 2) {
F[f++] = NaN;
F[f++] = pt[2];
}
return f;
},
C: (pts, F, f, l) => {
const hasM = l > 2;
for (const pt of pts) {
F[f++] = pt[0];
F[f++] = pt[1];
F[f++] = NaN;
if (hasM) {
F[f++] = pt[2];
}
}
},
},
};
/* ****************************************
* 1) Measure and validate
**************************************** */
class InvalidGeoJSONError extends GEOSError {
/** @internal */
constructor(geom, message, details) {
super(message);
this.name = 'InvalidGeoJSONError';
this.details = details;
this.geometry = geom;
}
}
const ptsTooFewError = (geom, limit, ptsLength, name) => (new InvalidGeoJSONError(geom, `${name} must have at leat ${limit} points`, `found ${ptsLength}`));
const ptsDifferError = (geom, a, b, ringOwner) => (new InvalidGeoJSONError(geom, ringOwner ? `${ringOwner} ring must be closed` : `${geom.type} segments must be continuous`, `points [${a.join()}] and [${b.join()}] are not equal`));
const wrongTypeError = (geom, actual, allowed, partName = 'component') => (new InvalidGeoJSONError(geom, `${geom.type} ${partName} must be ${allowed.map(id => GEOSGeometryTypeDecoder[id]).join(', ').replace(/,( \w+)$/, ' or$1')}`, `"${GEOSGeometryTypeDecoder[actual]}" is not allowed`));
const ptsDiffer = (a, b) => {
if (a.length !== b.length)
return true;
for (let i = 0; i < a.length; i++) {
if (a[i] !== b[i])
return true;
}
};
const validateLine = (geom, pts) => {
const ptsLength = pts.length;
if (ptsLength === 1) {
throw ptsTooFewError(geom, 2, ptsLength, 'LineString');
}
};
const validatePoly = (geom, ppts) => {
for (const pts of ppts) {
const ptsLength = pts.length;
if (ptsLength) { // could be empty
if (ptsLength < 3) {
throw ptsTooFewError(geom, 3, ptsLength, 'Polygon ring');
}
if (ptsDiffer(pts[0], pts[ptsLength - 1])) {
throw ptsDifferError(geom, pts[0], pts[ptsLength - 1], 'Polygon');
}
}
}
};
const geosifyMeasureAndValidateGeom = (geom, c, o) => {
switch (geom?.type) {
case 'Point': {
const pt = geom.coordinates;
const dim = o.L(pt.length);
c.f += dim;
c.d += 1; // [header]
return 0;
}
case 'MultiPoint': {
const pts = geom.coordinates;
const dim = o.L(pts[0]?.length);
c.f += pts.length * dim;
c.d += 2; // [header][numPoints]
return 4;
}
case 'LineString': {
validateLine(geom, geom.coordinates);
c.s += 1; // [cs->data]
c.d += 2; // [header][cs->size/ptr]
return 1;
}
case 'Polygon': {
const ppts = geom.coordinates;
const pptsLength = ppts.length;
validatePoly(geom, ppts);
c.s += pptsLength; // [R1:cs->data]…[RN:cs->data]
c.d += 2 + pptsLength; // [header][numRings] [R1:cs->size/ptr]…[RN:cs->size/ptr]
return 3;
}
case 'MultiLineString': {
const ppts = geom.coordinates;
const pptsLength = ppts.length;
for (const pts of ppts) {
validateLine(geom, pts);
}
c.s += pptsLength; // [L1:cs->data]…[LN:cs->data]
c.d += 2 + pptsLength; // [header][numLines] [L1:cs->size/ptr]…[LN:cs->size/ptr]
return 5;
}
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;
validatePoly(geom, ppts);
c.s += pptsLength; // [R1:cs->data]…[RN:cs->data]
c.d += pptsLength; // [R1:cs->size/ptr]…[RN:cs->size/ptr]
}
return 6;
}
case 'GeometryCollection': {
const geoms = geom.geometries;
for (const g of geoms) {
geosifyMeasureAndValidateGeom(g, c, o);
}
c.d += 2; // [header][numGeometries]
return 7;
}
case 'CircularString': {
const ptsLength = geom.coordinates.length;
if (ptsLength) { // could be empty
if (ptsLength < 3) {
throw new InvalidGeoJSONError(geom, `${geom.type} must have at least one circular arc defined by 3 points`);
}
if (!(ptsLength % 2)) {
throw new InvalidGeoJSONError(geom, `${geom.type} must have and odd number of points`);
}
}
c.s += 1; // [cs->data]
c.d += 2; // [header][cs->size/ptr]
return 8;
}
case 'CompoundCurve': {
const segments = geom.segments;
if (segments.length) {
let last;
for (const segment of segments) {
const t = geosifyMeasureAndValidateGeom(segment, c, o);
if (t !== 1 && t !== 8) {
throw wrongTypeError(geom, t, [1, 8], 'segment');
}
const pts = segment.coordinates;
if (!pts.length) {
throw new InvalidGeoJSONError(geom, `${geom.type} cannot contain empty segments`);
}
if (last && ptsDiffer(last, pts[0])) {
throw ptsDifferError(geom, last, pts[0]);
}
last = pts[pts.length - 1];
}
}
c.d += 2; // [header][numGeometries]
return 9;
}
case 'CurvePolygon': {
const rings = geom.rings;
if (rings.length) {
for (const ring of rings) {
let pts, first, last;
const t = geosifyMeasureAndValidateGeom(ring, c, o);
if (t === 1 || t === 8) {
pts = ring.coordinates;
first = pts[0];
last = pts[pts.length - 1];
}
else if (t === 9) {
const segments = ring.segments;
first = segments[0].coordinates[0];
pts = segments[segments.length - 1].coordinates;
last = pts[pts.length - 1];
}
else {
throw wrongTypeError(geom, t, [1, 8, 9], 'ring');
}
if (first && last && ptsDiffer(first, last)) { // allow for empty rings like with standard polygons
throw ptsDifferError(geom, first, last, geom.type);
}
// TODO each ring must have at least N points
}
}
c.d += 2; // [header][numGeometries]
return 10;
}
case 'MultiCurve': {
const geoms = geom.curves;
for (const g of geoms) {
const t = geosifyMeasureAndValidateGeom(g, c, o);
if (t !== 1 && t !== 8 && t !== 9) {
throw wrongTypeError(geom, t, [1, 8, 9]);
}
}
c.d += 2; // [header][numGeometries]
return 11;
}
case 'MultiSurface': {
const geoms = geom.surfaces;
for (const g of geoms) {
const t = geosifyMeasureAndValidateGeom(g, c, o);
if (t !== 3 && t !== 10) {
throw wrongTypeError(geom, t, [3, 10]);
}
}
c.d += 2; // [header][numGeometries]
return 12;
}
}
throw new InvalidGeoJSONError(geom, 'Invalid geometry');
};
const geosifyEncodeGeom = (geom, s, o) => {
const { B, F } = s;
let { d, f } = s;
// geom header = typeId | (+isEmpty << 4) | (+hasZ << 5) | (+hasM << 6);
const type = geom.type;
switch (type) {
case 'Point': {
const pt = geom.coordinates;
if (pt.length) {
B[d++] = o.H(type, pt);
f = o.P(pt, F, f, pt.length);
}
else {
B[d++] = 16; // 0 | (1 << 4) | (0 << 5) | (0 << 6)
}
break;
}
case 'MultiPoint': {
const pts = geom.coordinates;
const dim = pts[0]?.length;
B[d++] = o.H(type, pts[0]);
B[d++] = pts.length;
for (const pt of pts) {
f = o.P(pt, F, f, dim);
}
break;
}
case 'LineString':
case 'CircularString': {
const pts = geom.coordinates;
B[d++] = o.H(type, pts[0]);
B[d++] = pts.length;
break;
}
case 'Polygon':
case 'MultiLineString': {
const ppts = geom.coordinates;
B[d++] = o.H(type, ppts[0]?.[0]);
B[d++] = ppts.length;
for (const pts of ppts) {
B[d++] = pts.length;
}
break;
}
case 'MultiPolygon': {
const pppts = geom.coordinates;
B[d++] = o.H(type, pppts[0]?.[0]?.[0]);
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':
case 'CompoundCurve':
case 'CurvePolygon':
case 'MultiCurve':
case 'MultiSurface': {
const geoms = geom[CollectionElementsKeyMap[type]];
B[s.d++] = GEOSGeomTypeIdMap[type];
B[s.d++] = geoms.length;
for (const g of geoms) {
geosifyEncodeGeom(g, s, o);
}
return;
}
}
s.f = f;
s.d = d;
};
const geosifyPopulateGeom = (geom, s, o) => {
const { B, F } = s;
switch (geom.type) {
// Point & MultiPoint - skip
case 'LineString':
case 'CircularString': {
const pts = geom.coordinates;
o.C(pts, F, B[s.s++], pts[0]?.length);
break;
}
case 'Polygon':
case 'MultiLineString': {
const ppts = geom.coordinates;
const dim = ppts[0]?.[0]?.length;
for (const pts of ppts) {
o.C(pts, F, B[s.s++], dim);
}
break;
}
case 'MultiPolygon': {
const pppts = geom.coordinates;
const dim = pppts[0]?.[0]?.[0]?.length;
for (const ppts of pppts) {
for (const pts of ppts) {
o.C(pts, F, B[s.s++], dim);
}
}
break;
}
case 'GeometryCollection':
case 'CompoundCurve':
case 'CurvePolygon':
case 'MultiCurve':
case 'MultiSurface': {
const geoms = geom[CollectionElementsKeyMap[geom.type]];
for (const g of geoms) {
geosifyPopulateGeom(g, s, o);
}
}
}
};
/**
* Creates a {@link Geometry} from GeoJSON geometry object.
*
* @param geojson - GeoJSON geometry object
* @param layout - Input geometry coordinate layout
* @param extras - Optional geometry extras
* @returns A new geometry
* @throws {InvalidGeoJSONError} on invalid GeoJSON geometry
*
* @example
* const pt = geosifyGeometry({
* type: 'Point',
* coordinates: [ 1, 1 ],
* }, 'XYZM');
* const line = geosifyGeometry({
* type: 'LineString',
* coordinates: [ [ 0, 0 ], [ 1, 1 ] ],
* }, 'XYZM');
* const collection = geosifyGeometry({
* type: 'GeometryCollection',
* geometries: [
* { type: 'Point', coordinates: [ 1, 1 ] },
* { type: 'LineString', coordinates: [ [ 0, 0 ], [ 1, 1 ] ] },
* ],
* }, 'XYZM');
* pt.type; // 'Point'
* line.type; // 'LineString'
* collection.type; // 'GeometryCollection'
*/
function geosifyGeometry(geojson, layout, extras) {
const o = CoordsOptionsMap[layout || 'XYZM'];
const c = { d: 0, s: 0, f: 0 };
geosifyMeasureAndValidateGeom(geojson, c, o);
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, o);
if (c.s) {
geos.geosify_geomsCoords(buff[POINTER]);
const ps = { B: geos.U32, s, F: geos.F64 };
geosifyPopulateGeom(geojson, ps, o);
}
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
* @param layout - Input geometry coordinate layout
* @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,
* },
* ], 'XYZM');
* pt.type; // 'Point'
* line.type; // 'LineString'
* collection.type; // 'GeometryCollection'
*/
function geosifyFeatures(geojsons, layout) {
const o = CoordsOptionsMap[layout || 'XYZM'];
const c = { d: 0, s: 0, f: 0 };
for (const geom of geojsons) {
geosifyMeasureAndValidateGeom(geom.geometry, c, o);
}
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, o);
}
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, o);
}
}
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
*
* @example
* const a = point([ 0, 0 ]);
* const b = point([ 2, 0 ], { properties: { name: 'B' } });
*/
function point(pt, options) {
return geosifyGeometry({ type: 'Point', coordinates: pt }, options?.layout, 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 LineString geometry
* @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' } });
*/
function lineString(pts, options) {
return geosifyGeometry({ type: 'LineString', coordinates: pts }, options?.layout, 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 Polygon geometry
* @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, 2 ], [ 6, 6 ], [ 2, 2 ] ],
* ], { properties: { name: 'B' } });
*/
function polygon(ppts, options) {
return geosifyGeometry({ type: 'Polygon', coordinates: ppts }, options?.layout, options);
}
function multiPoint(data, options) {
if (areCoords(data)) {
return geosifyGeometry({ type: 'MultiPoint', coordinates: data }, options?.layout, options);
}
checkTypes(data, 1); // `(1 << 0) = 1` accept Point(0)
return collection(4, data, options);
}
function multiLineString(data, options) {
if (areCoords(data)) {
return geosifyGeometry({ type: 'MultiLineString', coordinates: data }, options?.layout, options);
}
checkTypes(data, 2); // `(1 << 1) = 2` accept LineString(1)
return collection(5, data, options);
}
function multiPolygon(data, options) {
if (areCoords(data)) {
return geosifyGeometry({ type: 'MultiPolygon', coordinates: data }, options?.layout, options);
}
checkTypes(data, 8); // `(1 << 3) = 8` accept Polygon(3)
return collection(6, data, options);
}
/**
* Creates a {@link GeometryCollection} geometry from an array of [Geometries]{@link Geometry}.
*
* @param geometries - Array of geometry objects to be included in the collection
* @param options - Optional geometry options
* @returns A new GeometryCollection geometry containing all input geometries
*
* @example
* const parts = [
* polygon([ [ [ 4, 1 ], [ 4, 3 ], [ 8, 2 ], [ 4, 1 ] ]