UNPKG

geos.js

Version:

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

510 lines (463 loc) 17.9 kB
/** * @file * # Geosify - GeoJSON to GEOS conversion overview * * The main idea behind this custom GeoJSON integration is to improve * the efficiency of importing data into Wasm by reducing unnecessary copies * and memory allocations. The Wasm-side implementation of this integration * operates directly on the memory of created coordinate sequences, bypassing * GEOS C-API. The entire process of importing data from GeoJSON consists of * the following steps: * * ## 1. [JS] measure and validate GeoJSON geometries * Measure how big the buffer needs to be to include all necessary information * about a given set of geometries. Verify the validity of provided geometries * to simplify the error handling done by Wasm. * * ## 2. [JS] encode data of GeoJSON geometries * Encode the basic information about the geometries. The resulting buffer * consists of three parts `D`, `S` and `F`: * - `D` - a sequence of `u32` numbers which are a numerical recipe for the * creation of a given set of geometries.\ * A single geometry is defined by a sequence of `u32` numbers, starting with * a header specifying the type of geometry and its dimension (whether it has * a Z coordinate). The next numbers depend on the type of geometry. In the case * of LineString it will be the number of its points, and in the case of Polygon * it will be the number of its rings and the length of each ring. This is somewhat * similar to WKB but stores the `u32` and `f64` values in separate continuous * buffers that keep the data properly aligned, which speeds up writing and reading. * - `S` - a sequence of `u32` numbers which are the pointers to * `GEOSCoordSequence` underlying data * - `F` - a sequence of `f64` numbers which are the concatenated coordinates * of all (Multi)Point geometries * <pre> * // example of a buffer describing MultiPolygon, Point and LineString * D meta (u32) 10 -> number of records in `D` part * S meta (u32) 4 -> number of records in `S` part * D (u32) 6 -> geometry header - MultiPolygon XY [<type>0110 <isEmpty>0 <hasZ>0] * D (u32) └─ 2 -> number of sub-polygons * D (u32) ├─ 2 -> number of rings in the first sub-polygon * D (u32) │ ├─ 4 -> number of coordinates in the first sub-polygon exterior ring * D (u32) │ └─ 6 -> number of coordinates in the first sub-polygon first interior ring * D (u32) └─ 1 -> number of rings in the second sub-polygon * D (u32) └─ 5 -> number of coordinates in the second sub-polygon exterior ring * D (u32) 0 -> geometry header - Point XY [<type>0000 <isEmpty>0 <hasZ>0] * D (u32) 33 -> geometry header - LineString XYZ [<type>0001 <isEmpty>0 <hasZ>1] * D (u32) └─ 8 -> number of coordinates * S (u32) blank * S (u32) blank * S (u32) blank * S (u32) blank * F (f64) some x -> point x coordinate * F (f64) some y -> point y coordinate * </pre> * * ## 3. [Wasm] create blank instances of `GEOSCoordSequence` * `geosify_geomsCoords` initializes `GEOSCoordSequence` with the specified dimensions. * <pre> * // continuation of the above example, `geosify_geomsCoords` will modify `S` and partially `D` * D (u32) 6 * D (u32) └─ 2 * D (u32) ├─ 2 * D *(u32) │ ├─ *GEOSCoordSequence -> pointer to the GEOSCoordSequence XY*4 * D *(u32) │ └─ *GEOSCoordSequence -> CS XY*6 * D (u32) └─ 1 * D *(u32) └─ *GEOSCoordSequence -> CS XY*5 * D (u32) 0 * D (u32) 33 * D *(u32) └─ *GEOSCoordSequence -> CS XYZ*8 * S *(u32) *f64 -> pointer to the data of GEOSCoordSequence XY*4 * S *(u32) *f64 -> CS XY*6 * S *(u32) *f64 -> CS XY*5 * S *(u32) *f64 -> CS XYZ*8 * </pre> * * ## 4. [JS] populate blank `GEOSCoordSequence` with data of GeoJSON geometries directly * * ## 5. [Wasm] create instances of `GEOSGeometry` * Based on the `D` part of the buffer and the already populated `GEOSCoordSequence`s, * `geosify_geoms` creates corresponding `GEOSGeometry` instances. * <pre> * // continuation of the above example, `D` part is replaced by a list of `GEOSGeometry` pointers * D *(u32) *GEOSGeometry -> pointer to the created GEOS MultiPolygon * D *(u32) *GEOSGeometry -> pointer to the created GEOS Point * D *(u32) *GEOSGeometry -> pointer to the created GEOS LineString * </pre> * * ## 6. [JS] retrieve pointers of `GEOSGeometry` */ declare const THIS_FILE: symbol; // to omit ^ @file doc from the bundle import type { Feature as GeoJSON_Feature, Geometry as GeoJSON_Geometry } from 'geojson'; import type { GEOSGeometry, Ptr } from '../core/types/WasmGEOS.mjs'; import { POINTER } from '../core/symbols.mjs'; import { type Geometry, type GeometryExtras, GeometryRef } from '../geom/Geometry.mjs'; import { GEOSError } from '../core/GEOSError.mjs'; import { geos } from '../core/geos.mjs'; export class InvalidGeoJSONError extends GEOSError { /** @internal */ constructor(geom: GeoJSON_Geometry, invalidType?: boolean) { super(`Invalid ${invalidType ? 'GeoJSON geometry' : geom.type}: ${JSON.stringify(geom)}`); this.name = 'InvalidGeoJSONError'; } } interface GeosifyCounter { d: number; // number of records in `D` (geometry set data) s: number; // number of records in `S` (`GEOSCoordSequence` that need to be created) f: number; // number of records in `F` (embedded coordinates of (Multi)Points) } const geosifyMeasureAndValidateGeom = (geom: GeoJSON_Geometry, c: GeosifyCounter): void => { 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); }; interface GeosifyEncodeState { B: Uint32Array; d: number; // `D` iterator F: Float64Array; f: number; // `F` iterator } const geosifyEncodeGeom = (geom: GeoJSON_Geometry, s: GeosifyEncodeState): void => { 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; }; interface GeosifyPopulateState { B: Uint32Array; s: number; // `S` iterator F: Float64Array; } const geosifyPopulateGeom = (geom: GeoJSON_Geometry, s: GeosifyPopulateState): void => { 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' */ export function geosifyGeometry<P>(geojson: GeoJSON_Geometry, extras?: GeometryExtras<P>): Geometry<P> { const c: GeosifyCounter = { 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: number, f: number; B[ d++ ] = c.d; B[ d++ ] = c.s; s = d + c.d; f = Math.ceil((s + c.s) / 2); const es: GeosifyEncodeState = { B, d, F: geos.F64, f }; geosifyEncodeGeom(geojson, es); if (c.s) { geos.geosify_geomsCoords(buff[ POINTER ]); const ps: GeosifyPopulateState = { B: geos.U32, s, F: geos.F64 }; geosifyPopulateGeom(geojson, ps); } geos.geosify_geoms(buff[ POINTER ]); B = geos.U32; return new GeometryRef( B[ d ] as Ptr<GEOSGeometry>, geojson.type, extras, ) as Geometry<P>; } 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' */ export function geosifyFeatures<P>(geojsons: GeoJSON_Feature<GeoJSON_Geometry, P>[]): Geometry<P>[] { const c: GeosifyCounter = { 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: number, f: number; B[ d++ ] = c.d; B[ d++ ] = c.s; s = d + c.d; f = Math.ceil((s + c.s) / 2); const es: GeosifyEncodeState = { 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: GeosifyPopulateState = { 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<Geometry<P>>(geometriesLength); for (let i = 0; i < geometriesLength; i++) { const feature = geojsons[ i ]; geosGeometries[ i ] = new GeometryRef( B[ d++ ] as Ptr<GEOSGeometry>, feature.geometry.type, feature, ) as Geometry<P>; } return geosGeometries; } finally { buff.freeIfTmp(); } }