UNPKG

geos.js

Version:

an easy-to-use JavaScript wrapper over WebAssembly build of GEOS

1,449 lines (1,438 loc) 1.51 MB
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