UNPKG

geos.js

Version:

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

267 lines (245 loc) 9.91 kB
import type { GEOSGeometry, Ptr, u32 } from '../core/types/WasmGEOS.mjs'; import type { STRtree } from '../core/types/WasmOther.mjs'; import type { OutPtr } from '../core/reusable-memory.mjs'; import type { Geometry } from '../geom/Geometry.mjs'; import { CLEANUP, FINALIZATION, POINTER } from '../core/symbols.mjs'; import { GEOSError } from '../core/GEOSError.mjs'; import { geos } from '../core/geos.mjs'; export interface STRTreeOptions { /** * The maximum number of child nodes that a tree node may have.\ * The minimum recommended capacity value is 4. * @default 10 */ nodeCapacity?: number; } /** * Constructs a 2D [R-tree](https://en.wikipedia.org/wiki/R-tree) spatial index * using Sort-Tile-Recursive (STR) packing algorithm. * * The index is query-only; once constructed, geometries cannot be added or * removed from the index. * * The tree indexes the [bounding boxes]{@link bounds} of each geometry. * Geometries of any type can be indexed, types can mix within one index. * Empty geometries are ignored during indexing and will never be returned by * queries. * * @template G - The type of indexed geometry * @param geometries - The array of geometries to be indexed * @param options - Optional options object * @returns A new {@link STRTreeRef} instance * @throws {GEOSError} when `options.nodeCapacity` is less than 2 * * @example #live * const geometriesToIndex = [ * point([ 0, 0 ]), point([ 0, 2 ]), point([ 0, 4 ]), * point([ 2, 0 ]), point([ 2, 2 ]), point([ 2, 4 ]), * point([ 4, 0 ]), point([ 4, 2 ]), point([ 4, 4 ]), * point([ 6, 0 ]), lineString([ [ 6, 2 ], [ 6, 4 ] ]), * ]; * const tree = strTreeIndex(geometriesToIndex); * * // index can now be queried by geometry bbox: * const g1 = box([ 3, 1, 7, 3 ]); // <POLYGON ((3 1, 3 3, 7 3, 7 1, 3 1))> * // note that not the geometry but just its bbox will be used by the query * // any geometry type could be used to query index, including points and lines * const queryMatches = tree.query(g1); * // [ <POINT (4 2)>, <LINESTRING (6 2, 6 4)> ] * * // or used to find the nearest tree geometry: * const g2 = point([ 1.5, 1.75 ]); * // `nearest` uses Cartesian distance not between bboxes but actual geometries * const nearestMatch = tree.nearest(g2); * // <POINT (2 2)> */ export function strTreeIndex<G extends Geometry>(geometries: G[], options?: STRTreeOptions): STRTreeRef<G> { const nodeCapacity = options?.nodeCapacity ?? 10; if (nodeCapacity < 2) { throw new GEOSError('Node capacity must be greater than 1'); } const ngeoms = geometries.length; const geoms = geos.malloc<GEOSGeometry[]>(ngeoms * 4); try { let B = geos.U32, b = geoms >>> 2; for (const geometry of geometries) { B[ b++ ] = geometry[ POINTER ]; } const treePtr = geos.STRtree_create(geoms, ngeoms, nodeCapacity); return new STRTreeRef(treePtr, geometries); } catch (e) { geos.free(geoms); throw e; } } /** * Class representing a (STR) [R-tree](https://en.wikipedia.org/wiki/R-tree) * that exists in the Wasm memory. * * STRTree is a query-only R-tree spatial index structure for two-dimensional * data that uses Sort-Tile-Recursive packing algorithm. * * To create new index use {@link strTreeIndex} function. * * @template G - The type of indexed geometry */ export class STRTreeRef<G extends Geometry = Geometry> { /** * The original geometries that were indexed by the tree. * This array is readonly and should not be modified. */ readonly geometries: G[]; /** * Object becomes detached when manually [freed]{@link STRTreeRef#free}. * Detached objects are no longer valid and should not be used. */ detached?: boolean; /** * Returns all geometries whose [bounding box]{@link bounds} intersects * with the query geometry's bounding box. * * @param geometry - The geometry whose bounding box will be used in the * query * @returns An array of geometries whose bounding box intersects with the * query geometry's bounding box. * * @see {@link box} creates Polygon geometry that can be used by the query * when starting from bounds array * * @example #live * const geometries = [ * point([ 0, 2 ]), lineString([ [ 4, 2 ], [ 8, 2 ] ]), * point([ 0, 4 ]), point([ 4, 4 ]), point([ 8, 4 ]), * ]; * const selector = box([ 2, 0, 6, 6 ]); * // <POLYGON ((2 0, 2 6, 6 6, 6 0, 2 0))> * * const tree = strTreeIndex(geometries); * const queryMatches = tree.query(selector); * // [ <LINESTRING (4 2, 8 2)>, <POINT (4 4)> ] * * // the results can be freely processed further * // for example, to get geometries that are fully inside `selector`: * const inside = queryMatches.filter(g => covers(selector, g)); * // [ <POINT (4 4)> ] */ query(geometry: Geometry): G[] { const l = geos.u1 as OutPtr<u32>; const matchesPtr = geos.STRtree_query(this[ POINTER ], geometry[ POINTER ], l[ POINTER ]); return selectMatches(this.geometries, matchesPtr, l); } /** * Returns the geometry with the minimum distance to the query geometry. * * Cartesian distance is calculated between the actual geometries, not * their bounding boxes. * * If there are multiple equidistant geometries, this function will return * only one of them. To return all such geometries, use {@link nearestAll}. * * @param geometry - Geometry for which the nearest neighbor is queried * @returns The nearest geometry or `undefined` if the tree is empty * @throws {GEOSError} if any of the considered candidates for the nearest * geometry is one of unsupported geometry types (curved) * * @example #live * const geometries = [ * point([ 0, 2 ]), lineString([ [ 4, 2 ], [ 8, 2 ] ]), * point([ 0, 4 ]), point([ 4, 4 ]), point([ 8, 4 ]), * ]; * const target = lineString([ [ 1, 0 ], [ 1, 5 ], [ 3, 6 ] ]); * * const tree = strTreeIndex(geometries); * const nearestMatch = tree.nearest(target); * // <POINT (0 2)> * const nearestAllMatches = tree.nearestAll(target); * // [ <POINT (0 2)>, <POINT (0 4)> ] */ nearest(geometry: Geometry): G | undefined { const l = geos.u1 as OutPtr<u32>; const nearestIdx = geos.STRtree_nearest(this[ POINTER ], geometry[ POINTER ], l[ POINTER ]); if (l.get()) { return this.geometries[ nearestIdx ]; } } /** * Returns all geometries with the minimum distance to the query geometry. * * Cartesian distance is calculated between the actual geometries, not * their bounding boxes. * * @param geometry - Geometry for which the nearest neighbors are queried * @returns An array of all nearest geometries with the same minimum * distance to the query geometry, or an empty array if the tree is empty * @throws {GEOSError} if any of the considered candidates for the nearest * geometry is one of unsupported geometry types (curved) * * @example #live * const geometries = [ * point([ 0, 5 ]), lineString([ [ 3, 4 ], [ 5, 5 ], [ 4, 3 ] ]), * point([ 5, 0 ]), lineString([ [ 4, -3 ], [ 5, -5 ], [ 3, -4 ] ]), * point([ 0, -5 ]), lineString([ [ -3, -4 ], [ -5, -5 ], [ -4, -3 ] ]), * point([ -5, 0 ]), lineString([ [ -3, 4 ], [ -5, 5 ], [ -4, 3 ] ]), * ]; * const tree = strTreeIndex(geometries); * * const g1 = point([ 0, 0 ]); * const matches1 = tree.nearestAll(g1); * // all geometries as they are all 5 units away from the `g1` * * const g2 = point([ 4.5, 1.5 ]); * const matches2 = tree.nearestAll(g2); * // [ <POINT (5 0)>, <LINESTRING (3 4, 5 5, 4 3)> ] * * const g3 = point([ 3, 3 ]); * const matches3 = tree.nearestAll(g3); * // [ <LINESTRING (3 4, 5 5, 4 3)> ] */ nearestAll(geometry: Geometry): G[] { const l = geos.u1 as OutPtr<u32>; const matchesPtr = geos.STRtree_nearestAll(this[ POINTER ], geometry[ POINTER ], l[ POINTER ]); return selectMatches(this.geometries, matchesPtr, l); } /** * Frees the Wasm memory allocated for the STRTree object. * * This method exists as a backup for those who find [`FinalizationRegistry`]{@link https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/FinalizationRegistry} * unreliable and want a way to free the memory manually. * * Manually freed object is marked as [detached]{@link STRTreeRef#detached}. */ free(): void { STRTreeRef[ FINALIZATION ].unregister(this); STRTreeRef[ CLEANUP ](this[ POINTER ]); this.detached = true; } /** @internal */ [ POINTER ]: Ptr<STRtree>; /** @internal */ constructor(ptr: Ptr<STRtree>, geometries: G[]) { STRTreeRef[ FINALIZATION ].register(this, ptr, this); this[ POINTER ] = ptr; this.geometries = geometries; } /** @internal */ static readonly [ FINALIZATION ] = ( new FinalizationRegistry(STRTreeRef[ CLEANUP ]) ); /** @internal */ static [ CLEANUP ](ptr: Ptr<STRtree>): void { geos.STRtree_destroy(ptr); } } function selectMatches<G extends Geometry>(geometries: G[], matchesPtr: Ptr<u32> | 0, l: OutPtr<u32>): G[] { if (matchesPtr) { const matchesLength = l.get(); const matches = Array<G>(matchesLength); let B = geos.U32, b = matchesPtr >>> 2; for (let i = 0; i < matchesLength; i++) { matches[ i ] = geometries[ B[ b++ ] ]; } geos.free(matchesPtr); return matches; } return []; }