@trackunit/react-map 0.2.125 → 0.2.127
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/index.cjs.js +163 -65
- package/index.esm.js +164 -67
- package/package.json +10 -10
- package/src/controls/renderingRules.d.ts +6 -1
- package/src/index.d.ts +1 -0
- package/src/layers/buildLabelPlacement.d.ts +29 -0
- package/src/layers/internal/labelFootprint.d.ts +37 -0
- package/src/layers/useLabelPlacement.d.ts +31 -0
- package/src/layers/useShapes/ShapeLabelPill.d.ts +22 -3
- package/src/markers/shared/mapMarkerVariants.d.ts +62 -6
- package/src/stories/_storyCoordUtils.d.ts +55 -6
- package/migrations/entry.js.map +0 -1
package/index.cjs.js
CHANGED
|
@@ -1059,17 +1059,18 @@ const FallbackAnnotationContent = ({ store }) => {
|
|
|
1059
1059
|
return (jsxRuntime.jsx("div", { className: "pointer-events-none col-start-1 row-start-1 self-end justify-self-start", children: jsxRuntime.jsx(AnnotationStack, { annotations: displayAnnotations, isExiting: isExiting }) }));
|
|
1060
1060
|
};
|
|
1061
1061
|
const DEBOUNCE_DELAY = 200;
|
|
1062
|
-
const cvaLoadingOverlay = cssClassVarianceUtilities.
|
|
1063
|
-
|
|
1064
|
-
|
|
1065
|
-
|
|
1066
|
-
|
|
1067
|
-
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
|
|
1071
|
-
|
|
1072
|
-
|
|
1062
|
+
const cvaLoadingOverlay = cssClassVarianceUtilities.cva({
|
|
1063
|
+
base: [
|
|
1064
|
+
"pointer-events-none",
|
|
1065
|
+
"col-start-1",
|
|
1066
|
+
"row-start-1",
|
|
1067
|
+
"flex",
|
|
1068
|
+
"items-start",
|
|
1069
|
+
"justify-center",
|
|
1070
|
+
"transition-[opacity,display]",
|
|
1071
|
+
"duration-150",
|
|
1072
|
+
"ease-in-out",
|
|
1073
|
+
],
|
|
1073
1074
|
variants: {
|
|
1074
1075
|
visible: {
|
|
1075
1076
|
true: ["opacity-100"],
|
|
@@ -1900,7 +1901,7 @@ const Custom = ({ config, overlaySide = undefined, resolved, placement }) => {
|
|
|
1900
1901
|
};
|
|
1901
1902
|
|
|
1902
1903
|
/** Shared padding for interactive menu items (button, toggle, stepper, select, radioGroup). */
|
|
1903
|
-
const cvaControlMenuItemBase = cssClassVarianceUtilities.
|
|
1904
|
+
const cvaControlMenuItemBase = cssClassVarianceUtilities.cva({ base: ["px-3", "py-2"] });
|
|
1904
1905
|
// ============================================================================
|
|
1905
1906
|
// Cross-Cutting Middleware
|
|
1906
1907
|
// ============================================================================
|
|
@@ -2697,21 +2698,22 @@ const ControlsInner = ({ api, controls, className }) => {
|
|
|
2697
2698
|
// shallow equality is sufficient; no custom comparator needed.
|
|
2698
2699
|
const Controls = react.memo(ControlsInner);
|
|
2699
2700
|
|
|
2700
|
-
const cvaMapPreview = cssClassVarianceUtilities.
|
|
2701
|
-
|
|
2702
|
-
|
|
2703
|
-
|
|
2704
|
-
|
|
2705
|
-
|
|
2706
|
-
|
|
2707
|
-
|
|
2708
|
-
|
|
2709
|
-
|
|
2710
|
-
|
|
2711
|
-
|
|
2712
|
-
|
|
2713
|
-
|
|
2714
|
-
],
|
|
2701
|
+
const cvaMapPreview = cssClassVarianceUtilities.cva({
|
|
2702
|
+
base: [
|
|
2703
|
+
"relative",
|
|
2704
|
+
"h-[72px]",
|
|
2705
|
+
"w-[72px]",
|
|
2706
|
+
"overflow-clip",
|
|
2707
|
+
"[--radius:theme(borderRadius.xl)]",
|
|
2708
|
+
"[--border:2px]",
|
|
2709
|
+
"rounded-[var(--radius)]",
|
|
2710
|
+
"before:pointer-events-none",
|
|
2711
|
+
"before:absolute",
|
|
2712
|
+
"before:inset-0",
|
|
2713
|
+
"before:z-10",
|
|
2714
|
+
"before:rounded-[var(--radius)]",
|
|
2715
|
+
"before:border-[length:var(--border)]",
|
|
2716
|
+
],
|
|
2715
2717
|
variants: {
|
|
2716
2718
|
selected: {
|
|
2717
2719
|
true: ["before:border-primary-500", "before:ring-2", "before:ring-inset", "before:ring-white"],
|
|
@@ -3980,19 +3982,21 @@ const resolveMarkerDomSize = (size, direction, form) => {
|
|
|
3980
3982
|
* properties that need to animate, intentionally excluding `width` to prevent
|
|
3981
3983
|
* non-composited layout thrashing. See `MarkerAnimatedSurface.tsx` for details.
|
|
3982
3984
|
*/
|
|
3983
|
-
const cvaMapMarker = cssClassVarianceUtilities.
|
|
3984
|
-
|
|
3985
|
-
|
|
3986
|
-
|
|
3987
|
-
|
|
3988
|
-
|
|
3989
|
-
|
|
3990
|
-
|
|
3991
|
-
|
|
3992
|
-
|
|
3993
|
-
|
|
3994
|
-
|
|
3995
|
-
|
|
3985
|
+
const cvaMapMarker = cssClassVarianceUtilities.cva({
|
|
3986
|
+
base: [
|
|
3987
|
+
"inline-flex",
|
|
3988
|
+
"items-center",
|
|
3989
|
+
"relative",
|
|
3990
|
+
"overflow-hidden",
|
|
3991
|
+
"select-none",
|
|
3992
|
+
"cursor-pointer",
|
|
3993
|
+
"rounded-full",
|
|
3994
|
+
"outline-none",
|
|
3995
|
+
"focus-visible:ring-2",
|
|
3996
|
+
"focus-visible:ring-info-500",
|
|
3997
|
+
"focus-visible:ring-offset-2",
|
|
3998
|
+
],
|
|
3999
|
+
});
|
|
3996
4000
|
/**
|
|
3997
4001
|
* Label span inside pill markers.
|
|
3998
4002
|
*
|
|
@@ -4005,19 +4009,14 @@ const cvaMapMarker = cssClassVarianceUtilities.cvaMerge([
|
|
|
4005
4009
|
* alone biases the text upward (extra space below). `overflow-x: clip` gives the
|
|
4006
4010
|
* horizontal ellipsis without a vertical clip on the span itself.
|
|
4007
4011
|
*/
|
|
4008
|
-
const cvaPillLabel = cssClassVarianceUtilities.
|
|
4009
|
-
"min-w-0",
|
|
4010
|
-
|
|
4011
|
-
"overflow-x-clip",
|
|
4012
|
-
"whitespace-nowrap",
|
|
4013
|
-
"text-ellipsis",
|
|
4014
|
-
"font-medium",
|
|
4015
|
-
"leading-none",
|
|
4016
|
-
]);
|
|
4012
|
+
const cvaPillLabel = cssClassVarianceUtilities.cva({
|
|
4013
|
+
base: ["min-w-0", "flex-1", "overflow-x-clip", "whitespace-nowrap", "text-ellipsis", "font-medium", "leading-none"],
|
|
4014
|
+
});
|
|
4017
4015
|
/**
|
|
4018
4016
|
* Inner indicator disc: marker fill via `--marker-color`, border via CSS `color-mix` on that token (plan §5).
|
|
4019
4017
|
*/
|
|
4020
|
-
const cvaMarkerIndicator = cssClassVarianceUtilities.
|
|
4018
|
+
const cvaMarkerIndicator = cssClassVarianceUtilities.cva({
|
|
4019
|
+
base: ["rounded-full", "bg-[var(--marker-color)]", "flex", "items-center", "justify-center"],
|
|
4021
4020
|
variants: {
|
|
4022
4021
|
size: {
|
|
4023
4022
|
xs: ["h-2", "w-2"],
|
|
@@ -5579,6 +5578,102 @@ const useFitToContent = (api, handles, options) => {
|
|
|
5579
5578
|
return react.useMemo(() => ({ hasFitted, isWaiting, fitNow }), [hasFitted, isWaiting, fitNow]);
|
|
5580
5579
|
};
|
|
5581
5580
|
|
|
5581
|
+
const isWithinBounds = (position, bounds) => {
|
|
5582
|
+
const [lng, lat] = position;
|
|
5583
|
+
const [minLng, minLat, maxLng, maxLat] = bounds;
|
|
5584
|
+
const lngInside = minLng > maxLng ? lng >= minLng || lng <= maxLng : lng >= minLng && lng <= maxLng;
|
|
5585
|
+
return lngInside && lat >= minLat && lat <= maxLat;
|
|
5586
|
+
};
|
|
5587
|
+
const overlaps = (a, b, footprint) => {
|
|
5588
|
+
// Shortest angular longitude delta so labels near the antimeridian (e.g. 179.9° and
|
|
5589
|
+
// -179.9°) are correctly seen as ~0.2° apart, not ~359.8°, matching `isWithinBounds`.
|
|
5590
|
+
const rawLngDelta = Math.abs(a[0] - b[0]);
|
|
5591
|
+
const lngDelta = Math.min(rawLngDelta, 360 - rawLngDelta);
|
|
5592
|
+
return lngDelta < footprint.widthDeg && Math.abs(a[1] - b[1]) < footprint.heightDeg;
|
|
5593
|
+
};
|
|
5594
|
+
/**
|
|
5595
|
+
* Chooses which markers render a label so that no two labels overlap.
|
|
5596
|
+
*
|
|
5597
|
+
* Greedy placement: walks `items` in order, skips any outside the view box,
|
|
5598
|
+
* and adds a label only when its nominal footprint does not collide with an
|
|
5599
|
+
* already-placed one, stopping at `ceiling`. The budget is emergent — however
|
|
5600
|
+
* many non-overlapping labels fit, bounded by the ceiling.
|
|
5601
|
+
*
|
|
5602
|
+
* Deterministic: identical input yields an identical set. Phase 1 places in the
|
|
5603
|
+
* caller-provided order; priority ordering, dispersion, and pin-on-a-stick
|
|
5604
|
+
* displacement layer on top in later phases.
|
|
5605
|
+
*/
|
|
5606
|
+
const buildLabelPlacement = ({ items, getId, getPosition, bounds, footprint, ceiling, }) => {
|
|
5607
|
+
const shown = new Set();
|
|
5608
|
+
const placed = [];
|
|
5609
|
+
for (const item of items) {
|
|
5610
|
+
if (shown.size >= ceiling)
|
|
5611
|
+
break;
|
|
5612
|
+
const position = getPosition(item);
|
|
5613
|
+
if (bounds !== null && !isWithinBounds(position, bounds))
|
|
5614
|
+
continue;
|
|
5615
|
+
if (placed.some(other => overlaps(other, position, footprint)))
|
|
5616
|
+
continue;
|
|
5617
|
+
shown.add(getId(item));
|
|
5618
|
+
placed.push(position);
|
|
5619
|
+
}
|
|
5620
|
+
return shown;
|
|
5621
|
+
};
|
|
5622
|
+
|
|
5623
|
+
const WEB_MERCATOR_TILE_SIZE_PX = 256;
|
|
5624
|
+
/**
|
|
5625
|
+
* Degrees of longitude/latitude spanned by a single screen pixel at `zoom`,
|
|
5626
|
+
* near `latitudeDeg`, under Web Mercator.
|
|
5627
|
+
*
|
|
5628
|
+
* Longitude degrees-per-pixel is constant everywhere; latitude degrees-per-pixel
|
|
5629
|
+
* scales by `cos(latitude)` because Mercator stretches higher latitudes. Used to
|
|
5630
|
+
* convert a pixel label footprint into a geo bounding box so collision runs in
|
|
5631
|
+
* the same lng/lat space the marker positions already live in.
|
|
5632
|
+
*/
|
|
5633
|
+
const degreesPerPixel$1 = (zoom, latitudeDeg) => {
|
|
5634
|
+
const worldWidthPx = WEB_MERCATOR_TILE_SIZE_PX * 2 ** zoom;
|
|
5635
|
+
const lng = 360 / worldWidthPx;
|
|
5636
|
+
return { lng, lat: lng * Math.cos((latitudeDeg * Math.PI) / 180) };
|
|
5637
|
+
};
|
|
5638
|
+
/**
|
|
5639
|
+
* Upper-bound label footprint as a geo-space width/height, for collision testing.
|
|
5640
|
+
*
|
|
5641
|
+
* A rendered pill's size does NOT follow the zoom circle-size tier: `MapMarker`
|
|
5642
|
+
* lays every pill out at the fixed {@link MARKER_PILL_CONTENT_LAYOUT_SIZE}, and the
|
|
5643
|
+
* label text is capped at `maxLabelWidthPx` (ellipsis beyond). We size the collision
|
|
5644
|
+
* box to that upper bound — widest pill that can render — so the box is never smaller
|
|
5645
|
+
* than what paints, which is what keeps the "no two labels overlap" guarantee honest.
|
|
5646
|
+
* Density is tuned via `paddingPx`, not by shrinking the box.
|
|
5647
|
+
*/
|
|
5648
|
+
const labelFootprintDeg = ({ zoom, latitudeDeg, paddingPx }) => {
|
|
5649
|
+
const { circle, pillPaddingY } = MARKER_SIZE_MAP[MARKER_PILL_CONTENT_LAYOUT_SIZE];
|
|
5650
|
+
const { maxLabelWidthPx, gapPx, paddingRightPx } = MARKER_TUNING.pill;
|
|
5651
|
+
// Left pad (== pillPaddingY) + icon disc + gap + max label text + right pad. Assume the icon
|
|
5652
|
+
// disc is present (the wider case) so the box stays an upper bound.
|
|
5653
|
+
const widthPx = pillPaddingY + circle + gapPx + maxLabelWidthPx + paddingRightPx + 2 * paddingPx;
|
|
5654
|
+
const heightPx = markerPillOuterHeightPx(MARKER_PILL_CONTENT_LAYOUT_SIZE) + 2 * paddingPx;
|
|
5655
|
+
const perPixel = degreesPerPixel$1(zoom, latitudeDeg);
|
|
5656
|
+
return { widthDeg: widthPx * perPixel.lng, heightDeg: heightPx * perPixel.lat };
|
|
5657
|
+
};
|
|
5658
|
+
|
|
5659
|
+
const EMPTY_LABEL_IDS = new Set();
|
|
5660
|
+
/**
|
|
5661
|
+
* Viewport-driven label placement: returns the ids of the markers that should
|
|
5662
|
+
* render a label such that no two labels overlap, bounded by `ceiling`.
|
|
5663
|
+
*
|
|
5664
|
+
* Derives an upper-bound pill footprint (`labelFootprintDeg`) and delegates
|
|
5665
|
+
* selection to the pure `buildLabelPlacement`. Exposed as a hook so the placement
|
|
5666
|
+
* participates in React's stability model and later phases can hold incumbency
|
|
5667
|
+
* state here without changing the public surface.
|
|
5668
|
+
*/
|
|
5669
|
+
const useLabelPlacement = ({ enabled, items, getId, getPosition, bounds, zoom, paddingPx, ceiling, }) => react.useMemo(() => {
|
|
5670
|
+
if (!enabled)
|
|
5671
|
+
return EMPTY_LABEL_IDS;
|
|
5672
|
+
const latitudeDeg = bounds === null ? 0 : (bounds[1] + bounds[3]) / 2;
|
|
5673
|
+
const footprint = labelFootprintDeg({ zoom, latitudeDeg, paddingPx });
|
|
5674
|
+
return buildLabelPlacement({ items, getId, getPosition, bounds, footprint, ceiling });
|
|
5675
|
+
}, [enabled, items, getId, getPosition, bounds, zoom, paddingPx, ceiling]);
|
|
5676
|
+
|
|
5582
5677
|
// ============================================================================
|
|
5583
5678
|
// Helpers
|
|
5584
5679
|
// ============================================================================
|
|
@@ -6403,20 +6498,21 @@ const extractSinglePartInfo = (geometry) => {
|
|
|
6403
6498
|
}
|
|
6404
6499
|
};
|
|
6405
6500
|
|
|
6406
|
-
const cvaShapeLabel = cssClassVarianceUtilities.
|
|
6407
|
-
|
|
6408
|
-
|
|
6409
|
-
|
|
6410
|
-
|
|
6411
|
-
|
|
6412
|
-
|
|
6413
|
-
|
|
6414
|
-
|
|
6415
|
-
|
|
6416
|
-
|
|
6417
|
-
|
|
6418
|
-
|
|
6419
|
-
|
|
6501
|
+
const cvaShapeLabel = cssClassVarianceUtilities.cva({
|
|
6502
|
+
base: [
|
|
6503
|
+
"select-none",
|
|
6504
|
+
"inline-flex",
|
|
6505
|
+
"items-center",
|
|
6506
|
+
"gap-1",
|
|
6507
|
+
"whitespace-nowrap",
|
|
6508
|
+
"px-2",
|
|
6509
|
+
"py-0.5",
|
|
6510
|
+
"text-xs",
|
|
6511
|
+
"font-medium",
|
|
6512
|
+
"leading-tight",
|
|
6513
|
+
"transition-opacity",
|
|
6514
|
+
"duration-150",
|
|
6515
|
+
],
|
|
6420
6516
|
variants: {
|
|
6421
6517
|
theme: {
|
|
6422
6518
|
light: ["shadow-sm"],
|
|
@@ -6473,7 +6569,8 @@ const EDGE_LABEL_BORDER_RADIUS = {
|
|
|
6473
6569
|
right: "3px 0 3px 3px",
|
|
6474
6570
|
},
|
|
6475
6571
|
};
|
|
6476
|
-
const cvaFitMode = cssClassVarianceUtilities.
|
|
6572
|
+
const cvaFitMode = cssClassVarianceUtilities.cva({
|
|
6573
|
+
base: [],
|
|
6477
6574
|
variants: {
|
|
6478
6575
|
fitMode: {
|
|
6479
6576
|
fits: ["pointer-events-auto"],
|
|
@@ -12208,6 +12305,7 @@ exports.useExpandedIds = useExpandedIds;
|
|
|
12208
12305
|
exports.useFitFeatureBounds = useFitFeatureBounds;
|
|
12209
12306
|
exports.useFitToContent = useFitToContent;
|
|
12210
12307
|
exports.useImageOverlay = useImageOverlay;
|
|
12308
|
+
exports.useLabelPlacement = useLabelPlacement;
|
|
12211
12309
|
exports.useLayers = useLayers;
|
|
12212
12310
|
exports.useMap = useMap;
|
|
12213
12311
|
exports.useMapAnnotation = useMapAnnotation;
|
package/index.esm.js
CHANGED
|
@@ -3,7 +3,7 @@ import { useNamespaceTranslation, registerTranslations } from '@trackunit/i18n-l
|
|
|
3
3
|
import { INITIAL_CAMERA_STATE, INITIAL_MAP_STATUS, KEYBOARD_PAN_AMOUNT, KEYBOARD_ZOOM_AMOUNT, mapAppearanceSchema, DEFAULT_MAP_APPEARANCE, WORLD_BBOX, INITIAL_INTERACTION_STATE, mapThemeSchema, mergeAntimeridianFeatures, extractSourceData, computeMarkerDomPortalZIndex, densifyGeodesicFeatures, angularDistance, intermediatePoint, GEODESIC_MAX_SEGMENT_KM, geometryTypeToShapeType, resolveStrokeColors } from '@trackunit/react-map-adapter-shared';
|
|
4
4
|
export { ANCHOR_SELECTOR, DEFAULT_MAP_APPEARANCE, HIT_SURFACE_SELECTOR, INITIAL_CAMERA_STATE, INITIAL_INTERACTION_STATE, INITIAL_MAP_STATE, INITIAL_MAP_STATUS, computeMarkerDomPortalZIndex, defineAdapter, geometryTypeToShapeType, mapAppearanceSchema, mapThemeSchema, mapTypeSchema } from '@trackunit/react-map-adapter-shared';
|
|
5
5
|
import { useSyncExternalStore, useMemo, useState, useRef, useCallback, useLayoutEffect, useEffect, useReducer, createContext, useContext, memo, Suspense, useId, Fragment as Fragment$1, forwardRef } from 'react';
|
|
6
|
-
import {
|
|
6
|
+
import { cva } from '@trackunit/css-class-variance-utilities';
|
|
7
7
|
import { Tooltip, useMergeRefs, Icon, Spinner, Text, useMeasure, useContainerBreakpoints, useDebounce, useWatch, useKeyboardShortcut, IconButton, ToggleGroup as ToggleGroup$1, useIsFullscreen, Popover, PopoverTrigger, PopoverContent, MoreMenu, useHover as useHover$1, cvaInteractableItem, useLocalStorageReducer } from '@trackunit/react-components';
|
|
8
8
|
import { twMerge } from 'tailwind-merge';
|
|
9
9
|
import { useFloating, autoUpdate, offset, flip, useHover, safePolygon, useInteractions, shift, hide, useDismiss, useRole } from '@floating-ui/react';
|
|
@@ -1058,17 +1058,18 @@ const FallbackAnnotationContent = ({ store }) => {
|
|
|
1058
1058
|
return (jsx("div", { className: "pointer-events-none col-start-1 row-start-1 self-end justify-self-start", children: jsx(AnnotationStack, { annotations: displayAnnotations, isExiting: isExiting }) }));
|
|
1059
1059
|
};
|
|
1060
1060
|
const DEBOUNCE_DELAY = 200;
|
|
1061
|
-
const cvaLoadingOverlay =
|
|
1062
|
-
|
|
1063
|
-
|
|
1064
|
-
|
|
1065
|
-
|
|
1066
|
-
|
|
1067
|
-
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
|
|
1071
|
-
|
|
1061
|
+
const cvaLoadingOverlay = cva({
|
|
1062
|
+
base: [
|
|
1063
|
+
"pointer-events-none",
|
|
1064
|
+
"col-start-1",
|
|
1065
|
+
"row-start-1",
|
|
1066
|
+
"flex",
|
|
1067
|
+
"items-start",
|
|
1068
|
+
"justify-center",
|
|
1069
|
+
"transition-[opacity,display]",
|
|
1070
|
+
"duration-150",
|
|
1071
|
+
"ease-in-out",
|
|
1072
|
+
],
|
|
1072
1073
|
variants: {
|
|
1073
1074
|
visible: {
|
|
1074
1075
|
true: ["opacity-100"],
|
|
@@ -1899,7 +1900,7 @@ const Custom = ({ config, overlaySide = undefined, resolved, placement }) => {
|
|
|
1899
1900
|
};
|
|
1900
1901
|
|
|
1901
1902
|
/** Shared padding for interactive menu items (button, toggle, stepper, select, radioGroup). */
|
|
1902
|
-
const cvaControlMenuItemBase =
|
|
1903
|
+
const cvaControlMenuItemBase = cva({ base: ["px-3", "py-2"] });
|
|
1903
1904
|
// ============================================================================
|
|
1904
1905
|
// Cross-Cutting Middleware
|
|
1905
1906
|
// ============================================================================
|
|
@@ -2696,21 +2697,22 @@ const ControlsInner = ({ api, controls, className }) => {
|
|
|
2696
2697
|
// shallow equality is sufficient; no custom comparator needed.
|
|
2697
2698
|
const Controls = memo(ControlsInner);
|
|
2698
2699
|
|
|
2699
|
-
const cvaMapPreview =
|
|
2700
|
-
|
|
2701
|
-
|
|
2702
|
-
|
|
2703
|
-
|
|
2704
|
-
|
|
2705
|
-
|
|
2706
|
-
|
|
2707
|
-
|
|
2708
|
-
|
|
2709
|
-
|
|
2710
|
-
|
|
2711
|
-
|
|
2712
|
-
|
|
2713
|
-
],
|
|
2700
|
+
const cvaMapPreview = cva({
|
|
2701
|
+
base: [
|
|
2702
|
+
"relative",
|
|
2703
|
+
"h-[72px]",
|
|
2704
|
+
"w-[72px]",
|
|
2705
|
+
"overflow-clip",
|
|
2706
|
+
"[--radius:theme(borderRadius.xl)]",
|
|
2707
|
+
"[--border:2px]",
|
|
2708
|
+
"rounded-[var(--radius)]",
|
|
2709
|
+
"before:pointer-events-none",
|
|
2710
|
+
"before:absolute",
|
|
2711
|
+
"before:inset-0",
|
|
2712
|
+
"before:z-10",
|
|
2713
|
+
"before:rounded-[var(--radius)]",
|
|
2714
|
+
"before:border-[length:var(--border)]",
|
|
2715
|
+
],
|
|
2714
2716
|
variants: {
|
|
2715
2717
|
selected: {
|
|
2716
2718
|
true: ["before:border-primary-500", "before:ring-2", "before:ring-inset", "before:ring-white"],
|
|
@@ -3979,19 +3981,21 @@ const resolveMarkerDomSize = (size, direction, form) => {
|
|
|
3979
3981
|
* properties that need to animate, intentionally excluding `width` to prevent
|
|
3980
3982
|
* non-composited layout thrashing. See `MarkerAnimatedSurface.tsx` for details.
|
|
3981
3983
|
*/
|
|
3982
|
-
const cvaMapMarker =
|
|
3983
|
-
|
|
3984
|
-
|
|
3985
|
-
|
|
3986
|
-
|
|
3987
|
-
|
|
3988
|
-
|
|
3989
|
-
|
|
3990
|
-
|
|
3991
|
-
|
|
3992
|
-
|
|
3993
|
-
|
|
3994
|
-
|
|
3984
|
+
const cvaMapMarker = cva({
|
|
3985
|
+
base: [
|
|
3986
|
+
"inline-flex",
|
|
3987
|
+
"items-center",
|
|
3988
|
+
"relative",
|
|
3989
|
+
"overflow-hidden",
|
|
3990
|
+
"select-none",
|
|
3991
|
+
"cursor-pointer",
|
|
3992
|
+
"rounded-full",
|
|
3993
|
+
"outline-none",
|
|
3994
|
+
"focus-visible:ring-2",
|
|
3995
|
+
"focus-visible:ring-info-500",
|
|
3996
|
+
"focus-visible:ring-offset-2",
|
|
3997
|
+
],
|
|
3998
|
+
});
|
|
3995
3999
|
/**
|
|
3996
4000
|
* Label span inside pill markers.
|
|
3997
4001
|
*
|
|
@@ -4004,19 +4008,14 @@ const cvaMapMarker = cvaMerge([
|
|
|
4004
4008
|
* alone biases the text upward (extra space below). `overflow-x: clip` gives the
|
|
4005
4009
|
* horizontal ellipsis without a vertical clip on the span itself.
|
|
4006
4010
|
*/
|
|
4007
|
-
const cvaPillLabel =
|
|
4008
|
-
"min-w-0",
|
|
4009
|
-
|
|
4010
|
-
"overflow-x-clip",
|
|
4011
|
-
"whitespace-nowrap",
|
|
4012
|
-
"text-ellipsis",
|
|
4013
|
-
"font-medium",
|
|
4014
|
-
"leading-none",
|
|
4015
|
-
]);
|
|
4011
|
+
const cvaPillLabel = cva({
|
|
4012
|
+
base: ["min-w-0", "flex-1", "overflow-x-clip", "whitespace-nowrap", "text-ellipsis", "font-medium", "leading-none"],
|
|
4013
|
+
});
|
|
4016
4014
|
/**
|
|
4017
4015
|
* Inner indicator disc: marker fill via `--marker-color`, border via CSS `color-mix` on that token (plan §5).
|
|
4018
4016
|
*/
|
|
4019
|
-
const cvaMarkerIndicator =
|
|
4017
|
+
const cvaMarkerIndicator = cva({
|
|
4018
|
+
base: ["rounded-full", "bg-[var(--marker-color)]", "flex", "items-center", "justify-center"],
|
|
4020
4019
|
variants: {
|
|
4021
4020
|
size: {
|
|
4022
4021
|
xs: ["h-2", "w-2"],
|
|
@@ -5578,6 +5577,102 @@ const useFitToContent = (api, handles, options) => {
|
|
|
5578
5577
|
return useMemo(() => ({ hasFitted, isWaiting, fitNow }), [hasFitted, isWaiting, fitNow]);
|
|
5579
5578
|
};
|
|
5580
5579
|
|
|
5580
|
+
const isWithinBounds = (position, bounds) => {
|
|
5581
|
+
const [lng, lat] = position;
|
|
5582
|
+
const [minLng, minLat, maxLng, maxLat] = bounds;
|
|
5583
|
+
const lngInside = minLng > maxLng ? lng >= minLng || lng <= maxLng : lng >= minLng && lng <= maxLng;
|
|
5584
|
+
return lngInside && lat >= minLat && lat <= maxLat;
|
|
5585
|
+
};
|
|
5586
|
+
const overlaps = (a, b, footprint) => {
|
|
5587
|
+
// Shortest angular longitude delta so labels near the antimeridian (e.g. 179.9° and
|
|
5588
|
+
// -179.9°) are correctly seen as ~0.2° apart, not ~359.8°, matching `isWithinBounds`.
|
|
5589
|
+
const rawLngDelta = Math.abs(a[0] - b[0]);
|
|
5590
|
+
const lngDelta = Math.min(rawLngDelta, 360 - rawLngDelta);
|
|
5591
|
+
return lngDelta < footprint.widthDeg && Math.abs(a[1] - b[1]) < footprint.heightDeg;
|
|
5592
|
+
};
|
|
5593
|
+
/**
|
|
5594
|
+
* Chooses which markers render a label so that no two labels overlap.
|
|
5595
|
+
*
|
|
5596
|
+
* Greedy placement: walks `items` in order, skips any outside the view box,
|
|
5597
|
+
* and adds a label only when its nominal footprint does not collide with an
|
|
5598
|
+
* already-placed one, stopping at `ceiling`. The budget is emergent — however
|
|
5599
|
+
* many non-overlapping labels fit, bounded by the ceiling.
|
|
5600
|
+
*
|
|
5601
|
+
* Deterministic: identical input yields an identical set. Phase 1 places in the
|
|
5602
|
+
* caller-provided order; priority ordering, dispersion, and pin-on-a-stick
|
|
5603
|
+
* displacement layer on top in later phases.
|
|
5604
|
+
*/
|
|
5605
|
+
const buildLabelPlacement = ({ items, getId, getPosition, bounds, footprint, ceiling, }) => {
|
|
5606
|
+
const shown = new Set();
|
|
5607
|
+
const placed = [];
|
|
5608
|
+
for (const item of items) {
|
|
5609
|
+
if (shown.size >= ceiling)
|
|
5610
|
+
break;
|
|
5611
|
+
const position = getPosition(item);
|
|
5612
|
+
if (bounds !== null && !isWithinBounds(position, bounds))
|
|
5613
|
+
continue;
|
|
5614
|
+
if (placed.some(other => overlaps(other, position, footprint)))
|
|
5615
|
+
continue;
|
|
5616
|
+
shown.add(getId(item));
|
|
5617
|
+
placed.push(position);
|
|
5618
|
+
}
|
|
5619
|
+
return shown;
|
|
5620
|
+
};
|
|
5621
|
+
|
|
5622
|
+
const WEB_MERCATOR_TILE_SIZE_PX = 256;
|
|
5623
|
+
/**
|
|
5624
|
+
* Degrees of longitude/latitude spanned by a single screen pixel at `zoom`,
|
|
5625
|
+
* near `latitudeDeg`, under Web Mercator.
|
|
5626
|
+
*
|
|
5627
|
+
* Longitude degrees-per-pixel is constant everywhere; latitude degrees-per-pixel
|
|
5628
|
+
* scales by `cos(latitude)` because Mercator stretches higher latitudes. Used to
|
|
5629
|
+
* convert a pixel label footprint into a geo bounding box so collision runs in
|
|
5630
|
+
* the same lng/lat space the marker positions already live in.
|
|
5631
|
+
*/
|
|
5632
|
+
const degreesPerPixel$1 = (zoom, latitudeDeg) => {
|
|
5633
|
+
const worldWidthPx = WEB_MERCATOR_TILE_SIZE_PX * 2 ** zoom;
|
|
5634
|
+
const lng = 360 / worldWidthPx;
|
|
5635
|
+
return { lng, lat: lng * Math.cos((latitudeDeg * Math.PI) / 180) };
|
|
5636
|
+
};
|
|
5637
|
+
/**
|
|
5638
|
+
* Upper-bound label footprint as a geo-space width/height, for collision testing.
|
|
5639
|
+
*
|
|
5640
|
+
* A rendered pill's size does NOT follow the zoom circle-size tier: `MapMarker`
|
|
5641
|
+
* lays every pill out at the fixed {@link MARKER_PILL_CONTENT_LAYOUT_SIZE}, and the
|
|
5642
|
+
* label text is capped at `maxLabelWidthPx` (ellipsis beyond). We size the collision
|
|
5643
|
+
* box to that upper bound — widest pill that can render — so the box is never smaller
|
|
5644
|
+
* than what paints, which is what keeps the "no two labels overlap" guarantee honest.
|
|
5645
|
+
* Density is tuned via `paddingPx`, not by shrinking the box.
|
|
5646
|
+
*/
|
|
5647
|
+
const labelFootprintDeg = ({ zoom, latitudeDeg, paddingPx }) => {
|
|
5648
|
+
const { circle, pillPaddingY } = MARKER_SIZE_MAP[MARKER_PILL_CONTENT_LAYOUT_SIZE];
|
|
5649
|
+
const { maxLabelWidthPx, gapPx, paddingRightPx } = MARKER_TUNING.pill;
|
|
5650
|
+
// Left pad (== pillPaddingY) + icon disc + gap + max label text + right pad. Assume the icon
|
|
5651
|
+
// disc is present (the wider case) so the box stays an upper bound.
|
|
5652
|
+
const widthPx = pillPaddingY + circle + gapPx + maxLabelWidthPx + paddingRightPx + 2 * paddingPx;
|
|
5653
|
+
const heightPx = markerPillOuterHeightPx(MARKER_PILL_CONTENT_LAYOUT_SIZE) + 2 * paddingPx;
|
|
5654
|
+
const perPixel = degreesPerPixel$1(zoom, latitudeDeg);
|
|
5655
|
+
return { widthDeg: widthPx * perPixel.lng, heightDeg: heightPx * perPixel.lat };
|
|
5656
|
+
};
|
|
5657
|
+
|
|
5658
|
+
const EMPTY_LABEL_IDS = new Set();
|
|
5659
|
+
/**
|
|
5660
|
+
* Viewport-driven label placement: returns the ids of the markers that should
|
|
5661
|
+
* render a label such that no two labels overlap, bounded by `ceiling`.
|
|
5662
|
+
*
|
|
5663
|
+
* Derives an upper-bound pill footprint (`labelFootprintDeg`) and delegates
|
|
5664
|
+
* selection to the pure `buildLabelPlacement`. Exposed as a hook so the placement
|
|
5665
|
+
* participates in React's stability model and later phases can hold incumbency
|
|
5666
|
+
* state here without changing the public surface.
|
|
5667
|
+
*/
|
|
5668
|
+
const useLabelPlacement = ({ enabled, items, getId, getPosition, bounds, zoom, paddingPx, ceiling, }) => useMemo(() => {
|
|
5669
|
+
if (!enabled)
|
|
5670
|
+
return EMPTY_LABEL_IDS;
|
|
5671
|
+
const latitudeDeg = bounds === null ? 0 : (bounds[1] + bounds[3]) / 2;
|
|
5672
|
+
const footprint = labelFootprintDeg({ zoom, latitudeDeg, paddingPx });
|
|
5673
|
+
return buildLabelPlacement({ items, getId, getPosition, bounds, footprint, ceiling });
|
|
5674
|
+
}, [enabled, items, getId, getPosition, bounds, zoom, paddingPx, ceiling]);
|
|
5675
|
+
|
|
5581
5676
|
// ============================================================================
|
|
5582
5677
|
// Helpers
|
|
5583
5678
|
// ============================================================================
|
|
@@ -6402,20 +6497,21 @@ const extractSinglePartInfo = (geometry) => {
|
|
|
6402
6497
|
}
|
|
6403
6498
|
};
|
|
6404
6499
|
|
|
6405
|
-
const cvaShapeLabel =
|
|
6406
|
-
|
|
6407
|
-
|
|
6408
|
-
|
|
6409
|
-
|
|
6410
|
-
|
|
6411
|
-
|
|
6412
|
-
|
|
6413
|
-
|
|
6414
|
-
|
|
6415
|
-
|
|
6416
|
-
|
|
6417
|
-
|
|
6418
|
-
|
|
6500
|
+
const cvaShapeLabel = cva({
|
|
6501
|
+
base: [
|
|
6502
|
+
"select-none",
|
|
6503
|
+
"inline-flex",
|
|
6504
|
+
"items-center",
|
|
6505
|
+
"gap-1",
|
|
6506
|
+
"whitespace-nowrap",
|
|
6507
|
+
"px-2",
|
|
6508
|
+
"py-0.5",
|
|
6509
|
+
"text-xs",
|
|
6510
|
+
"font-medium",
|
|
6511
|
+
"leading-tight",
|
|
6512
|
+
"transition-opacity",
|
|
6513
|
+
"duration-150",
|
|
6514
|
+
],
|
|
6419
6515
|
variants: {
|
|
6420
6516
|
theme: {
|
|
6421
6517
|
light: ["shadow-sm"],
|
|
@@ -6472,7 +6568,8 @@ const EDGE_LABEL_BORDER_RADIUS = {
|
|
|
6472
6568
|
right: "3px 0 3px 3px",
|
|
6473
6569
|
},
|
|
6474
6570
|
};
|
|
6475
|
-
const cvaFitMode =
|
|
6571
|
+
const cvaFitMode = cva({
|
|
6572
|
+
base: [],
|
|
6476
6573
|
variants: {
|
|
6477
6574
|
fitMode: {
|
|
6478
6575
|
fits: ["pointer-events-auto"],
|
|
@@ -12120,4 +12217,4 @@ const mockMapApi = (overrides) => {
|
|
|
12120
12217
|
*/
|
|
12121
12218
|
setupLibraryTranslations();
|
|
12122
12219
|
|
|
12123
|
-
export { ClusterMarker, ClusterStick, Controls, DEFAULT_MARKER_SIZE_BREAKPOINTS, DefaultControls, Layers, MARKER_DARK_PILL, MARKER_DISC_BORDER_WIDTH_PX, MARKER_LIGHT_PILL, MARKER_PILL_CONTENT_LAYOUT_SIZE, MARKER_SIZE_MAP, MARKER_TUNING, MapLoadingState, MapMarker, MapMarkerIcon, PanelIconButton, ShapeAnnotationLabel, buildExpandedIds, cvaMapMarker, cvaMarkerIndicator, mockMapApi, useAdaptiveMarkerHelpers, useAutoPanResolver, useCameraIdle, useCameraState, useClusterCountFormat, useControlStack, useControls, useDefaultControls, useDirectionIndicator, useEntitiesNearCursor, useExpandedIds, useFitFeatureBounds, useFitToContent, useImageOverlay, useLayers, useMap, useMapAnnotation, useMapAnnotations, useMapAppearanceControls, useMapKeyboardNavigation, useMarkerColors, useMarkerStateResolvers, useMarkers, usePanel, usePanelPreload, usePreviewMap, useRoute, useShapeLabelHelpers, useShapes, useViewportContext };
|
|
12220
|
+
export { ClusterMarker, ClusterStick, Controls, DEFAULT_MARKER_SIZE_BREAKPOINTS, DefaultControls, Layers, MARKER_DARK_PILL, MARKER_DISC_BORDER_WIDTH_PX, MARKER_LIGHT_PILL, MARKER_PILL_CONTENT_LAYOUT_SIZE, MARKER_SIZE_MAP, MARKER_TUNING, MapLoadingState, MapMarker, MapMarkerIcon, PanelIconButton, ShapeAnnotationLabel, buildExpandedIds, cvaMapMarker, cvaMarkerIndicator, mockMapApi, useAdaptiveMarkerHelpers, useAutoPanResolver, useCameraIdle, useCameraState, useClusterCountFormat, useControlStack, useControls, useDefaultControls, useDirectionIndicator, useEntitiesNearCursor, useExpandedIds, useFitFeatureBounds, useFitToContent, useImageOverlay, useLabelPlacement, useLayers, useMap, useMapAnnotation, useMapAnnotations, useMapAppearanceControls, useMapKeyboardNavigation, useMarkerColors, useMarkerStateResolvers, useMarkers, usePanel, usePanelPreload, usePreviewMap, useRoute, useShapeLabelHelpers, useShapes, useViewportContext };
|
package/package.json
CHANGED
|
@@ -1,22 +1,22 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@trackunit/react-map",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.127",
|
|
4
4
|
"repository": "https://github.com/Trackunit/manager",
|
|
5
5
|
"license": "SEE LICENSE IN LICENSE.txt",
|
|
6
6
|
"engines": {
|
|
7
7
|
"node": ">=24.x"
|
|
8
8
|
},
|
|
9
9
|
"dependencies": {
|
|
10
|
-
"@trackunit/react-components": "2.
|
|
11
|
-
"@trackunit/css-class-variance-utilities": "
|
|
12
|
-
"@trackunit/react-form-components": "2.6.
|
|
13
|
-
"@trackunit/react-core-hooks": "1.21.
|
|
14
|
-
"@trackunit/geo-json-utils": "1.15.
|
|
15
|
-
"@trackunit/i18n-library-translation": "2.4.
|
|
10
|
+
"@trackunit/react-components": "2.13.1",
|
|
11
|
+
"@trackunit/css-class-variance-utilities": "2.0.0",
|
|
12
|
+
"@trackunit/react-form-components": "2.6.49",
|
|
13
|
+
"@trackunit/react-core-hooks": "1.21.42",
|
|
14
|
+
"@trackunit/geo-json-utils": "1.15.32",
|
|
15
|
+
"@trackunit/i18n-library-translation": "2.4.42",
|
|
16
16
|
"react-minimal-pie-chart": "^8.4.0",
|
|
17
|
-
"@trackunit/react-map-adapter-shared": "0.0.
|
|
18
|
-
"@trackunit/react-map-color-utils": "0.0.
|
|
19
|
-
"@trackunit/ui-design-tokens": "1.15.
|
|
17
|
+
"@trackunit/react-map-adapter-shared": "0.0.113",
|
|
18
|
+
"@trackunit/react-map-color-utils": "0.0.93",
|
|
19
|
+
"@trackunit/ui-design-tokens": "1.15.20",
|
|
20
20
|
"@floating-ui/react": "^0.26.25",
|
|
21
21
|
"es-toolkit": "^1.39.10",
|
|
22
22
|
"tailwind-merge": "^2.0.0",
|
|
@@ -1,7 +1,12 @@
|
|
|
1
1
|
import type { AvailableSpace, Compactness, ControlConfig, MenuItemConfig, Placement, RenderContext, ResolvedControlProps, ResolvedControlPropsBase } from "./types";
|
|
2
2
|
export type { AvailableSpace, Compactness, Placement, RenderContext, ResolvedControlProps, ResolvedControlPropsBase };
|
|
3
3
|
/** Shared padding for interactive menu items (button, toggle, stepper, select, radioGroup). */
|
|
4
|
-
export declare const cvaControlMenuItemBase:
|
|
4
|
+
export declare const cvaControlMenuItemBase: import("cva").CVAComponent<Omit<{
|
|
5
|
+
base: string[];
|
|
6
|
+
}, "defaultVariants"> & {
|
|
7
|
+
variants: unknown;
|
|
8
|
+
defaultVariants: Omit<{}, never>;
|
|
9
|
+
}, unknown>;
|
|
5
10
|
/** Context available to all middleware and resolvers */
|
|
6
11
|
export type MiddlewareContext = Readonly<{
|
|
7
12
|
renderContext: RenderContext;
|
package/src/index.d.ts
CHANGED
|
@@ -53,6 +53,7 @@ export { useImageOverlay, type UseImageOverlayOptions, type UseImageOverlayRetur
|
|
|
53
53
|
export { buildExpandedIds, useExpandedIds, type MapFocus, type MapFocusTier, type MapFocusTierDisplay, } from "./layers/mapFocus";
|
|
54
54
|
export { useRoute, type UseRouteOptions, type UseRouteReturn } from "./layers/routes/useRoute";
|
|
55
55
|
export { useFitToContent, type FitToContentOptions, type FitToContentResult } from "./layers/useFitToContent";
|
|
56
|
+
export { useLabelPlacement, type UseLabelPlacementParams } from "./layers/useLabelPlacement";
|
|
56
57
|
export { useLayers, type UseLayersReturn } from "./layers/useLayers";
|
|
57
58
|
export { DEFAULT_MARKER_SIZE_BREAKPOINTS, MARKER_DISC_BORDER_WIDTH_PX, type MarkerSizeBreakpoint, type PickMarkerSizeOptions, type PickRenderMediumOptions, type PickSymbolDiameterOptions, } from "./layers/useMarkers/adaptiveHelpers";
|
|
58
59
|
export { useMarkers, type UseMarkersOptions, type UseMarkersReturn } from "./layers/useMarkers/useMarkers";
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import type { GeoJsonBbox } from "@trackunit/geo-json-utils";
|
|
2
|
+
import type { LabelFootprintDeg } from "./internal/labelFootprint";
|
|
3
|
+
type Position = readonly [number, number, ...Array<number>];
|
|
4
|
+
export type BuildLabelPlacementParams<TAsset> = Readonly<{
|
|
5
|
+
/** Candidate markers, in the order they should be considered for a label. */
|
|
6
|
+
items: ReadonlyArray<TAsset>;
|
|
7
|
+
getId: (item: TAsset) => string;
|
|
8
|
+
getPosition: (item: TAsset) => Position;
|
|
9
|
+
/** Current camera view box; candidates outside it are skipped. `null` disables clipping. */
|
|
10
|
+
bounds: Readonly<GeoJsonBbox> | null;
|
|
11
|
+
/** Nominal geo footprint every label is assumed to occupy, for collision testing. */
|
|
12
|
+
footprint: LabelFootprintDeg;
|
|
13
|
+
/** Maximum number of labels to place (DOM-node ceiling). */
|
|
14
|
+
ceiling: number;
|
|
15
|
+
}>;
|
|
16
|
+
/**
|
|
17
|
+
* Chooses which markers render a label so that no two labels overlap.
|
|
18
|
+
*
|
|
19
|
+
* Greedy placement: walks `items` in order, skips any outside the view box,
|
|
20
|
+
* and adds a label only when its nominal footprint does not collide with an
|
|
21
|
+
* already-placed one, stopping at `ceiling`. The budget is emergent — however
|
|
22
|
+
* many non-overlapping labels fit, bounded by the ceiling.
|
|
23
|
+
*
|
|
24
|
+
* Deterministic: identical input yields an identical set. Phase 1 places in the
|
|
25
|
+
* caller-provided order; priority ordering, dispersion, and pin-on-a-stick
|
|
26
|
+
* displacement layer on top in later phases.
|
|
27
|
+
*/
|
|
28
|
+
export declare const buildLabelPlacement: <TAsset>({ items, getId, getPosition, bounds, footprint, ceiling, }: BuildLabelPlacementParams<TAsset>) => ReadonlySet<string>;
|
|
29
|
+
export {};
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
export type DegreesPerPixel = Readonly<{
|
|
2
|
+
lng: number;
|
|
3
|
+
lat: number;
|
|
4
|
+
}>;
|
|
5
|
+
/**
|
|
6
|
+
* Degrees of longitude/latitude spanned by a single screen pixel at `zoom`,
|
|
7
|
+
* near `latitudeDeg`, under Web Mercator.
|
|
8
|
+
*
|
|
9
|
+
* Longitude degrees-per-pixel is constant everywhere; latitude degrees-per-pixel
|
|
10
|
+
* scales by `cos(latitude)` because Mercator stretches higher latitudes. Used to
|
|
11
|
+
* convert a pixel label footprint into a geo bounding box so collision runs in
|
|
12
|
+
* the same lng/lat space the marker positions already live in.
|
|
13
|
+
*/
|
|
14
|
+
export declare const degreesPerPixel: (zoom: number, latitudeDeg: number) => DegreesPerPixel;
|
|
15
|
+
export type LabelFootprintDeg = Readonly<{
|
|
16
|
+
widthDeg: number;
|
|
17
|
+
heightDeg: number;
|
|
18
|
+
}>;
|
|
19
|
+
export type LabelFootprintParams = Readonly<{
|
|
20
|
+
/** Current camera zoom. */
|
|
21
|
+
zoom: number;
|
|
22
|
+
/** Reference latitude (typically the viewport centre) for the lat-per-pixel scale. */
|
|
23
|
+
latitudeDeg: number;
|
|
24
|
+
/** Extra spacing added around every label — the density dial. */
|
|
25
|
+
paddingPx: number;
|
|
26
|
+
}>;
|
|
27
|
+
/**
|
|
28
|
+
* Upper-bound label footprint as a geo-space width/height, for collision testing.
|
|
29
|
+
*
|
|
30
|
+
* A rendered pill's size does NOT follow the zoom circle-size tier: `MapMarker`
|
|
31
|
+
* lays every pill out at the fixed {@link MARKER_PILL_CONTENT_LAYOUT_SIZE}, and the
|
|
32
|
+
* label text is capped at `maxLabelWidthPx` (ellipsis beyond). We size the collision
|
|
33
|
+
* box to that upper bound — widest pill that can render — so the box is never smaller
|
|
34
|
+
* than what paints, which is what keeps the "no two labels overlap" guarantee honest.
|
|
35
|
+
* Density is tuned via `paddingPx`, not by shrinking the box.
|
|
36
|
+
*/
|
|
37
|
+
export declare const labelFootprintDeg: ({ zoom, latitudeDeg, paddingPx }: LabelFootprintParams) => LabelFootprintDeg;
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import type { GeoJsonBbox } from "@trackunit/geo-json-utils";
|
|
2
|
+
type Position = readonly [number, number, ...Array<number>];
|
|
3
|
+
export type UseLabelPlacementParams<TAsset> = Readonly<{
|
|
4
|
+
/** When false the hook returns a stable empty set without computing placement. */
|
|
5
|
+
enabled: boolean;
|
|
6
|
+
/** Candidate markers, in the order they should be considered for a label. */
|
|
7
|
+
items: ReadonlyArray<TAsset>;
|
|
8
|
+
/** Stable id accessor — pass a referentially stable function (module const or `useCallback`). */
|
|
9
|
+
getId: (item: TAsset) => string;
|
|
10
|
+
/** Stable position accessor — pass a referentially stable function. */
|
|
11
|
+
getPosition: (item: TAsset) => Position;
|
|
12
|
+
/** Current camera view box; candidates outside it are skipped. `null` disables clipping. */
|
|
13
|
+
bounds: Readonly<GeoJsonBbox> | null;
|
|
14
|
+
/** Current camera zoom; drives the px→geo footprint scale. */
|
|
15
|
+
zoom: number;
|
|
16
|
+
/** Spacing added around every label footprint — the density dial. */
|
|
17
|
+
paddingPx: number;
|
|
18
|
+
/** Maximum number of labels to place (DOM-node ceiling). */
|
|
19
|
+
ceiling: number;
|
|
20
|
+
}>;
|
|
21
|
+
/**
|
|
22
|
+
* Viewport-driven label placement: returns the ids of the markers that should
|
|
23
|
+
* render a label such that no two labels overlap, bounded by `ceiling`.
|
|
24
|
+
*
|
|
25
|
+
* Derives an upper-bound pill footprint (`labelFootprintDeg`) and delegates
|
|
26
|
+
* selection to the pure `buildLabelPlacement`. Exposed as a hook so the placement
|
|
27
|
+
* participates in React's stability model and later phases can hold incumbency
|
|
28
|
+
* state here without changing the public surface.
|
|
29
|
+
*/
|
|
30
|
+
export declare const useLabelPlacement: <TAsset>({ enabled, items, getId, getPosition, bounds, zoom, paddingPx, ceiling, }: UseLabelPlacementParams<TAsset>) => ReadonlySet<string>;
|
|
31
|
+
export {};
|
|
@@ -9,9 +9,28 @@ type ShapeLabelPillProps = Readonly<{
|
|
|
9
9
|
"data-testid"?: string;
|
|
10
10
|
children: ReactNode;
|
|
11
11
|
}>;
|
|
12
|
-
export declare const cvaShapeLabel: (
|
|
13
|
-
|
|
14
|
-
|
|
12
|
+
export declare const cvaShapeLabel: import("cva").CVAComponent<Omit<{
|
|
13
|
+
base: string[];
|
|
14
|
+
variants: {
|
|
15
|
+
theme: {
|
|
16
|
+
light: string[];
|
|
17
|
+
dark: string[];
|
|
18
|
+
};
|
|
19
|
+
};
|
|
20
|
+
}, "defaultVariants"> & {
|
|
21
|
+
variants: {
|
|
22
|
+
theme: {
|
|
23
|
+
light: string[];
|
|
24
|
+
dark: string[];
|
|
25
|
+
};
|
|
26
|
+
};
|
|
27
|
+
defaultVariants: Omit<{}, never>;
|
|
28
|
+
}, {
|
|
29
|
+
theme: {
|
|
30
|
+
light: string[];
|
|
31
|
+
dark: string[];
|
|
32
|
+
};
|
|
33
|
+
}>;
|
|
15
34
|
/**
|
|
16
35
|
* Shared styled container for shape labels (edge labels and annotation labels).
|
|
17
36
|
*
|
|
@@ -12,7 +12,12 @@
|
|
|
12
12
|
* properties that need to animate, intentionally excluding `width` to prevent
|
|
13
13
|
* non-composited layout thrashing. See `MarkerAnimatedSurface.tsx` for details.
|
|
14
14
|
*/
|
|
15
|
-
export declare const cvaMapMarker:
|
|
15
|
+
export declare const cvaMapMarker: import("cva").CVAComponent<Omit<{
|
|
16
|
+
base: string[];
|
|
17
|
+
}, "defaultVariants"> & {
|
|
18
|
+
variants: unknown;
|
|
19
|
+
defaultVariants: Omit<{}, never>;
|
|
20
|
+
}, unknown>;
|
|
16
21
|
/**
|
|
17
22
|
* Label span inside pill markers.
|
|
18
23
|
*
|
|
@@ -25,11 +30,62 @@ export declare const cvaMapMarker: (props?: import("class-variance-authority/typ
|
|
|
25
30
|
* alone biases the text upward (extra space below). `overflow-x: clip` gives the
|
|
26
31
|
* horizontal ellipsis without a vertical clip on the span itself.
|
|
27
32
|
*/
|
|
28
|
-
export declare const cvaPillLabel:
|
|
33
|
+
export declare const cvaPillLabel: import("cva").CVAComponent<Omit<{
|
|
34
|
+
base: string[];
|
|
35
|
+
}, "defaultVariants"> & {
|
|
36
|
+
variants: unknown;
|
|
37
|
+
defaultVariants: Omit<{}, never>;
|
|
38
|
+
}, unknown>;
|
|
29
39
|
/**
|
|
30
40
|
* Inner indicator disc: marker fill via `--marker-color`, border via CSS `color-mix` on that token (plan §5).
|
|
31
41
|
*/
|
|
32
|
-
export declare const cvaMarkerIndicator: (
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
42
|
+
export declare const cvaMarkerIndicator: import("cva").CVAComponent<Omit<{
|
|
43
|
+
base: string[];
|
|
44
|
+
variants: {
|
|
45
|
+
size: {
|
|
46
|
+
xs: string[];
|
|
47
|
+
sm: string[];
|
|
48
|
+
md: string[];
|
|
49
|
+
lg: string[];
|
|
50
|
+
xl: string[];
|
|
51
|
+
};
|
|
52
|
+
theme: {
|
|
53
|
+
light: string[];
|
|
54
|
+
dark: string[];
|
|
55
|
+
};
|
|
56
|
+
};
|
|
57
|
+
defaultVariants: {
|
|
58
|
+
size: "md";
|
|
59
|
+
theme: "light";
|
|
60
|
+
};
|
|
61
|
+
}, "defaultVariants"> & {
|
|
62
|
+
variants: {
|
|
63
|
+
size: {
|
|
64
|
+
xs: string[];
|
|
65
|
+
sm: string[];
|
|
66
|
+
md: string[];
|
|
67
|
+
lg: string[];
|
|
68
|
+
xl: string[];
|
|
69
|
+
};
|
|
70
|
+
theme: {
|
|
71
|
+
light: string[];
|
|
72
|
+
dark: string[];
|
|
73
|
+
};
|
|
74
|
+
};
|
|
75
|
+
defaultVariants: Omit<{}, "size" | "theme"> & {
|
|
76
|
+
size: "md";
|
|
77
|
+
theme: "light";
|
|
78
|
+
};
|
|
79
|
+
}, {
|
|
80
|
+
size: {
|
|
81
|
+
xs: string[];
|
|
82
|
+
sm: string[];
|
|
83
|
+
md: string[];
|
|
84
|
+
lg: string[];
|
|
85
|
+
xl: string[];
|
|
86
|
+
};
|
|
87
|
+
theme: {
|
|
88
|
+
light: string[];
|
|
89
|
+
dark: string[];
|
|
90
|
+
};
|
|
91
|
+
}>;
|
|
@@ -1,8 +1,57 @@
|
|
|
1
1
|
/** Pads numeric string: 3-digit int part + reserved sign column to avoid layout shift when sign flips. */
|
|
2
2
|
export declare const formatPaddedNumber: (n: number, decimals: number) => string;
|
|
3
|
-
export declare const cvaCoordCell: (
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
3
|
+
export declare const cvaCoordCell: import("cva").CVAComponent<Omit<{
|
|
4
|
+
base: string[];
|
|
5
|
+
variants: {
|
|
6
|
+
variant: {
|
|
7
|
+
lng: string[];
|
|
8
|
+
lat: string[];
|
|
9
|
+
we: string[];
|
|
10
|
+
sn: string[];
|
|
11
|
+
};
|
|
12
|
+
};
|
|
13
|
+
}, "defaultVariants"> & {
|
|
14
|
+
variants: {
|
|
15
|
+
variant: {
|
|
16
|
+
lng: string[];
|
|
17
|
+
lat: string[];
|
|
18
|
+
we: string[];
|
|
19
|
+
sn: string[];
|
|
20
|
+
};
|
|
21
|
+
};
|
|
22
|
+
defaultVariants: Omit<{}, never>;
|
|
23
|
+
}, {
|
|
24
|
+
variant: {
|
|
25
|
+
lng: string[];
|
|
26
|
+
lat: string[];
|
|
27
|
+
we: string[];
|
|
28
|
+
sn: string[];
|
|
29
|
+
};
|
|
30
|
+
}>;
|
|
31
|
+
export declare const cvaStateDisplay: import("cva").CVAComponent<Omit<{
|
|
32
|
+
base: string[];
|
|
33
|
+
variants: {
|
|
34
|
+
expanded: {
|
|
35
|
+
true: string[];
|
|
36
|
+
false: string[];
|
|
37
|
+
};
|
|
38
|
+
};
|
|
39
|
+
defaultVariants: {
|
|
40
|
+
expanded: true;
|
|
41
|
+
};
|
|
42
|
+
}, "defaultVariants"> & {
|
|
43
|
+
variants: {
|
|
44
|
+
expanded: {
|
|
45
|
+
true: string[];
|
|
46
|
+
false: string[];
|
|
47
|
+
};
|
|
48
|
+
};
|
|
49
|
+
defaultVariants: Omit<{}, "expanded"> & {
|
|
50
|
+
expanded: true;
|
|
51
|
+
};
|
|
52
|
+
}, {
|
|
53
|
+
expanded: {
|
|
54
|
+
true: string[];
|
|
55
|
+
false: string[];
|
|
56
|
+
};
|
|
57
|
+
}>;
|
package/migrations/entry.js.map
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"entry.js","sourceRoot":"","sources":["../../../../../libs/react/map/migrations/entry.ts"],"names":[],"mappings":"","sourcesContent":["export {};\n"]}
|