import { type GeoJsonBbox, type GeoJsonFeature, type GeoJsonGeometry, type GeoJsonMultiPolygon, type GeoJsonPosition } from "@trackunit/geo-json-utils"; import { type ShapeStyle } from "@trackunit/react-map-adapter-shared"; import type { ResolveShapeStackOrder } from "./shapeStackOrder"; /** * Pure fill-tiling core for ADR-0021. Given a set of features, the current * viewport, a stack-order resolver, and an optional cursor, it computes the * clipped fill geometry for every feature whose fill overlaps a neighbour. * * Non-overlapping features and the top-ranked feature of each overlap group * keep their full geometry (no entry returned → adapter falls back to full). * Fully covered features map to an empty MultiPolygon (no fill painted). * * Kept free of React/rAF so it is unit-testable; the surrounding hook owns the * pointermove throttling and emits the result through the adapter. */ /** Empty fill = painted by nobody (fully covered loser or suppressed feature). */ export declare const EMPTY_FILL: GeoJsonMultiPolygon; /** * A feature's fill is suppressed when `fillOpacity` is exactly `0` on the * resolved style. Suppressed features are excluded from tiling stack * computation (no overlap ownership, no clip neighbour) and from fill * hit-testing (stroke remains interactive). */ export declare const isFillSuppressed: (style: ShapeStyle | undefined) => boolean; /** Per-feature visible fill lookup for hit-testing (clipped geometry from tiling). */ export type VisibleFillLookup = (featureId: string) => GeoJsonGeometry | null | undefined; export type FillTilingInput = Readonly<{ features: ReadonlyArray; /** Only features whose bbox intersects this are considered. */ viewportBounds: GeoJsonBbox; resolveStackOrder: ResolveShapeStackOrder; /** * Feature id that is currently selected. Selection forces the feature to the * top of its overlap group, overriding resting order. Takes highest priority. */ selectedFeatureId?: string | null; /** * Feature ids whose fill is suppressed (`fillOpacity === 0`). Suppressed * features are excluded from the tiling stack (no overlap group, no clip * of neighbours) and receive `EMPTY_FILL` when their bbox is in the * viewport. They get no `featureToGroupKey` entry so promotion is a no-op. */ suppressedFeatureIds?: ReadonlySet; /** * Layer-level geodesic flag. When `true` (or `undefined`), polygon/line edges * follow great-circle arcs and coordinates are densified before any clip math. * Mirror of `ShapeStyle.geodesic` at the layer level. */ layerGeodesic: boolean | undefined; /** * Per-feature style overrides. `geodesic` is read from each entry to determine * whether that feature's edges should be densified (overrides `layerGeodesic`). */ featureStyles: ReadonlyMap | undefined; }>; export type FillTilingResult = Readonly<{ /** Feature id → clipped fill geometry. Only clipped/covered features appear. */ fillGeometries: ReadonlyMap; /** * Feature id → group key. Lets the hook look up which group a stroke-hover * hit belongs to so it can set the promotion overlay on the next hover. */ featureToGroupKey: ReadonlyMap; /** * Resting z-index per feature in each overlap group (rank 0 = highest value = * visually on top). Only features that belong to an overlap group are included. * Used by Path B promotion: the hook keeps these stable across hover events and * only overrides the promoted feature's value to raise it above its peers. */ featureZIndex: ReadonlyMap; /** Features excluded from fill tiling when boundary schema validation fails. */ droppedInvalidFeatures: ReadonlyArray; /** * Feature pairs whose overlap test threw inside `polygon-clipping` (SAGA-743). * The pair was treated as non-overlapping; surfaced here so the hook can report * it to Sentry, since the geometry is schema-valid and never hits * `droppedInvalidFeatures`. */ intersectionFailures: ReadonlyArray; }>; export type DroppedInvalidFillTilingFeature = Readonly<{ feature: GeoJsonFeature; /** `safeParse` failure details (`parsed.error.format()`). */ validationError: unknown; }>; export type FillTilingIntersectionFailure = Readonly<{ featureIdA: string; featureIdB: string; /** The error thrown by `polygon-clipping` (typically "Unable to complete output ring"). */ error: unknown; }>; /** * Planar area of any GeoJSON geometry (outer rings minus holes). Returns 0 for * non-polygonal types (Point, LineString, …). Relative magnitude only — not * geodesically accurate, but consistent enough for stack-order and tiebreak use. */ export declare const geometryArea: (geometry: GeoJsonGeometry) => number; /** Geographic degrees spanned by one screen pixel at a given zoom and tile size. */ export declare const degreesPerPixel: (zoom: number, tileSize: number) => number; /** A single hit-test result from `shapesUnderCursor`. */ export type ShapesUnderCursorHit = Readonly<{ featureId: string; /** True when the position is inside the polygon fill (holes respected). */ fill: boolean; /** True when the position is within half the rendered stroke width of any boundary ring. */ stroke: boolean; /** * Minimum distance from `position` to the nearest boundary segment, in geographic * degrees. `null` when the geometry could not be parsed. Used by callers for * tiebreaks (e.g. "top stroke hit = smallest area"). */ distanceToBoundary: number | null; }>; /** * Adapter-agnostic hit test: given a cursor `position` and a list of GeoJSON * features, returns all features whose fill or stroke region contains the * position, along with raw distance-to-boundary for caller tiebreaks. * * - **fill**: point-in-polygon on the feature's **visible** fill geometry when * `visibleFillFor` is supplied (clipped tiling output); otherwise the full * feature geometry. Stroke distance always uses the full outline. * - **stroke**: `distanceToBoundary < max(strokeWidth, 3) / 2` pixels, converted * to geographic degrees via `degreesPerPixel(zoom, tileSize)`. * - Bbox-prefiltered for performance; results are in deterministic (feature-id) order. */ export declare const shapesUnderCursor: (position: GeoJsonPosition, features: ReadonlyArray, options: Readonly<{ zoom: number; tileSize: number; strokeWidthFor: (featureId: string) => number; /** * Optional per-feature visible fill geometry (from fill tiling). Absent entry * = full geometry; null / empty MultiPolygon = no visible fill. */ visibleFillFor?: VisibleFillLookup; /** * Layer-level geodesic flag. When `true` (or `undefined`), edges are * densified before boundary-distance and point-in-polygon checks so stroke * and fill hits follow great-circle arcs. */ layerGeodesic: boolean | undefined; /** * Per-feature style overrides; `geodesic` is read per-feature to determine * whether that feature's edges should be densified. */ featureStyles: ReadonlyMap | undefined; }>) => Array; /** * Hover-promotion overlay for one overlap group: the winner renders at full * geometry (caller removes its fill override). Each peer is clipped against the * winner's full outline starting from its **resting** fill (when present), not * its full geometry — so peer-to-peer overlap ownership from resting tiling is * preserved and translucent fills do not compound under the winner. */ export declare const computePromotionGroupFills: (members: ReadonlyArray, winnerId: string, restingPeerFills?: ReadonlyMap, layerGeodesic?: boolean | undefined, featureStyles?: ReadonlyMap | undefined) => ReadonlyMap; export declare const computeFillTiling: (input: FillTilingInput) => FillTilingResult;