import type { GeoJsonBbox, GeoJsonFeature, GeoJsonPosition } from "@trackunit/geo-json-utils"; import type { MarkerAnchor } from "@trackunit/react-map-adapter-shared"; import { type EdgeIdentity, type EdgeInsets, type EdgeLabelAnchor, type EdgeLabelPlacementResolver, type EdgePlacement } from "./findBestEdgePosition"; /** * Placement mode chosen by the caller (typically derived from a `resolveLabel` * callback on the `edge-auto` anchor). * * - `"auto"` — only place when an edge fits the measured label width. * - `"force"` — if no edge fits, retry with no width requirement so the best * available edge holds the label (may overflow visually). * * In both modes the label is hidden when the feature is entirely off-screen. */ export type EdgeAutoPlacementMode = "auto" | "force"; export type EdgeAutoPlacementOptions = Readonly<{ /** Pixel width measurement callback for the label text. */ labelPixelWidth: (label: string) => number; /** Max angle from horizontal for readable rotated text (degrees). */ maxReadableAngleDeg: number | undefined; /** Previous edge identity for hysteresis — avoids label jumping during panning. */ previousEdgeIdentity: EdgeIdentity | undefined; /** Previous point position for hysteresis — avoids MultiPoint label jumping during panning. */ previousPointPosition: GeoJsonPosition | undefined; /** * Geographic position of the previous anchor, for "don't slide back left" hysteresis. * When provided, a left-anchored label will not retreat left once the viewport has * pushed it rightward along the edge to stay in view. */ previousAnchorGeo?: GeoJsonPosition; /** * The layout side ("above" | "below") from the previous frame. * When provided, the label will not flip sides unless the current side * no longer fits within the viewport. */ previousLayoutSide?: "above" | "below"; /** Per-edge insets from shape decorations (e.g. corner badges) to avoid overlap. */ edgeInsets: ReadonlyArray | undefined; /** Where the label attaches along the selected edge. Defaults to `"left"`. */ labelAnchor?: EdgeLabelAnchor; /** Custom placement resolver. When omitted, `defaultEdgeLabelPlacementResolver` is used. */ labelPlacementResolver?: EdgeLabelPlacementResolver; /** Placement mode. Defaults to `"auto"`. */ mode?: EdgeAutoPlacementMode; /** * When `true`, the label anchor and rotation angle are computed via great-circle * arc math rather than Mercator straight-line interpolation. Should match * `ShapeStyle.geodesic` so the label tracks the visible curved edge. * Defaults to `false`. */ geodesic?: boolean; }>; export type EdgeAutoPlacementOutcome = Readonly<{ type: "placed"; placement: EdgePlacement; anchor: MarkerAnchor; }> | Readonly<{ type: "hidden"; }>; /** * Compute the best placement for a shape label. * * Centralizes layout decisions: geometry branching (Point vs polygon/line), * pixel width estimation, edge-finding for non-point geometries, and * placement-to-anchor mapping. * * Returns a discriminated outcome: * - `"placed"` — label positioned on an edge with placement + anchor * - `"hidden"` — shape is off-screen, no qualifying edge, or (mode `"auto"`) no * edge fits the label width * * Whether the viewport is fully inside the shape is no longer decided here — * callers compute that as part of the `ShapeLabelResolutionContext` so the * `resolveLabel` callback can react to it (e.g. hide, or fall back to a * caller-managed annotation). */ export declare const computeEdgeAutoPlacement: (feature: GeoJsonFeature, label: string, viewportBounds: Readonly, zoom: number, tileSize: number, options: EdgeAutoPlacementOptions) => EdgeAutoPlacementOutcome;