UNPKG

geos.js

Version:

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

1,435 lines (1,427 loc) 1.56 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 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 ] ]