UNPKG

geos.js

Version:

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

135 lines (124 loc) 5.85 kB
import type { Point as GeoJSON_Point } from 'geojson'; import type { GEOSGeometry, Ptr } from '../core/types/WasmGEOS.mjs'; import type { OutPtr } from '../core/reusable-memory.mjs'; import { POINTER } from '../core/symbols.mjs'; import { type Geometry, GeometryRef } from '../geom/Geometry.mjs'; import { GEOSError } from '../core/GEOSError.mjs'; import { jsonifyGeometry } from '../io/jsonify.mjs'; import { geos } from '../core/geos.mjs'; export class TopologyValidationError extends GEOSError { /** Array with X and Y coordinates of the point at which the error occurred. */ location: [ x: number, y: number ]; /** @internal */ constructor(message: string, location: number[]) { super(message); this.name = 'TopologyValidationError'; this.location = location as [ number, number ]; } } export interface IsValidOptions { /** * Sets how to treat a polygon with self-touching rings. * * If set to `true` the following self-touching conditions are treated * as being valid (ESRI SDE model): * - **inverted shell** - the shell ring self-touches to create a hole * touching the shell * - **exverted hole** - a hole ring self-touches to create two holes * touching at a point * * If set to `false` the above conditions, following the OGC SFS standard, * are treated as not valid. * * @default false */ isInvertedRingValid?: boolean; } /** * Returns `true` when the geometry is well-formed and valid in 2D according to * the OGC rules. For geometries with 3 and 4 dimensions, the validity is still * only tested in 2 dimensions. * * Validity is defined for each Geometry type as follows: * - Point coordinates must be finite (not `NaN` or `Infinity`) * - MultiPoint points must all be valid * - LineString must have at least 2 unique points * - MultiLineString lines must all be valid * - LinearRing must have at lest 4 unique points, be closed (first and last * point must be equal), be [simple]{@link isSimple} i.e. must not self-intersect * except at endpoints * - Polygon interior must be connected (some hole cannot split interior into parts) * - Shell (exterior ring) must be a valid LinearRing, not be self-touching * (could be configured by `options.isInvertedRingValid` parameter), * not be exverted ("bow-tie" configuration) * - Holes (interior rings) each must be a valid LinearRing, be completely * inside the shell, not be nested inside other holes, not self-touch * to create disconnected interiors, not be "C-shaped" with self-touching * that creates islands * - MultiPolygon polygons must all be valid, no polygon can be in the interior * of another polygon, shells cannot partially overlap or touch along an edge * - GeometryCollection geometries must all be valid * - Empty geometries are valid * * @param geometry - The geometry to check * @param options - Optional options object * @returns `true` when geometry is valid, `false` otherwise * @throws {GEOSError} on unsupported geometry types (curved) * * @see {@link isValidOrThrow} throws an error when geometry is not valid * @see {@link makeValid} repairs invalid geometries * * @example #live * const line = lineString([ [ 1, 1 ], [ 2, 2 ] ]); * const line_valid = isValid(line); // true * * const poly = polygon([ // self-touching exterior ring forming hole * [ [ 0, 0 ], [ 0, 10 ], [ 10, 0 ], [ 0, 0 ], [ 4, 2 ], [ 2, 4 ], [ 0, 0 ] ], * ]); * const poly_valid1 = isValid(poly); // false * const poly_valid2 = isValid(poly, { isInvertedRingValid: true }); // true */ export function isValid(geometry: Geometry, options?: IsValidOptions): boolean { return Boolean(geos.GEOSisValidDetail(geometry[ POINTER ], options?.isInvertedRingValid ? 1 : 0, 0 as Ptr<string[]>, 0 as Ptr<GEOSGeometry[]>)); } /** * Asserts whether the geometry is valid. * Same as {@link isValid} but when geometry is not valid instead * of returning `false` throws an error with the reason of invalidity * and with the XY location of the point at which the error occurred. * When geometry is valid, it does nothing. * * @param geometry - The geometry to check * @param options - Optional options object * @throws {GEOSError} on unsupported geometry types (curved) * @throws {TopologyValidationError} on invalid geometry * * @see {@link isValid} checks whether a geometry is valid (`true`/`false`) * @see {@link makeValid} repairs invalid geometries * * @example #live * isValidOrThrow(lineString([ [ 0, 0 ], [ 1, 1 ] ])); // pass * const selfTouchingExteriorRingFormingHole = polygon([ * [ [ 0, 0 ], [ 0, 10 ], [ 10, 0 ], [ 0, 0 ], [ 4, 2 ], [ 2, 4 ], [ 0, 0 ] ], * ]); * isValidOrThrow(selfTouchingExteriorRingFormingHole, { isInvertedRingValid: true }); // pass * isValidOrThrow(selfTouchingExteriorRingFormingHole); // throw * // TopologyValidationError { message: 'Ring Self-intersection', location: [ 0, 0 ] } * * isValidOrThrow(polygon([ [ [ 0, 0 ], [ 1, 1 ], [ 1, 0 ], [ 0, 1 ], [ 0, 0 ] ] ])); // throw * // TopologyValidationError { message: 'Self-intersection', location: [ 0.5, 0.5 ] } */ export function isValidOrThrow(geometry: Geometry, options?: IsValidOptions): void { const r = geos.u1 as OutPtr<string[]>; const l = geos.u2 as OutPtr<GEOSGeometry[]>; const isValid = geos.GEOSisValidDetail(geometry[ POINTER ], options?.isInvertedRingValid ? 1 : 0, r[ POINTER ], l[ POINTER ]); if (!isValid) { const reasonPtr = r.get(); const reason = geos.decodeString(reasonPtr); const pt = new GeometryRef(l.get(), 'Point'); const location = jsonifyGeometry<GeoJSON_Point>(pt).coordinates; geos.free(reasonPtr); pt.free(); throw new TopologyValidationError(reason, location); } }