/** * Geodetic helpers for building a corridor (buffer) polygon around a polyline. * * ## Overview * Given a polyline (sequence of [lon, lat] vertices) and a half-width distance, * this module computes a closed GeoJSON-compatible polygon that represents the * corridor around the line — i.e. all points within `bufferMeters` of the line. * * ## Coordinate system * All coordinates are **[longitude, latitude]** in decimal degrees (WGS-84), * matching the GeoJSON convention used throughout the application. * * ## Geodetic model * All distance and direction calculations use the **spherical-earth (Haversine)** * model with R = 6 371 000 m. Accurate to ~0.3% for buffers up to a few kilometres. */ import type { LineCapStyle } from '../_types/geometryDrawingTypes'; /** * Computes the destination point reached by travelling `distanceM` metres * from `origin` in direction `bearingRad`. * * Spherical direct problem: * ``` * lat2 = asin( sin(lat1)*cos(d) + cos(lat1)*sin(d)*cos(b) ) * lon2 = lon1 + atan2( sin(b)*sin(d)*cos(lat1), cos(d) - sin(lat1)*sin(lat2) ) * ``` * where `d = distanceM / R` (angular distance) and `b` = bearing. * * @param origin Starting point as [lon, lat] in decimal degrees. * @param distanceM Travel distance in metres. * @param bearingRad Bearing in radians, clockwise from north (0=N, pi/2=E, pi=S). * @returns Destination point as [lon, lat] in decimal degrees. */ export declare function destinationPoint(origin: [number, number], distanceM: number, bearingRad: number): [number, number]; /** * Builds a closed corridor polygon around a polyline. * * ## Algorithm * * ### Step 1 — Per-segment bearings * Segment bearings are computed independently for each segment * (`bearing(Pi, Pi+1)`). Interior vertices do **not** average bearings — * averaging was the source of two bugs: * - **Miter spike**: the averaged bisector point is `bufferMeters / sin(α/2)` * from the line, which grows to infinity at sharp turns. * - **180° reversal**: when incoming and outgoing bearings are exactly opposite, * `sinAvg = cosAvg = 0`, so `atan2(0, 0)` returns an arbitrary bearing * and the offset point is placed in the wrong direction entirely. * * ### Step 2 — Round joins at every interior vertex (both sides) * At each interior vertex `Pi`, **both** sides receive a circular arc of radius * `bufferMeters`, sweeping from the incoming offset bearing to the outgoing * offset bearing by `delta`: * * ``` * delta = normalise(b_out - b_in) // signed turn angle, in (-π, π] * * right arc: from (b_in + π/2) sweeping delta to (b_out + π/2) * left arc: from (b_in - π/2) sweeping delta to (b_out - π/2) * ``` * * This matches the `PathLayer` visual layer (`jointRounded: true`) exactly, * so the query polygon sent to the backend encloses the same area that the * user sees highlighted on screen — including all points near bent vertices * on the concave (interior) side. * * Arc density scales with the turn angle: * `numSegs = max(1, ceil(JOIN_SEGS_PER_PI × |delta| / π))`. * * ### Step 3 — Ring assembly * * **Flat caps** (`capStyle = 'flat'`): straight perpendicular edge at both ends. * ``` * leftPoints[0] <────────────────── leftPoints[n] * | (flat cap) (flat cap) | * rightPoints[0] ──────────────────> rightPoints[n] * ``` * * **Round caps** (`capStyle = 'round'`, default): semicircular arcs at both ends. * ``` * leftPoints[0] <────────────────── leftPoints[n] * ╰──(start arc) (end arc)──╯ * rightPoints[0] ──────────────────> rightPoints[n] * ``` * * @param coords Ordered [lon, lat] vertices of the polyline (>= 2 required). * Fewer than 2 points returns an empty array. * @param bufferMeters Corridor **half-width** in metres. Total width = 2 x bufferMeters. * @param capStyle End-cap style: `'round'` (default) or `'flat'`. * @returns Closed ring of [lon, lat] pairs for a GeoJSON `Polygon` exterior ring. */ export declare function buildLineBufferPolygon(coords: [number, number][], bufferMeters: number, capStyle?: LineCapStyle): [number, number][]; /** * Computes the geodetic (Haversine) great-circle distance in metres between two points. * * ``` * a = sin²(dLat/2) + cos(lat1)*cos(lat2)*sin²(dLon/2) * d = R * 2 * atan2(sqrt(a), sqrt(1-a)) * ``` * * @param pointA First point as [lon, lat] in decimal degrees. * @param pointB Second point as [lon, lat] in decimal degrees. * @returns Distance in metres. */ export declare function haversineDistance(pointA: [number, number], pointB: [number, number]): number; /** * Generates a geodesic circle polygon around `center` with a given radius. * * Each vertex is placed using the spherical {@link destinationPoint} formula, * so the polygon accurately represents a circle of `radiusMeters` on the globe * — unlike a flat-earth approximation that distorts at high latitudes or large radii. * * Returns a **closed** ring (last point === first point) compatible with both * GeoJSON `Polygon` coordinates and deck.gl `PolygonLayer`. * * @param center Circle centre as [lon, lat] in decimal degrees. * @param radiusMeters Radius in metres. * @param numPoints Number of ring vertices before closing (default 64). * @returns Closed ring of [lon, lat] pairs. */ export declare function buildCirclePolygon(center: [number, number], radiusMeters: number, numPoints?: number): [number, number][];