@honua/geometry — client-side geometry operations

@honua/geometry (the @honua/sdk-js/geometry subpath, split-packaged as @honua/geometry) adds curated, tree-shakeable client-side geometry operations and reprojection to the Honua SDK, wrapping the individual @turf/* packages and proj4. It is typed against the SDK's own GeoJSON contract and never reimplements the underlying algorithms.

The core SDK has no dependency edge into this module, so core-only consumers never pull turf/proj4 into their bundle. See decisions/geometry-library-selection.md for the dependency-placement ADR.

Install / import

# split package (turf/proj4 are regular deps — zero config)
npm install @honua/geometry

# or via the unified SDK subpath (install the turf/proj4 optional peers you use)
npm install @honua/sdk-js @turf/buffer @turf/area proj4
import { area, buffer, project, toWgs84 } from "@honua/geometry";
// unified: import { area, buffer } from "@honua/sdk-js/geometry";

Operations

Op turf backing Notes
buffer(geom, radius, unit?) @turf/buffer Geodesic buffer; unit default "meters".
area(geom) @turf/area Geodesic area in .
length(geom, unit?) @turf/length Geodesic length; unit default "meters".
centroid(geom) @turf/centroid Returns a Point.
bbox(geom) @turf/bbox [minX, minY, maxX, maxY].
simplify(geom, tolerance, highQuality?) @turf/simplify Ramer–Douglas–Peucker vertex reduction.
booleanIntersects(a, b) @turf/boolean-intersects Predicate.
booleanContains(a, b) @turf/boolean-contains Predicate.
booleanWithin(a, b) @turf/boolean-within Predicate.
union(...polys) @turf/union Polygon union; null if empty.
intersect(a, b) @turf/intersect Polygon intersection; null if disjoint.
difference(a, b) @turf/difference Polygon a − b; null if fully removed.
nearestPoint(target, points) @turf/nearest-point Nearest candidate Feature<Point>.
convex(geom) @turf/convex Convex hull; null if undefined.

Spatial-filter envelope migration

Before 1.0, the package root exposed buffer(x, y, distance, spatialReference?) for an axis-aligned bounding box around a point. Despite its name, that helper never created a geometry buffer. It has been renamed so the approximation is visible at the call site:

import { bufferEnvelope } from "@honua/sdk-js";

const nearbyFilter = bufferEnvelope(-157.858, 21.307, 0.01, { wkid: 4326 });

bufferEnvelope expands x and y by distance in the input coordinate system's units. It is planar and axis-aligned; it is not circular, geodesic, or suitable for measuring a setback. Pre-1.0 callers should mechanically rename root or /honua imports and calls from buffer to bufferEnvelope when they intended the old envelope behavior. Callers that intended a true geometry buffer must instead import buffer(geometry, radius, unit?) from @honua/sdk-js/geometry or @honua/geometry, supply WGS84 coordinates, and consume its polygonal GeoJSON result. The two operations deliberately have different signatures and return types.

Units

Linear units (buffer, length): "meters" (default), "kilometers", "miles", "feet", "yards", "nauticalmiles". area always returns square meters — divide by the relevant factor for other units.

Reprojection

Function Purpose
project(geom, fromCrs, toCrs) Reproject between any registered CRS.
toWgs84(geom, fromCrs) Fast path → EPSG:4326.
toWebMercator(geom, fromCrs) Fast path → EPSG:3857.
defineProjection(code, proj4def) Register an additional CRS on demand.
normalizeCrs(code) Resolve numeric/"EPSG:xxxx"/alias codes to a proj4 id.

Only EPSG:4326 and EPSG:3857 are bundled (both are built into proj4, so the module registers nothing at import time and is side-effect-safe for tree-shaking). Web Mercator aliases (3785, 900913, 102100, 102113) are normalized to EPSG:3857. project never mutates its input; a same-CRS call is a deep clone. Extra ordinates (z/m) pass through untouched.

defineProjection(32610, "+proj=utm +zone=10 +datum=WGS84 +units=m +no_defs +type=crs");
const utm = project(feature.geometry, 4326, 32610);

CRS caveats

geometryEngine compat shim

For ArcGIS migrants, @honua/sdk-js/esri-compat exposes a geometryEngine-shaped shim (geometryEngineCompat / geometryEngineAsyncCompat) backed by @honua/geometry. Inputs may be plain Esri-JSON geometries or Honua compat instances (PointCompat, PolygonCompat, …) that expose toJSON(). Results are Esri geometries stamped with the source spatialReference. The migration codemod rewrites import geometryEngine from "@arcgis/core/geometry/geometryEngine" (and geometryEngineAsync) to these shims.

Coverage matrix

geometryEngine op Shim turf backing Semantic difference vs. Esri
buffer buffer @turf/buffer Esri buffer is planar; the shim is geodesic (closer to geodesicBuffer).
union union @turf/union Coordinate-space; parity within polygon-clipping tolerance.
intersect intersect @turf/intersect Coordinate-space. null when disjoint.
difference difference @turf/difference Coordinate-space.
geodesicArea geodesicArea @turf/area Reprojects to WGS84; spherical model.
planarArea planarArea shoelace Exact in a projected CRS (e.g. Web Mercator); meaningless in degrees.
geodesicLength geodesicLength @turf/length Reprojects to WGS84; spherical model.
planarLength planarLength Euclidean Exact in a projected CRS.
simplify simplify Esri↔GeoJSON round-trip Topological normalization (ring rewinding), not vertex reduction. Use @honua/geometry's simplify(geom, tolerance) for RDP thinning.
convexHull convexHull @turf/convex Coordinate-space.
contains contains @turf/boolean-contains Predicate.
intersects intersects @turf/boolean-intersects Predicate.
geodesicDensify, densify, offset, cut, generalize, nearestCoordinate, nearestVertex, rotate, flip*, clip, overlaps, touches, crosses, within, disjoint, equals, isSimple, relate, symmetricDifference, geodesicBuffer ❌ not shimmed Migration codemod keeps a manual-intervention TODO for these call sites.

Parity tolerances

Numerical parity is tested in test/geometry-engine-compat.test.ts against analytically-derived expected outputs (fixtures use Web Mercator geometries so planar ops have exact values):

Example: measure + buffer + reproject

import { area, buffer, toWgs84 } from "@honua/geometry";

// A parcel returned in Web Mercator (EPSG:3857).
const parcel3857 = feature.geometry;

// Reproject to WGS84 so geodesic measures are correct.
const parcel = toWgs84(parcel3857, 3857);

// Measure it (m²) and grow a 25 m setback ring.
const parcelArea = area(parcel);
const setback = buffer(parcel, 25, "meters");

console.log(`parcel is ${parcelArea.toFixed(0)} m²`);