@trackunit/react-map 0.1.24 → 0.1.27
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 +215 -59
- package/index.esm.js +197 -42
- package/package.json +11 -11
- package/src/cursor/useEntitiesNearCursor.d.ts +36 -0
- package/src/index.d.ts +1 -0
- package/src/layers/useShapes/edge/findBestEdgePosition.d.ts +0 -7
package/index.cjs.js
CHANGED
|
@@ -4470,6 +4470,122 @@ function MapMarkerIcon({ name, size, title }) {
|
|
|
4470
4470
|
}, title: title, children: jsxRuntime.jsx(reactComponents.Icon, { ariaHidden: true, name: name, size: "small" }) }));
|
|
4471
4471
|
}
|
|
4472
4472
|
|
|
4473
|
+
const EMPTY_SET = new Set();
|
|
4474
|
+
/**
|
|
4475
|
+
* Order-sensitive equality. The returned Set keeps a stable reference only when
|
|
4476
|
+
* both membership AND nearest-first ordering are identical. A nearest-first swap
|
|
4477
|
+
* within the same id set produces a new reference so consumers that cap to the
|
|
4478
|
+
* N closest via iteration order see the updated ranking.
|
|
4479
|
+
*/
|
|
4480
|
+
const sameNearby = (a, b) => {
|
|
4481
|
+
if (a === b)
|
|
4482
|
+
return true;
|
|
4483
|
+
if (a.size !== b.size)
|
|
4484
|
+
return false;
|
|
4485
|
+
const ai = a.values();
|
|
4486
|
+
const bi = b.values();
|
|
4487
|
+
for (let i = 0; i < a.size; i++) {
|
|
4488
|
+
if (ai.next().value !== bi.next().value)
|
|
4489
|
+
return false;
|
|
4490
|
+
}
|
|
4491
|
+
return true;
|
|
4492
|
+
};
|
|
4493
|
+
const computeNearby = (cursor, entities, zoom, tileSize, radiusPx) => {
|
|
4494
|
+
if (entities.length === 0)
|
|
4495
|
+
return EMPTY_SET;
|
|
4496
|
+
// Project once per recompute: worldSize is constant for all entities at this
|
|
4497
|
+
// camera, so hoist Math.pow(2, zoom) out of the per-entity loop.
|
|
4498
|
+
const worldSize = tileSize * Math.pow(2, zoom);
|
|
4499
|
+
const [cursorPx, cursorPy] = geoJsonUtils.lngLatToMercatorPxWS(cursor[0], cursor[1], worldSize);
|
|
4500
|
+
const radiusSq = radiusPx * radiusPx;
|
|
4501
|
+
const hits = [];
|
|
4502
|
+
for (const entity of entities) {
|
|
4503
|
+
const [px, py] = geoJsonUtils.lngLatToMercatorPxWS(entity.position[0], entity.position[1], worldSize);
|
|
4504
|
+
const dx = px - cursorPx;
|
|
4505
|
+
const dy = py - cursorPy;
|
|
4506
|
+
// Shortest-path x-distance across the antimeridian: a cursor at 179.99° and
|
|
4507
|
+
// an entity at −179.99° are ~0.02° apart on screen but raw Mercator dx is
|
|
4508
|
+
// nearly worldSize. Taking min(|dx|, worldSize−|dx|) collapses that gap.
|
|
4509
|
+
const absDx = Math.abs(dx);
|
|
4510
|
+
const wrappedDx = Math.min(absDx, worldSize - absDx);
|
|
4511
|
+
const distSq = wrappedDx * wrappedDx + dy * dy;
|
|
4512
|
+
if (distSq <= radiusSq)
|
|
4513
|
+
hits.push({ id: entity.id, distSq });
|
|
4514
|
+
}
|
|
4515
|
+
if (hits.length === 0)
|
|
4516
|
+
return EMPTY_SET;
|
|
4517
|
+
// Nearest-first so a consumer can cap to the N closest by iteration order.
|
|
4518
|
+
hits.sort((a, b) => a.distSq - b.distSq);
|
|
4519
|
+
return new Set(hits.map(hit => hit.id));
|
|
4520
|
+
};
|
|
4521
|
+
/**
|
|
4522
|
+
* Reference/value equality for the recompute inputs. `entities` is compared by
|
|
4523
|
+
* reference (the caller memoises it), keeping the per-render change check O(1)
|
|
4524
|
+
* rather than deep-comparing the whole marker array on every frame.
|
|
4525
|
+
*/
|
|
4526
|
+
const sameInputs = (a, b) => a.entities === b.entities && a.zoom === b.zoom && a.tileSize === b.tileSize && a.radiusPx === b.radiusPx;
|
|
4527
|
+
/**
|
|
4528
|
+
* The **Entities Near Cursor** primitive: returns a referentially-stable
|
|
4529
|
+
* `ReadonlySet<string>` of entity ids whose projected position is within
|
|
4530
|
+
* `radiusPx` CSS pixels of the projected cursor, nearest-first.
|
|
4531
|
+
*
|
|
4532
|
+
* It is the point-marker counterpart to **Shapes Under Cursor** and runs on the
|
|
4533
|
+
* same pure Web Mercator substrate (`@trackunit/geo-json-utils`) fed by
|
|
4534
|
+
* `useCameraState` + the RAF-throttled `pointermove` event — adapter-agnostic by
|
|
4535
|
+
* construction (see ADR-0025). The hook owns radius geometry only; consumers
|
|
4536
|
+
* decide why the ids are wanted (preload, highlight, …) and may cap the set to
|
|
4537
|
+
* the nearest N via iteration order.
|
|
4538
|
+
*
|
|
4539
|
+
* The returned Set keeps a stable identity until either its membership or its
|
|
4540
|
+
* nearest-first ordering changes, so wiring it straight into a memo or query
|
|
4541
|
+
* does not churn while the cursor drifts within the same cluster of dots at the
|
|
4542
|
+
* same relative distances. The host only re-renders when membership or ordering
|
|
4543
|
+
* actually changes: the cursor lives in a ref and recomputes commit through an
|
|
4544
|
+
* order-sensitive functional update that bails out (returns the previous Set)
|
|
4545
|
+
* when nothing changed.
|
|
4546
|
+
*/
|
|
4547
|
+
const useEntitiesNearCursor = (api, options) => {
|
|
4548
|
+
const { entities, radiusPx } = options;
|
|
4549
|
+
const { zoom } = useCameraState(api);
|
|
4550
|
+
const { tileSize } = api.state;
|
|
4551
|
+
const [nearby, setNearby] = react.useState(EMPTY_SET);
|
|
4552
|
+
// Latest cursor position and recompute inputs, read by the stable subscription
|
|
4553
|
+
// and recompute without re-subscribing. Written only in effects / event
|
|
4554
|
+
// handlers — never during render.
|
|
4555
|
+
const cursorRef = react.useRef(null);
|
|
4556
|
+
const inputsRef = react.useRef({ entities, zoom, tileSize, radiusPx });
|
|
4557
|
+
const recompute = react.useCallback(() => {
|
|
4558
|
+
const cursor = cursorRef.current;
|
|
4559
|
+
const current = inputsRef.current;
|
|
4560
|
+
const next = cursor === null
|
|
4561
|
+
? EMPTY_SET
|
|
4562
|
+
: computeNearby(cursor, current.entities, current.zoom, current.tileSize, current.radiusPx);
|
|
4563
|
+
// Membership-gated functional update: return the previous reference when the
|
|
4564
|
+
// id set is unchanged so React bails out of the re-render and the output
|
|
4565
|
+
// identity stays stable across cursor drift within the same cluster.
|
|
4566
|
+
setNearby(prev => (sameNearby(prev, next) ? prev : next));
|
|
4567
|
+
}, []);
|
|
4568
|
+
// Keep the recompute inputs fresh and recompute when the camera, entities, or
|
|
4569
|
+
// radius change while the cursor stays put (e.g. zooming under a stationary
|
|
4570
|
+
// pointer brings new dots into radius). `useWatch` owns the effect, so this
|
|
4571
|
+
// file never calls setState synchronously inside its own effect.
|
|
4572
|
+
reactComponents.useWatch({
|
|
4573
|
+
value: { entities, zoom, tileSize, radiusPx },
|
|
4574
|
+
immediate: true,
|
|
4575
|
+
isEqual: sameInputs,
|
|
4576
|
+
onChange: latest => {
|
|
4577
|
+
inputsRef.current = latest;
|
|
4578
|
+
recompute();
|
|
4579
|
+
},
|
|
4580
|
+
});
|
|
4581
|
+
// Track the cursor and recompute on each throttled move.
|
|
4582
|
+
react.useEffect(() => api.on("pointermove", event => {
|
|
4583
|
+
cursorRef.current = event.position;
|
|
4584
|
+
recompute();
|
|
4585
|
+
}), [api, recompute]);
|
|
4586
|
+
return nearby;
|
|
4587
|
+
};
|
|
4588
|
+
|
|
4473
4589
|
const DEFAULT_DEBOUNCE_MS = 150;
|
|
4474
4590
|
/**
|
|
4475
4591
|
* Fires `onShouldPreload` for the currently hovered entity after a debounce
|
|
@@ -7461,14 +7577,17 @@ const degreesPerPixel = (zoom, tileSize) => 360 / (tileSize * Math.pow(2, zoom))
|
|
|
7461
7577
|
const shapesUnderCursor = (position, features, options) => {
|
|
7462
7578
|
const { zoom, tileSize, strokeWidthFor, visibleFillFor, layerGeodesic, featureStyles } = options;
|
|
7463
7579
|
// Densify before boundary-distance and point-in-polygon math so stroke/fill
|
|
7464
|
-
// hit-tests follow great-circle arcs on large polygons.
|
|
7580
|
+
// hit-tests follow great-circle arcs on large polygons. Always defer to
|
|
7581
|
+
// densifyGeodesicFeatures' per-feature resolution (geodesic override > layer
|
|
7582
|
+
// default) rather than short-circuiting on layerGeodesic === false — that
|
|
7583
|
+
// matches the Mapbox render path, so the hit-test geometry never diverges
|
|
7584
|
+
// from the painted edge when a feature opts into geodesic under a non-geodesic
|
|
7585
|
+
// layer. The util returns the original collection when nothing is densified.
|
|
7465
7586
|
const featureCollection = {
|
|
7466
7587
|
type: "FeatureCollection",
|
|
7467
7588
|
features: Array.from(features),
|
|
7468
7589
|
};
|
|
7469
|
-
const densifiedFeatures = layerGeodesic
|
|
7470
|
-
? reactMapAdapterShared.densifyGeodesicFeatures(featureCollection, layerGeodesic ?? true, featureStyles).features
|
|
7471
|
-
: features;
|
|
7590
|
+
const densifiedFeatures = reactMapAdapterShared.densifyGeodesicFeatures(featureCollection, layerGeodesic ?? true, featureStyles).features;
|
|
7472
7591
|
const degPerPx = degreesPerPixel(zoom, tileSize);
|
|
7473
7592
|
const hits = [];
|
|
7474
7593
|
for (const feature of densifiedFeatures) {
|
|
@@ -7580,14 +7699,16 @@ const promoteToFront = (order, id) => order.includes(id) ? [id, ...order.filter(
|
|
|
7580
7699
|
* preserved and translucent fills do not compound under the winner.
|
|
7581
7700
|
*/
|
|
7582
7701
|
const computePromotionGroupFills = (members, winnerId, restingPeerFills = new Map(), layerGeodesic = undefined, featureStyles = undefined) => {
|
|
7583
|
-
// Densify before peer-vs-winner clip so promotion clips follow great-circle
|
|
7702
|
+
// Densify before peer-vs-winner clip so promotion clips follow great-circle
|
|
7703
|
+
// arcs. Always defer to densifyGeodesicFeatures' per-feature resolution
|
|
7704
|
+
// (geodesic override > layer default) rather than short-circuiting on
|
|
7705
|
+
// layerGeodesic === false — keeps the clip geometry in step with the render
|
|
7706
|
+
// path. The util returns the original collection when nothing is densified.
|
|
7584
7707
|
const featureCollection = {
|
|
7585
7708
|
type: "FeatureCollection",
|
|
7586
7709
|
features: Array.from(members),
|
|
7587
7710
|
};
|
|
7588
|
-
const densifiedMembers = layerGeodesic
|
|
7589
|
-
? reactMapAdapterShared.densifyGeodesicFeatures(featureCollection, layerGeodesic ?? true, featureStyles).features
|
|
7590
|
-
: members;
|
|
7711
|
+
const densifiedMembers = reactMapAdapterShared.densifyGeodesicFeatures(featureCollection, layerGeodesic ?? true, featureStyles).features;
|
|
7591
7712
|
const geometries = new Map();
|
|
7592
7713
|
for (const feature of densifiedMembers) {
|
|
7593
7714
|
if (feature.id === undefined)
|
|
@@ -7627,14 +7748,16 @@ const computePromotionGroupFills = (members, winnerId, restingPeerFills = new Ma
|
|
|
7627
7748
|
const computeFillTiling = (input) => {
|
|
7628
7749
|
const { features, viewportBounds, resolveStackOrder, selectedFeatureId, suppressedFeatureIds, layerGeodesic, featureStyles, } = input;
|
|
7629
7750
|
// Densify before any polygon/boundary math so clip geometry follows great-circle
|
|
7630
|
-
// arcs on large polygons (mirrors the ADR-0024 precedent for edge labels).
|
|
7751
|
+
// arcs on large polygons (mirrors the ADR-0024 precedent for edge labels). Always
|
|
7752
|
+
// defer to densifyGeodesicFeatures' per-feature resolution (geodesic override >
|
|
7753
|
+
// layer default) rather than short-circuiting on layerGeodesic === false — keeps
|
|
7754
|
+
// the clip geometry in step with the Mapbox render path. The util returns the
|
|
7755
|
+
// original collection when nothing is densified.
|
|
7631
7756
|
const featureCollection = {
|
|
7632
7757
|
type: "FeatureCollection",
|
|
7633
7758
|
features: Array.from(features),
|
|
7634
7759
|
};
|
|
7635
|
-
const densifiedFeatures = layerGeodesic
|
|
7636
|
-
? reactMapAdapterShared.densifyGeodesicFeatures(featureCollection, layerGeodesic ?? true, featureStyles).features
|
|
7637
|
-
: features;
|
|
7760
|
+
const densifiedFeatures = reactMapAdapterShared.densifyGeodesicFeatures(featureCollection, layerGeodesic ?? true, featureStyles).features;
|
|
7638
7761
|
const records = [];
|
|
7639
7762
|
for (const feature of densifiedFeatures) {
|
|
7640
7763
|
if (feature.id === undefined)
|
|
@@ -8111,7 +8234,6 @@ const DEG_TO_RAD = Math.PI / 180;
|
|
|
8111
8234
|
const RAD_TO_DEG$1 = 180 / Math.PI;
|
|
8112
8235
|
const DEFAULT_EDGE_LABEL_INSET_PX = 6;
|
|
8113
8236
|
const LABEL_HEIGHT_PX = 20;
|
|
8114
|
-
const MAX_WEB_MERCATOR_LAT = 85.05112878;
|
|
8115
8237
|
const EARTH_RADIUS_KM = 6371;
|
|
8116
8238
|
const GEODESIC_PLACEMENT_MAX_SEGMENT_RAD = reactMapAdapterShared.GEODESIC_MAX_SEGMENT_KM / EARTH_RADIUS_KM;
|
|
8117
8239
|
/**
|
|
@@ -8171,24 +8293,6 @@ const clipSegmentToRect = (x0, y0, x1, y1, xMin, yMin, xMax, yMax) => {
|
|
|
8171
8293
|
}
|
|
8172
8294
|
return [x0 + tMin * dx, y0 + tMin * dy, x0 + tMax * dx, y0 + tMax * dy];
|
|
8173
8295
|
};
|
|
8174
|
-
/**
|
|
8175
|
-
* Inner implementations that accept a pre-computed worldSize.
|
|
8176
|
-
* Used by findBestEdgePosition to avoid recomputing Math.pow(2, zoom) on
|
|
8177
|
-
* every vertex when projecting dozens of geodesic sub-segments per frame.
|
|
8178
|
-
*/
|
|
8179
|
-
const lngLatToMercatorPxWS = (lng, lat, worldSize) => {
|
|
8180
|
-
const clampedLat = Math.max(-MAX_WEB_MERCATOR_LAT, Math.min(MAX_WEB_MERCATOR_LAT, lat));
|
|
8181
|
-
const x = ((lng + 180) / 360) * worldSize;
|
|
8182
|
-
const latRad = clampedLat * DEG_TO_RAD;
|
|
8183
|
-
const y = (0.5 - Math.log(Math.tan(Math.PI / 4 + latRad / 2)) / (2 * Math.PI)) * worldSize;
|
|
8184
|
-
return [x, y];
|
|
8185
|
-
};
|
|
8186
|
-
const mercatorPxToLngLatWS = (px, py, worldSize) => {
|
|
8187
|
-
const lng = (px / worldSize) * 360 - 180;
|
|
8188
|
-
const lat = Math.atan(Math.sinh(Math.PI * (1 - (2 * py) / worldSize))) * RAD_TO_DEG$1;
|
|
8189
|
-
return [lng, lat];
|
|
8190
|
-
};
|
|
8191
|
-
const lngLatToWebMercatorPx = (lng, lat, zoom, tileSize = 256) => lngLatToMercatorPxWS(lng, lat, tileSize * Math.pow(2, zoom));
|
|
8192
8296
|
/**
|
|
8193
8297
|
* Determine which side of an edge the geometry interior lies on, in screen space.
|
|
8194
8298
|
* Returns "above" if the label should extend above the edge (interior is below),
|
|
@@ -8539,8 +8643,8 @@ const findBestEdgePosition = ({ features, viewportBounds, zoom, minPixelWidth, l
|
|
|
8539
8643
|
// edge — causing labels to float away from the line at mid-edge positions.
|
|
8540
8644
|
// Clipping in pixel space ensures clip endpoints lie exactly on the
|
|
8541
8645
|
// rendered edge, which fixes the floating at the midpoint of long edges.
|
|
8542
|
-
const [pxViewMin, pyViewMin] = lngLatToMercatorPxWS(minLon, maxLat, worldSize);
|
|
8543
|
-
const [pxViewMax, pyViewMax] = lngLatToMercatorPxWS(maxLon, minLat, worldSize);
|
|
8646
|
+
const [pxViewMin, pyViewMin] = geoJsonUtils.lngLatToMercatorPxWS(minLon, maxLat, worldSize);
|
|
8647
|
+
const [pxViewMax, pyViewMax] = geoJsonUtils.lngLatToMercatorPxWS(maxLon, minLat, worldSize);
|
|
8544
8648
|
const viewportWidth = pxViewMax - pxViewMin;
|
|
8545
8649
|
const viewportHeight = pyViewMax - pyViewMin;
|
|
8546
8650
|
for (let edgeIdx = 0; edgeIdx < edges.length; edgeIdx++) {
|
|
@@ -8558,8 +8662,8 @@ const findBestEdgePosition = ({ features, viewportBounds, zoom, minPixelWidth, l
|
|
|
8558
8662
|
const segmentClips = [];
|
|
8559
8663
|
for (const segment of getPlacementSegments(edge[0], edge[1], geodesic)) {
|
|
8560
8664
|
const { start, end } = segment;
|
|
8561
|
-
const [startPxX, startPxY] = lngLatToMercatorPxWS(start[0], start[1], worldSize);
|
|
8562
|
-
const [endPxX, endPxY] = lngLatToMercatorPxWS(end[0], end[1], worldSize);
|
|
8665
|
+
const [startPxX, startPxY] = geoJsonUtils.lngLatToMercatorPxWS(start[0], start[1], worldSize);
|
|
8666
|
+
const [endPxX, endPxY] = geoJsonUtils.lngLatToMercatorPxWS(end[0], end[1], worldSize);
|
|
8563
8667
|
const clippedPx = clipSegmentToRect(startPxX, startPxY, endPxX, endPxY, pxViewMin, pyViewMin, pxViewMax, pyViewMax);
|
|
8564
8668
|
if (!clippedPx)
|
|
8565
8669
|
continue;
|
|
@@ -8575,8 +8679,8 @@ const findBestEdgePosition = ({ features, viewportBounds, zoom, minPixelWidth, l
|
|
|
8575
8679
|
: [-rawDx / segPxLen, -rawDy / segPxLen];
|
|
8576
8680
|
// Inverse-project pixel clip endpoints to geo for outward-side and
|
|
8577
8681
|
// polygon-interior checks (qualitative, so approximately correct geo is fine).
|
|
8578
|
-
const [ex0, ey0] = mercatorPxToLngLatWS(px0, py0, worldSize);
|
|
8579
|
-
const [ex1, ey1] = mercatorPxToLngLatWS(px1, py1, worldSize);
|
|
8682
|
+
const [ex0, ey0] = geoJsonUtils.mercatorPxToLngLatWS(px0, py0, worldSize);
|
|
8683
|
+
const [ex1, ey1] = geoJsonUtils.mercatorPxToLngLatWS(px1, py1, worldSize);
|
|
8580
8684
|
const midLat = (ey0 + ey1) / 2;
|
|
8581
8685
|
const edgeOutwardSide = centroid !== null ? computeOutwardSide(ex0, ey0, ex1, ey1, centroid, midLat) : "above";
|
|
8582
8686
|
if (isLabelInsidePolygon(ex0, ey0, ex1, ey1, features, centroid))
|
|
@@ -8713,7 +8817,7 @@ const findBestEdgePosition = ({ features, viewportBounds, zoom, minPixelWidth, l
|
|
|
8713
8817
|
for (const matchingCandidate of candidates.filter(c => isSameEdge(previousEdgeIdentity, c.edge))) {
|
|
8714
8818
|
if (matchingCandidate.pxLen <= 0)
|
|
8715
8819
|
continue;
|
|
8716
|
-
const [prevAnchorAbsPx, prevAnchorAbsPy] = lngLatToMercatorPxWS(previousAnchorGeo[0], previousAnchorGeo[1], worldSize);
|
|
8820
|
+
const [prevAnchorAbsPx, prevAnchorAbsPy] = geoJsonUtils.lngLatToMercatorPxWS(previousAnchorGeo[0], previousAnchorGeo[1], worldSize);
|
|
8717
8821
|
const prevAnchorRelX = prevAnchorAbsPx - pxViewMin;
|
|
8718
8822
|
const prevAnchorRelY = prevAnchorAbsPy - pyViewMin;
|
|
8719
8823
|
const dx = prevAnchorRelX - matchingCandidate.readingStartPx[0];
|
|
@@ -8783,7 +8887,7 @@ const findBestEdgePosition = ({ features, viewportBounds, zoom, minPixelWidth, l
|
|
|
8783
8887
|
// great-circle arc by computing the arc fraction via angular distance
|
|
8784
8888
|
// from A. This places the label physically on the visible curved arc
|
|
8785
8889
|
// rather than on the straight Mercator chord.
|
|
8786
|
-
const [approxLng, approxLat] = mercatorPxToLngLatWS(anchorRelX + pxViewMin, anchorRelY + pyViewMin, worldSize);
|
|
8890
|
+
const [approxLng, approxLat] = geoJsonUtils.mercatorPxToLngLatWS(anchorRelX + pxViewMin, anchorRelY + pyViewMin, worldSize);
|
|
8787
8891
|
const tArc = Math.max(0, Math.min(1, reactMapAdapterShared.angularDistance(A[0], A[1], approxLng, approxLat) / delta));
|
|
8788
8892
|
const geoAnchor = reactMapAdapterShared.intermediatePoint(A, B, tArc, delta);
|
|
8789
8893
|
position = [geoAnchor[0], geoAnchor[1]];
|
|
@@ -8797,13 +8901,13 @@ const findBestEdgePosition = ({ features, viewportBounds, zoom, minPixelWidth, l
|
|
|
8797
8901
|
const centerOffsetFactor = resolvedAnchor === "left" ? 1 : resolvedAnchor === "right" ? -1 : 0;
|
|
8798
8902
|
const centerRelX = anchorRelX + centerOffsetFactor * halfLabelPx * best.directionPx[0];
|
|
8799
8903
|
const centerRelY = anchorRelY + centerOffsetFactor * halfLabelPx * best.directionPx[1];
|
|
8800
|
-
const [approxCenterLng, approxCenterLat] = mercatorPxToLngLatWS(centerRelX + pxViewMin, centerRelY + pyViewMin, worldSize);
|
|
8904
|
+
const [approxCenterLng, approxCenterLat] = geoJsonUtils.mercatorPxToLngLatWS(centerRelX + pxViewMin, centerRelY + pyViewMin, worldSize);
|
|
8801
8905
|
const tArcCenter = Math.max(0, Math.min(1, reactMapAdapterShared.angularDistance(A[0], A[1], approxCenterLng, approxCenterLat) / delta));
|
|
8802
8906
|
const TANGENT_EPS = 0.001;
|
|
8803
8907
|
const p0 = reactMapAdapterShared.intermediatePoint(A, B, Math.max(0, tArcCenter - TANGENT_EPS), delta);
|
|
8804
8908
|
const p1 = reactMapAdapterShared.intermediatePoint(A, B, Math.min(1, tArcCenter + TANGENT_EPS), delta);
|
|
8805
|
-
const [bx, by] = lngLatToMercatorPxWS(p0[0], p0[1], worldSize);
|
|
8806
|
-
const [ax, ay] = lngLatToMercatorPxWS(p1[0], p1[1], worldSize);
|
|
8909
|
+
const [bx, by] = geoJsonUtils.lngLatToMercatorPxWS(p0[0], p0[1], worldSize);
|
|
8910
|
+
const [ax, ay] = geoJsonUtils.lngLatToMercatorPxWS(p1[0], p1[1], worldSize);
|
|
8807
8911
|
const ddx = ax - bx;
|
|
8808
8912
|
const ddy = ay - by;
|
|
8809
8913
|
const tangentLen = Math.sqrt(ddx * ddx + ddy * ddy);
|
|
@@ -8820,13 +8924,13 @@ const findBestEdgePosition = ({ features, viewportBounds, zoom, minPixelWidth, l
|
|
|
8820
8924
|
}
|
|
8821
8925
|
else {
|
|
8822
8926
|
// Co-located vertices — fall back to Mercator straight-line.
|
|
8823
|
-
const [anchorLng, anchorLat] = mercatorPxToLngLatWS(anchorRelX + pxViewMin, anchorRelY + pyViewMin, worldSize);
|
|
8927
|
+
const [anchorLng, anchorLat] = geoJsonUtils.mercatorPxToLngLatWS(anchorRelX + pxViewMin, anchorRelY + pyViewMin, worldSize);
|
|
8824
8928
|
position = [anchorLng, anchorLat];
|
|
8825
8929
|
resolvedDirectionPx = best.directionPx;
|
|
8826
8930
|
}
|
|
8827
8931
|
}
|
|
8828
8932
|
else {
|
|
8829
|
-
const [anchorLng, anchorLat] = mercatorPxToLngLatWS(anchorRelX + pxViewMin, anchorRelY + pyViewMin, worldSize);
|
|
8933
|
+
const [anchorLng, anchorLat] = geoJsonUtils.mercatorPxToLngLatWS(anchorRelX + pxViewMin, anchorRelY + pyViewMin, worldSize);
|
|
8830
8934
|
position = [anchorLng, anchorLat];
|
|
8831
8935
|
resolvedDirectionPx = best.directionPx;
|
|
8832
8936
|
}
|
|
@@ -9014,19 +9118,19 @@ const computeEdgeProperties = (start, end, geometry, zoom, tileSize, geodesic =
|
|
|
9014
9118
|
const TANGENT_EPS = 0.001;
|
|
9015
9119
|
const p0 = reactMapAdapterShared.intermediatePoint(start, end, 0.5 - TANGENT_EPS, delta);
|
|
9016
9120
|
const p1 = reactMapAdapterShared.intermediatePoint(start, end, 0.5 + TANGENT_EPS, delta);
|
|
9017
|
-
const [bx, by] = lngLatToWebMercatorPx(p0[0], p0[1], zoom, tileSize);
|
|
9018
|
-
const [ax, ay] = lngLatToWebMercatorPx(p1[0], p1[1], zoom, tileSize);
|
|
9121
|
+
const [bx, by] = geoJsonUtils.lngLatToWebMercatorPx(p0[0], p0[1], zoom, tileSize);
|
|
9122
|
+
const [ax, ay] = geoJsonUtils.lngLatToWebMercatorPx(p1[0], p1[1], zoom, tileSize);
|
|
9019
9123
|
angleDeg = normalizeReadableAngle(Math.atan2(ay - by, ax - bx) * RAD_TO_DEG);
|
|
9020
9124
|
}
|
|
9021
9125
|
else {
|
|
9022
|
-
const [startPx, startPy] = lngLatToWebMercatorPx(start[0], start[1], zoom, tileSize);
|
|
9023
|
-
const [endPx, endPy] = lngLatToWebMercatorPx(end[0], end[1], zoom, tileSize);
|
|
9126
|
+
const [startPx, startPy] = geoJsonUtils.lngLatToWebMercatorPx(start[0], start[1], zoom, tileSize);
|
|
9127
|
+
const [endPx, endPy] = geoJsonUtils.lngLatToWebMercatorPx(end[0], end[1], zoom, tileSize);
|
|
9024
9128
|
angleDeg = normalizeReadableAngle(Math.atan2(endPy - startPy, endPx - startPx) * RAD_TO_DEG);
|
|
9025
9129
|
}
|
|
9026
9130
|
}
|
|
9027
9131
|
else {
|
|
9028
|
-
const [startPx, startPy] = lngLatToWebMercatorPx(start[0], start[1], zoom, tileSize);
|
|
9029
|
-
const [endPx, endPy] = lngLatToWebMercatorPx(end[0], end[1], zoom, tileSize);
|
|
9132
|
+
const [startPx, startPy] = geoJsonUtils.lngLatToWebMercatorPx(start[0], start[1], zoom, tileSize);
|
|
9133
|
+
const [endPx, endPy] = geoJsonUtils.lngLatToWebMercatorPx(end[0], end[1], zoom, tileSize);
|
|
9030
9134
|
angleDeg = normalizeReadableAngle(Math.atan2(endPy - startPy, endPx - startPx) * RAD_TO_DEG);
|
|
9031
9135
|
}
|
|
9032
9136
|
const pixelLength = geoJsonUtils.edgePixelLength(start[0], start[1], end[0], end[1], zoom, midLat, tileSize);
|
|
@@ -10031,6 +10135,8 @@ const useShapeFillTiling = ({ api, handles, onFillGeometriesChange, onZIndexOver
|
|
|
10031
10135
|
}, [suppressedFillIdsByHandle]);
|
|
10032
10136
|
const debounceTimerRef = react.useRef(null);
|
|
10033
10137
|
const idleRecomputeTimerRef = react.useRef(null);
|
|
10138
|
+
/** Pending deferred selection re-tile (double-rAF handle); see selection effect. */
|
|
10139
|
+
const selectionRecomputeRafRef = react.useRef(null);
|
|
10034
10140
|
// Currently emitted overrides — used for hit-testing visible fills in handleSettle.
|
|
10035
10141
|
const prevFillOverridesRef = react.useRef(EMPTY_OVERRIDES);
|
|
10036
10142
|
const prevZIndexOverridesRef = react.useRef(EMPTY_ZINDEX_OVERRIDES);
|
|
@@ -10285,8 +10391,30 @@ const useShapeFillTiling = ({ api, handles, onFillGeometriesChange, onZIndexOver
|
|
|
10285
10391
|
return;
|
|
10286
10392
|
recompute();
|
|
10287
10393
|
}, [recompute, tilingContentKey]);
|
|
10394
|
+
// Selection forces the selected feature to the front of its overlap group,
|
|
10395
|
+
// which requires a resting re-tile (computeFillTiling depends on
|
|
10396
|
+
// selectedFeatureId via resolveStackOrder). For large layers that re-tile is
|
|
10397
|
+
// expensive (~600ms for 200 polygons over global bounds) and must NOT block
|
|
10398
|
+
// the interaction's first paint — e.g. a panel opening on the clicked feature.
|
|
10399
|
+
// Defer it past the next paint with a double rAF so the click commits and
|
|
10400
|
+
// paints immediately, then the selected polygon restacks one frame later.
|
|
10401
|
+
// Rapid selections coalesce: each schedule cancels the previous pending one.
|
|
10288
10402
|
react.useEffect(() => {
|
|
10289
|
-
|
|
10403
|
+
if (selectionRecomputeRafRef.current !== null) {
|
|
10404
|
+
cancelAnimationFrame(selectionRecomputeRafRef.current);
|
|
10405
|
+
}
|
|
10406
|
+
selectionRecomputeRafRef.current = requestAnimationFrame(() => {
|
|
10407
|
+
selectionRecomputeRafRef.current = requestAnimationFrame(() => {
|
|
10408
|
+
selectionRecomputeRafRef.current = null;
|
|
10409
|
+
recompute();
|
|
10410
|
+
});
|
|
10411
|
+
});
|
|
10412
|
+
return () => {
|
|
10413
|
+
if (selectionRecomputeRafRef.current !== null) {
|
|
10414
|
+
cancelAnimationFrame(selectionRecomputeRafRef.current);
|
|
10415
|
+
selectionRecomputeRafRef.current = null;
|
|
10416
|
+
}
|
|
10417
|
+
};
|
|
10290
10418
|
}, [recompute, selectedFeatureId]);
|
|
10291
10419
|
// Suppression changes must trigger a recompute so EMPTY_FILL entries are
|
|
10292
10420
|
// added / removed even when the map is idle (covers the decorations-vs-tiling
|
|
@@ -10538,7 +10666,24 @@ const Layers = ({ layers, api, onEntityClick, onEntityDblClick }) => {
|
|
|
10538
10666
|
const [viewportStyleOverrides, setViewportStyleOverrides] = react.useState(new Map());
|
|
10539
10667
|
const [fillGeometryOverrides, setFillGeometryOverrides] = react.useState(new Map());
|
|
10540
10668
|
const [zIndexOverrides, setZIndexOverrides] = react.useState(new Map());
|
|
10669
|
+
// Mirror the authoritative hovered entity into a ref. The settle channel fires
|
|
10670
|
+
// on a ~120ms debounce (outside React's render cycle) and needs to read the
|
|
10671
|
+
// current hover synchronously.
|
|
10672
|
+
const hoveredEntityRef = react.useRef(layers.interaction.hoveredEntity);
|
|
10673
|
+
react.useEffect(() => {
|
|
10674
|
+
hoveredEntityRef.current = layers.interaction.hoveredEntity;
|
|
10675
|
+
}, [layers.interaction.hoveredEntity]);
|
|
10541
10676
|
const onSettledHover = react.useCallback((handleId, featureId) => {
|
|
10677
|
+
// Z-order truth: markers and clusters render above shapes (DOM/symbol portals
|
|
10678
|
+
// over the polygon canvas). When a point entity is the current hovered entity
|
|
10679
|
+
// it is the authoritative top hit under the cursor, so the shape settle — which
|
|
10680
|
+
// is not itself position-gated against the marker layer — must not overwrite it.
|
|
10681
|
+
// Without this guard, moving onto an asset pill that overlaps a site would leave
|
|
10682
|
+
// the covered site as `hoveredEntity` (a stale cross-channel hover write). We read
|
|
10683
|
+
// the single source of truth (`hoveredEntity`) rather than a parallel guard flag.
|
|
10684
|
+
const hovered = hoveredEntityRef.current;
|
|
10685
|
+
if (hovered !== null && (hovered.type === "marker" || hovered.type === "cluster"))
|
|
10686
|
+
return;
|
|
10542
10687
|
// Fill tiling only fires on polygonal features — shapeType is always "polygon".
|
|
10543
10688
|
layers.hover({ type: "shape", id: featureId, handleId, shapeType: "polygon" });
|
|
10544
10689
|
}, [layers]);
|
|
@@ -10600,17 +10745,27 @@ const Layers = ({ layers, api, onEntityClick, onEntityDblClick }) => {
|
|
|
10600
10745
|
});
|
|
10601
10746
|
}, [layers.handles, adaptiveByLayer, viewportStyleOverrides, fillGeometryOverrides, zIndexOverrides, queryAt]);
|
|
10602
10747
|
const { renderedSourceIds, hasSourceReadiness, readyRevision } = useLayerHandleSync(layerPort, enrichedHandles, layers.interaction, decorationLayers);
|
|
10603
|
-
const hoveredEntityRef = react.useRef(layers.interaction.hoveredEntity);
|
|
10604
|
-
react.useEffect(() => {
|
|
10605
|
-
hoveredEntityRef.current = layers.interaction.hoveredEntity;
|
|
10606
|
-
}, [layers.interaction.hoveredEntity]);
|
|
10607
10748
|
const coordinatedSelect = react.useCallback((entity) => {
|
|
10608
|
-
|
|
10609
|
-
|
|
10610
|
-
}
|
|
10611
|
-
else {
|
|
10749
|
+
const hovered = hoveredEntityRef.current;
|
|
10750
|
+
if (entity === null || hovered === null) {
|
|
10612
10751
|
layers.select(entity);
|
|
10752
|
+
return;
|
|
10753
|
+
}
|
|
10754
|
+
// Default to the entity the click actually carried — it is the freshest,
|
|
10755
|
+
// most direct signal of what the user hit, and it hit-tests per element
|
|
10756
|
+
// for markers/clusters/routes. The single exception is a shape-on-shape
|
|
10757
|
+
// conflict: the adapter resolves polygon clicks with its own z-order and
|
|
10758
|
+
// full (unclipped) hit areas, so it cannot honor the fill-tiling ownership
|
|
10759
|
+
// model that decides which overlapping site is visually on top (ADR-0021).
|
|
10760
|
+
// There the settle-resolved hovered shape is the precise winner, so we
|
|
10761
|
+
// trust it instead. This is also why an asset pill overlapping a site
|
|
10762
|
+
// selects the asset: a marker click is never a shape, so it bypasses the
|
|
10763
|
+
// exception even when a stale site shape is still the hovered entity.
|
|
10764
|
+
if (entity.type === "shape" && hovered.type === "shape") {
|
|
10765
|
+
layers.select(hovered);
|
|
10766
|
+
return;
|
|
10613
10767
|
}
|
|
10768
|
+
layers.select(entity);
|
|
10614
10769
|
}, [layers]);
|
|
10615
10770
|
const fitOnDblClick = useDblClickFit(api, layers.handles);
|
|
10616
10771
|
const handleDblClick = react.useCallback((entity) => {
|
|
@@ -11671,6 +11826,7 @@ exports.useControlStack = useControlStack;
|
|
|
11671
11826
|
exports.useControls = useControls;
|
|
11672
11827
|
exports.useDefaultControls = useDefaultControls;
|
|
11673
11828
|
exports.useDirectionIndicator = useDirectionIndicator;
|
|
11829
|
+
exports.useEntitiesNearCursor = useEntitiesNearCursor;
|
|
11674
11830
|
exports.useExpandedIds = useExpandedIds;
|
|
11675
11831
|
exports.useFitFeatureBounds = useFitFeatureBounds;
|
|
11676
11832
|
exports.useFitToContent = useFitToContent;
|
package/index.esm.js
CHANGED
|
@@ -15,7 +15,7 @@ import { useModal, Modal as Modal$1, ModalHeader, ModalBody } from '@trackunit/r
|
|
|
15
15
|
import { z } from 'zod';
|
|
16
16
|
import { PieChart } from 'react-minimal-pie-chart';
|
|
17
17
|
import { tailwindPalette } from '@trackunit/ui-design-tokens';
|
|
18
|
-
import { validateBboxWithFallback, extractPositionsFromGeometry, geoJsonPositionSchema, validateFeatureCollection, EMPTY_FEATURE_COLLECTION, validateBbox, projectPolygonalToWebMercator, geoJsonPolygonDifference, unprojectPolygonalFromWebMercator, distanceToGeoJsonPolygonBoundary, getGeoJsonPolygonIntersection, isFullyContainedInGeoJsonGeometry, isGeoJsonPointInPolygon, isBboxInsideFeatureCollection, extractEdges, computeGeometryCentroid, isPositionInsideRing, edgePixelLength } from '@trackunit/geo-json-utils';
|
|
18
|
+
import { lngLatToMercatorPxWS, validateBboxWithFallback, extractPositionsFromGeometry, geoJsonPositionSchema, validateFeatureCollection, EMPTY_FEATURE_COLLECTION, validateBbox, projectPolygonalToWebMercator, geoJsonPolygonDifference, unprojectPolygonalFromWebMercator, distanceToGeoJsonPolygonBoundary, getGeoJsonPolygonIntersection, isFullyContainedInGeoJsonGeometry, isGeoJsonPointInPolygon, isBboxInsideFeatureCollection, extractEdges, computeGeometryCentroid, mercatorPxToLngLatWS, isPositionInsideRing, lngLatToWebMercatorPx, edgePixelLength } from '@trackunit/geo-json-utils';
|
|
19
19
|
import { darkenColor, lightenColor } from '@trackunit/react-map-color-utils';
|
|
20
20
|
|
|
21
21
|
var defaultTranslations = {
|
|
@@ -4469,6 +4469,122 @@ function MapMarkerIcon({ name, size, title }) {
|
|
|
4469
4469
|
}, title: title, children: jsx(Icon, { ariaHidden: true, name: name, size: "small" }) }));
|
|
4470
4470
|
}
|
|
4471
4471
|
|
|
4472
|
+
const EMPTY_SET = new Set();
|
|
4473
|
+
/**
|
|
4474
|
+
* Order-sensitive equality. The returned Set keeps a stable reference only when
|
|
4475
|
+
* both membership AND nearest-first ordering are identical. A nearest-first swap
|
|
4476
|
+
* within the same id set produces a new reference so consumers that cap to the
|
|
4477
|
+
* N closest via iteration order see the updated ranking.
|
|
4478
|
+
*/
|
|
4479
|
+
const sameNearby = (a, b) => {
|
|
4480
|
+
if (a === b)
|
|
4481
|
+
return true;
|
|
4482
|
+
if (a.size !== b.size)
|
|
4483
|
+
return false;
|
|
4484
|
+
const ai = a.values();
|
|
4485
|
+
const bi = b.values();
|
|
4486
|
+
for (let i = 0; i < a.size; i++) {
|
|
4487
|
+
if (ai.next().value !== bi.next().value)
|
|
4488
|
+
return false;
|
|
4489
|
+
}
|
|
4490
|
+
return true;
|
|
4491
|
+
};
|
|
4492
|
+
const computeNearby = (cursor, entities, zoom, tileSize, radiusPx) => {
|
|
4493
|
+
if (entities.length === 0)
|
|
4494
|
+
return EMPTY_SET;
|
|
4495
|
+
// Project once per recompute: worldSize is constant for all entities at this
|
|
4496
|
+
// camera, so hoist Math.pow(2, zoom) out of the per-entity loop.
|
|
4497
|
+
const worldSize = tileSize * Math.pow(2, zoom);
|
|
4498
|
+
const [cursorPx, cursorPy] = lngLatToMercatorPxWS(cursor[0], cursor[1], worldSize);
|
|
4499
|
+
const radiusSq = radiusPx * radiusPx;
|
|
4500
|
+
const hits = [];
|
|
4501
|
+
for (const entity of entities) {
|
|
4502
|
+
const [px, py] = lngLatToMercatorPxWS(entity.position[0], entity.position[1], worldSize);
|
|
4503
|
+
const dx = px - cursorPx;
|
|
4504
|
+
const dy = py - cursorPy;
|
|
4505
|
+
// Shortest-path x-distance across the antimeridian: a cursor at 179.99° and
|
|
4506
|
+
// an entity at −179.99° are ~0.02° apart on screen but raw Mercator dx is
|
|
4507
|
+
// nearly worldSize. Taking min(|dx|, worldSize−|dx|) collapses that gap.
|
|
4508
|
+
const absDx = Math.abs(dx);
|
|
4509
|
+
const wrappedDx = Math.min(absDx, worldSize - absDx);
|
|
4510
|
+
const distSq = wrappedDx * wrappedDx + dy * dy;
|
|
4511
|
+
if (distSq <= radiusSq)
|
|
4512
|
+
hits.push({ id: entity.id, distSq });
|
|
4513
|
+
}
|
|
4514
|
+
if (hits.length === 0)
|
|
4515
|
+
return EMPTY_SET;
|
|
4516
|
+
// Nearest-first so a consumer can cap to the N closest by iteration order.
|
|
4517
|
+
hits.sort((a, b) => a.distSq - b.distSq);
|
|
4518
|
+
return new Set(hits.map(hit => hit.id));
|
|
4519
|
+
};
|
|
4520
|
+
/**
|
|
4521
|
+
* Reference/value equality for the recompute inputs. `entities` is compared by
|
|
4522
|
+
* reference (the caller memoises it), keeping the per-render change check O(1)
|
|
4523
|
+
* rather than deep-comparing the whole marker array on every frame.
|
|
4524
|
+
*/
|
|
4525
|
+
const sameInputs = (a, b) => a.entities === b.entities && a.zoom === b.zoom && a.tileSize === b.tileSize && a.radiusPx === b.radiusPx;
|
|
4526
|
+
/**
|
|
4527
|
+
* The **Entities Near Cursor** primitive: returns a referentially-stable
|
|
4528
|
+
* `ReadonlySet<string>` of entity ids whose projected position is within
|
|
4529
|
+
* `radiusPx` CSS pixels of the projected cursor, nearest-first.
|
|
4530
|
+
*
|
|
4531
|
+
* It is the point-marker counterpart to **Shapes Under Cursor** and runs on the
|
|
4532
|
+
* same pure Web Mercator substrate (`@trackunit/geo-json-utils`) fed by
|
|
4533
|
+
* `useCameraState` + the RAF-throttled `pointermove` event — adapter-agnostic by
|
|
4534
|
+
* construction (see ADR-0025). The hook owns radius geometry only; consumers
|
|
4535
|
+
* decide why the ids are wanted (preload, highlight, …) and may cap the set to
|
|
4536
|
+
* the nearest N via iteration order.
|
|
4537
|
+
*
|
|
4538
|
+
* The returned Set keeps a stable identity until either its membership or its
|
|
4539
|
+
* nearest-first ordering changes, so wiring it straight into a memo or query
|
|
4540
|
+
* does not churn while the cursor drifts within the same cluster of dots at the
|
|
4541
|
+
* same relative distances. The host only re-renders when membership or ordering
|
|
4542
|
+
* actually changes: the cursor lives in a ref and recomputes commit through an
|
|
4543
|
+
* order-sensitive functional update that bails out (returns the previous Set)
|
|
4544
|
+
* when nothing changed.
|
|
4545
|
+
*/
|
|
4546
|
+
const useEntitiesNearCursor = (api, options) => {
|
|
4547
|
+
const { entities, radiusPx } = options;
|
|
4548
|
+
const { zoom } = useCameraState(api);
|
|
4549
|
+
const { tileSize } = api.state;
|
|
4550
|
+
const [nearby, setNearby] = useState(EMPTY_SET);
|
|
4551
|
+
// Latest cursor position and recompute inputs, read by the stable subscription
|
|
4552
|
+
// and recompute without re-subscribing. Written only in effects / event
|
|
4553
|
+
// handlers — never during render.
|
|
4554
|
+
const cursorRef = useRef(null);
|
|
4555
|
+
const inputsRef = useRef({ entities, zoom, tileSize, radiusPx });
|
|
4556
|
+
const recompute = useCallback(() => {
|
|
4557
|
+
const cursor = cursorRef.current;
|
|
4558
|
+
const current = inputsRef.current;
|
|
4559
|
+
const next = cursor === null
|
|
4560
|
+
? EMPTY_SET
|
|
4561
|
+
: computeNearby(cursor, current.entities, current.zoom, current.tileSize, current.radiusPx);
|
|
4562
|
+
// Membership-gated functional update: return the previous reference when the
|
|
4563
|
+
// id set is unchanged so React bails out of the re-render and the output
|
|
4564
|
+
// identity stays stable across cursor drift within the same cluster.
|
|
4565
|
+
setNearby(prev => (sameNearby(prev, next) ? prev : next));
|
|
4566
|
+
}, []);
|
|
4567
|
+
// Keep the recompute inputs fresh and recompute when the camera, entities, or
|
|
4568
|
+
// radius change while the cursor stays put (e.g. zooming under a stationary
|
|
4569
|
+
// pointer brings new dots into radius). `useWatch` owns the effect, so this
|
|
4570
|
+
// file never calls setState synchronously inside its own effect.
|
|
4571
|
+
useWatch({
|
|
4572
|
+
value: { entities, zoom, tileSize, radiusPx },
|
|
4573
|
+
immediate: true,
|
|
4574
|
+
isEqual: sameInputs,
|
|
4575
|
+
onChange: latest => {
|
|
4576
|
+
inputsRef.current = latest;
|
|
4577
|
+
recompute();
|
|
4578
|
+
},
|
|
4579
|
+
});
|
|
4580
|
+
// Track the cursor and recompute on each throttled move.
|
|
4581
|
+
useEffect(() => api.on("pointermove", event => {
|
|
4582
|
+
cursorRef.current = event.position;
|
|
4583
|
+
recompute();
|
|
4584
|
+
}), [api, recompute]);
|
|
4585
|
+
return nearby;
|
|
4586
|
+
};
|
|
4587
|
+
|
|
4472
4588
|
const DEFAULT_DEBOUNCE_MS = 150;
|
|
4473
4589
|
/**
|
|
4474
4590
|
* Fires `onShouldPreload` for the currently hovered entity after a debounce
|
|
@@ -7460,14 +7576,17 @@ const degreesPerPixel = (zoom, tileSize) => 360 / (tileSize * Math.pow(2, zoom))
|
|
|
7460
7576
|
const shapesUnderCursor = (position, features, options) => {
|
|
7461
7577
|
const { zoom, tileSize, strokeWidthFor, visibleFillFor, layerGeodesic, featureStyles } = options;
|
|
7462
7578
|
// Densify before boundary-distance and point-in-polygon math so stroke/fill
|
|
7463
|
-
// hit-tests follow great-circle arcs on large polygons.
|
|
7579
|
+
// hit-tests follow great-circle arcs on large polygons. Always defer to
|
|
7580
|
+
// densifyGeodesicFeatures' per-feature resolution (geodesic override > layer
|
|
7581
|
+
// default) rather than short-circuiting on layerGeodesic === false — that
|
|
7582
|
+
// matches the Mapbox render path, so the hit-test geometry never diverges
|
|
7583
|
+
// from the painted edge when a feature opts into geodesic under a non-geodesic
|
|
7584
|
+
// layer. The util returns the original collection when nothing is densified.
|
|
7464
7585
|
const featureCollection = {
|
|
7465
7586
|
type: "FeatureCollection",
|
|
7466
7587
|
features: Array.from(features),
|
|
7467
7588
|
};
|
|
7468
|
-
const densifiedFeatures = layerGeodesic
|
|
7469
|
-
? densifyGeodesicFeatures(featureCollection, layerGeodesic ?? true, featureStyles).features
|
|
7470
|
-
: features;
|
|
7589
|
+
const densifiedFeatures = densifyGeodesicFeatures(featureCollection, layerGeodesic ?? true, featureStyles).features;
|
|
7471
7590
|
const degPerPx = degreesPerPixel(zoom, tileSize);
|
|
7472
7591
|
const hits = [];
|
|
7473
7592
|
for (const feature of densifiedFeatures) {
|
|
@@ -7579,14 +7698,16 @@ const promoteToFront = (order, id) => order.includes(id) ? [id, ...order.filter(
|
|
|
7579
7698
|
* preserved and translucent fills do not compound under the winner.
|
|
7580
7699
|
*/
|
|
7581
7700
|
const computePromotionGroupFills = (members, winnerId, restingPeerFills = new Map(), layerGeodesic = undefined, featureStyles = undefined) => {
|
|
7582
|
-
// Densify before peer-vs-winner clip so promotion clips follow great-circle
|
|
7701
|
+
// Densify before peer-vs-winner clip so promotion clips follow great-circle
|
|
7702
|
+
// arcs. Always defer to densifyGeodesicFeatures' per-feature resolution
|
|
7703
|
+
// (geodesic override > layer default) rather than short-circuiting on
|
|
7704
|
+
// layerGeodesic === false — keeps the clip geometry in step with the render
|
|
7705
|
+
// path. The util returns the original collection when nothing is densified.
|
|
7583
7706
|
const featureCollection = {
|
|
7584
7707
|
type: "FeatureCollection",
|
|
7585
7708
|
features: Array.from(members),
|
|
7586
7709
|
};
|
|
7587
|
-
const densifiedMembers = layerGeodesic
|
|
7588
|
-
? densifyGeodesicFeatures(featureCollection, layerGeodesic ?? true, featureStyles).features
|
|
7589
|
-
: members;
|
|
7710
|
+
const densifiedMembers = densifyGeodesicFeatures(featureCollection, layerGeodesic ?? true, featureStyles).features;
|
|
7590
7711
|
const geometries = new Map();
|
|
7591
7712
|
for (const feature of densifiedMembers) {
|
|
7592
7713
|
if (feature.id === undefined)
|
|
@@ -7626,14 +7747,16 @@ const computePromotionGroupFills = (members, winnerId, restingPeerFills = new Ma
|
|
|
7626
7747
|
const computeFillTiling = (input) => {
|
|
7627
7748
|
const { features, viewportBounds, resolveStackOrder, selectedFeatureId, suppressedFeatureIds, layerGeodesic, featureStyles, } = input;
|
|
7628
7749
|
// Densify before any polygon/boundary math so clip geometry follows great-circle
|
|
7629
|
-
// arcs on large polygons (mirrors the ADR-0024 precedent for edge labels).
|
|
7750
|
+
// arcs on large polygons (mirrors the ADR-0024 precedent for edge labels). Always
|
|
7751
|
+
// defer to densifyGeodesicFeatures' per-feature resolution (geodesic override >
|
|
7752
|
+
// layer default) rather than short-circuiting on layerGeodesic === false — keeps
|
|
7753
|
+
// the clip geometry in step with the Mapbox render path. The util returns the
|
|
7754
|
+
// original collection when nothing is densified.
|
|
7630
7755
|
const featureCollection = {
|
|
7631
7756
|
type: "FeatureCollection",
|
|
7632
7757
|
features: Array.from(features),
|
|
7633
7758
|
};
|
|
7634
|
-
const densifiedFeatures = layerGeodesic
|
|
7635
|
-
? densifyGeodesicFeatures(featureCollection, layerGeodesic ?? true, featureStyles).features
|
|
7636
|
-
: features;
|
|
7759
|
+
const densifiedFeatures = densifyGeodesicFeatures(featureCollection, layerGeodesic ?? true, featureStyles).features;
|
|
7637
7760
|
const records = [];
|
|
7638
7761
|
for (const feature of densifiedFeatures) {
|
|
7639
7762
|
if (feature.id === undefined)
|
|
@@ -8110,7 +8233,6 @@ const DEG_TO_RAD = Math.PI / 180;
|
|
|
8110
8233
|
const RAD_TO_DEG$1 = 180 / Math.PI;
|
|
8111
8234
|
const DEFAULT_EDGE_LABEL_INSET_PX = 6;
|
|
8112
8235
|
const LABEL_HEIGHT_PX = 20;
|
|
8113
|
-
const MAX_WEB_MERCATOR_LAT = 85.05112878;
|
|
8114
8236
|
const EARTH_RADIUS_KM = 6371;
|
|
8115
8237
|
const GEODESIC_PLACEMENT_MAX_SEGMENT_RAD = GEODESIC_MAX_SEGMENT_KM / EARTH_RADIUS_KM;
|
|
8116
8238
|
/**
|
|
@@ -8170,24 +8292,6 @@ const clipSegmentToRect = (x0, y0, x1, y1, xMin, yMin, xMax, yMax) => {
|
|
|
8170
8292
|
}
|
|
8171
8293
|
return [x0 + tMin * dx, y0 + tMin * dy, x0 + tMax * dx, y0 + tMax * dy];
|
|
8172
8294
|
};
|
|
8173
|
-
/**
|
|
8174
|
-
* Inner implementations that accept a pre-computed worldSize.
|
|
8175
|
-
* Used by findBestEdgePosition to avoid recomputing Math.pow(2, zoom) on
|
|
8176
|
-
* every vertex when projecting dozens of geodesic sub-segments per frame.
|
|
8177
|
-
*/
|
|
8178
|
-
const lngLatToMercatorPxWS = (lng, lat, worldSize) => {
|
|
8179
|
-
const clampedLat = Math.max(-MAX_WEB_MERCATOR_LAT, Math.min(MAX_WEB_MERCATOR_LAT, lat));
|
|
8180
|
-
const x = ((lng + 180) / 360) * worldSize;
|
|
8181
|
-
const latRad = clampedLat * DEG_TO_RAD;
|
|
8182
|
-
const y = (0.5 - Math.log(Math.tan(Math.PI / 4 + latRad / 2)) / (2 * Math.PI)) * worldSize;
|
|
8183
|
-
return [x, y];
|
|
8184
|
-
};
|
|
8185
|
-
const mercatorPxToLngLatWS = (px, py, worldSize) => {
|
|
8186
|
-
const lng = (px / worldSize) * 360 - 180;
|
|
8187
|
-
const lat = Math.atan(Math.sinh(Math.PI * (1 - (2 * py) / worldSize))) * RAD_TO_DEG$1;
|
|
8188
|
-
return [lng, lat];
|
|
8189
|
-
};
|
|
8190
|
-
const lngLatToWebMercatorPx = (lng, lat, zoom, tileSize = 256) => lngLatToMercatorPxWS(lng, lat, tileSize * Math.pow(2, zoom));
|
|
8191
8295
|
/**
|
|
8192
8296
|
* Determine which side of an edge the geometry interior lies on, in screen space.
|
|
8193
8297
|
* Returns "above" if the label should extend above the edge (interior is below),
|
|
@@ -10030,6 +10134,8 @@ const useShapeFillTiling = ({ api, handles, onFillGeometriesChange, onZIndexOver
|
|
|
10030
10134
|
}, [suppressedFillIdsByHandle]);
|
|
10031
10135
|
const debounceTimerRef = useRef(null);
|
|
10032
10136
|
const idleRecomputeTimerRef = useRef(null);
|
|
10137
|
+
/** Pending deferred selection re-tile (double-rAF handle); see selection effect. */
|
|
10138
|
+
const selectionRecomputeRafRef = useRef(null);
|
|
10033
10139
|
// Currently emitted overrides — used for hit-testing visible fills in handleSettle.
|
|
10034
10140
|
const prevFillOverridesRef = useRef(EMPTY_OVERRIDES);
|
|
10035
10141
|
const prevZIndexOverridesRef = useRef(EMPTY_ZINDEX_OVERRIDES);
|
|
@@ -10284,8 +10390,30 @@ const useShapeFillTiling = ({ api, handles, onFillGeometriesChange, onZIndexOver
|
|
|
10284
10390
|
return;
|
|
10285
10391
|
recompute();
|
|
10286
10392
|
}, [recompute, tilingContentKey]);
|
|
10393
|
+
// Selection forces the selected feature to the front of its overlap group,
|
|
10394
|
+
// which requires a resting re-tile (computeFillTiling depends on
|
|
10395
|
+
// selectedFeatureId via resolveStackOrder). For large layers that re-tile is
|
|
10396
|
+
// expensive (~600ms for 200 polygons over global bounds) and must NOT block
|
|
10397
|
+
// the interaction's first paint — e.g. a panel opening on the clicked feature.
|
|
10398
|
+
// Defer it past the next paint with a double rAF so the click commits and
|
|
10399
|
+
// paints immediately, then the selected polygon restacks one frame later.
|
|
10400
|
+
// Rapid selections coalesce: each schedule cancels the previous pending one.
|
|
10287
10401
|
useEffect(() => {
|
|
10288
|
-
|
|
10402
|
+
if (selectionRecomputeRafRef.current !== null) {
|
|
10403
|
+
cancelAnimationFrame(selectionRecomputeRafRef.current);
|
|
10404
|
+
}
|
|
10405
|
+
selectionRecomputeRafRef.current = requestAnimationFrame(() => {
|
|
10406
|
+
selectionRecomputeRafRef.current = requestAnimationFrame(() => {
|
|
10407
|
+
selectionRecomputeRafRef.current = null;
|
|
10408
|
+
recompute();
|
|
10409
|
+
});
|
|
10410
|
+
});
|
|
10411
|
+
return () => {
|
|
10412
|
+
if (selectionRecomputeRafRef.current !== null) {
|
|
10413
|
+
cancelAnimationFrame(selectionRecomputeRafRef.current);
|
|
10414
|
+
selectionRecomputeRafRef.current = null;
|
|
10415
|
+
}
|
|
10416
|
+
};
|
|
10289
10417
|
}, [recompute, selectedFeatureId]);
|
|
10290
10418
|
// Suppression changes must trigger a recompute so EMPTY_FILL entries are
|
|
10291
10419
|
// added / removed even when the map is idle (covers the decorations-vs-tiling
|
|
@@ -10537,7 +10665,24 @@ const Layers = ({ layers, api, onEntityClick, onEntityDblClick }) => {
|
|
|
10537
10665
|
const [viewportStyleOverrides, setViewportStyleOverrides] = useState(new Map());
|
|
10538
10666
|
const [fillGeometryOverrides, setFillGeometryOverrides] = useState(new Map());
|
|
10539
10667
|
const [zIndexOverrides, setZIndexOverrides] = useState(new Map());
|
|
10668
|
+
// Mirror the authoritative hovered entity into a ref. The settle channel fires
|
|
10669
|
+
// on a ~120ms debounce (outside React's render cycle) and needs to read the
|
|
10670
|
+
// current hover synchronously.
|
|
10671
|
+
const hoveredEntityRef = useRef(layers.interaction.hoveredEntity);
|
|
10672
|
+
useEffect(() => {
|
|
10673
|
+
hoveredEntityRef.current = layers.interaction.hoveredEntity;
|
|
10674
|
+
}, [layers.interaction.hoveredEntity]);
|
|
10540
10675
|
const onSettledHover = useCallback((handleId, featureId) => {
|
|
10676
|
+
// Z-order truth: markers and clusters render above shapes (DOM/symbol portals
|
|
10677
|
+
// over the polygon canvas). When a point entity is the current hovered entity
|
|
10678
|
+
// it is the authoritative top hit under the cursor, so the shape settle — which
|
|
10679
|
+
// is not itself position-gated against the marker layer — must not overwrite it.
|
|
10680
|
+
// Without this guard, moving onto an asset pill that overlaps a site would leave
|
|
10681
|
+
// the covered site as `hoveredEntity` (a stale cross-channel hover write). We read
|
|
10682
|
+
// the single source of truth (`hoveredEntity`) rather than a parallel guard flag.
|
|
10683
|
+
const hovered = hoveredEntityRef.current;
|
|
10684
|
+
if (hovered !== null && (hovered.type === "marker" || hovered.type === "cluster"))
|
|
10685
|
+
return;
|
|
10541
10686
|
// Fill tiling only fires on polygonal features — shapeType is always "polygon".
|
|
10542
10687
|
layers.hover({ type: "shape", id: featureId, handleId, shapeType: "polygon" });
|
|
10543
10688
|
}, [layers]);
|
|
@@ -10599,17 +10744,27 @@ const Layers = ({ layers, api, onEntityClick, onEntityDblClick }) => {
|
|
|
10599
10744
|
});
|
|
10600
10745
|
}, [layers.handles, adaptiveByLayer, viewportStyleOverrides, fillGeometryOverrides, zIndexOverrides, queryAt]);
|
|
10601
10746
|
const { renderedSourceIds, hasSourceReadiness, readyRevision } = useLayerHandleSync(layerPort, enrichedHandles, layers.interaction, decorationLayers);
|
|
10602
|
-
const hoveredEntityRef = useRef(layers.interaction.hoveredEntity);
|
|
10603
|
-
useEffect(() => {
|
|
10604
|
-
hoveredEntityRef.current = layers.interaction.hoveredEntity;
|
|
10605
|
-
}, [layers.interaction.hoveredEntity]);
|
|
10606
10747
|
const coordinatedSelect = useCallback((entity) => {
|
|
10607
|
-
|
|
10608
|
-
|
|
10609
|
-
}
|
|
10610
|
-
else {
|
|
10748
|
+
const hovered = hoveredEntityRef.current;
|
|
10749
|
+
if (entity === null || hovered === null) {
|
|
10611
10750
|
layers.select(entity);
|
|
10751
|
+
return;
|
|
10752
|
+
}
|
|
10753
|
+
// Default to the entity the click actually carried — it is the freshest,
|
|
10754
|
+
// most direct signal of what the user hit, and it hit-tests per element
|
|
10755
|
+
// for markers/clusters/routes. The single exception is a shape-on-shape
|
|
10756
|
+
// conflict: the adapter resolves polygon clicks with its own z-order and
|
|
10757
|
+
// full (unclipped) hit areas, so it cannot honor the fill-tiling ownership
|
|
10758
|
+
// model that decides which overlapping site is visually on top (ADR-0021).
|
|
10759
|
+
// There the settle-resolved hovered shape is the precise winner, so we
|
|
10760
|
+
// trust it instead. This is also why an asset pill overlapping a site
|
|
10761
|
+
// selects the asset: a marker click is never a shape, so it bypasses the
|
|
10762
|
+
// exception even when a stale site shape is still the hovered entity.
|
|
10763
|
+
if (entity.type === "shape" && hovered.type === "shape") {
|
|
10764
|
+
layers.select(hovered);
|
|
10765
|
+
return;
|
|
10612
10766
|
}
|
|
10767
|
+
layers.select(entity);
|
|
10613
10768
|
}, [layers]);
|
|
10614
10769
|
const fitOnDblClick = useDblClickFit(api, layers.handles);
|
|
10615
10770
|
const handleDblClick = useCallback((entity) => {
|
|
@@ -11589,4 +11744,4 @@ const mockMapApi = (overrides) => {
|
|
|
11589
11744
|
*/
|
|
11590
11745
|
setupLibraryTranslations();
|
|
11591
11746
|
|
|
11592
|
-
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, ShapeAnnotationLabel, buildExpandedIds, cvaMapMarker, cvaMarkerIndicator, mockMapApi, useAdaptiveMarkerHelpers, useAutoPanResolver, useCameraIdle, useCameraState, useClusterCountFormat, useControlStack, useControls, useDefaultControls, useDirectionIndicator, useExpandedIds, useFitFeatureBounds, useFitToContent, useImageOverlay, useLayers, useMap, useMapAnnotation, useMapAnnotations, useMapAppearanceControls, useMapKeyboardNavigation, useMarkerColors, useMarkerStateResolvers, useMarkers, usePanel, usePanelPreload, usePreviewMap, useRoute, useShapeLabelHelpers, useShapes, useViewportContext };
|
|
11747
|
+
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, 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 };
|
package/package.json
CHANGED
|
@@ -1,23 +1,23 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@trackunit/react-map",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.27",
|
|
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.1.
|
|
11
|
-
"@trackunit/css-class-variance-utilities": "1.13.
|
|
12
|
-
"@trackunit/react-form-components": "2.1.
|
|
13
|
-
"@trackunit/react-core-hooks": "1.17.
|
|
14
|
-
"@trackunit/geo-json-utils": "1.14.
|
|
15
|
-
"@trackunit/i18n-library-translation": "2.0.
|
|
16
|
-
"@trackunit/react-modal": "2.1.
|
|
10
|
+
"@trackunit/react-components": "2.1.38",
|
|
11
|
+
"@trackunit/css-class-variance-utilities": "1.13.42",
|
|
12
|
+
"@trackunit/react-form-components": "2.1.40",
|
|
13
|
+
"@trackunit/react-core-hooks": "1.17.51",
|
|
14
|
+
"@trackunit/geo-json-utils": "1.14.45",
|
|
15
|
+
"@trackunit/i18n-library-translation": "2.0.39",
|
|
16
|
+
"@trackunit/react-modal": "2.1.41",
|
|
17
17
|
"react-minimal-pie-chart": "^8.4.0",
|
|
18
|
-
"@trackunit/react-map-adapter-shared": "0.0.
|
|
19
|
-
"@trackunit/react-map-color-utils": "0.0.
|
|
20
|
-
"@trackunit/ui-design-tokens": "1.13.
|
|
18
|
+
"@trackunit/react-map-adapter-shared": "0.0.26",
|
|
19
|
+
"@trackunit/react-map-color-utils": "0.0.11",
|
|
20
|
+
"@trackunit/ui-design-tokens": "1.13.42",
|
|
21
21
|
"@floating-ui/react": "^0.26.25",
|
|
22
22
|
"es-toolkit": "^1.39.10",
|
|
23
23
|
"tailwind-merge": "^2.0.0",
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import type { GeoJsonPosition, MapApi } from "../core/types";
|
|
2
|
+
/**
|
|
3
|
+
* A point entity that can be tested against the cursor. `position` is the
|
|
4
|
+
* geographic anchor as `[lng, lat]`.
|
|
5
|
+
*/
|
|
6
|
+
export type EntityNearCursor = Readonly<{
|
|
7
|
+
id: string;
|
|
8
|
+
position: GeoJsonPosition;
|
|
9
|
+
}>;
|
|
10
|
+
export type UseEntitiesNearCursorOptions = Readonly<{
|
|
11
|
+
/** The point entities to hit-test against the cursor on every move. */
|
|
12
|
+
entities: ReadonlyArray<EntityNearCursor>;
|
|
13
|
+
/** Hit radius around the cursor, in CSS pixels. */
|
|
14
|
+
radiusPx: number;
|
|
15
|
+
}>;
|
|
16
|
+
/**
|
|
17
|
+
* The **Entities Near Cursor** primitive: returns a referentially-stable
|
|
18
|
+
* `ReadonlySet<string>` of entity ids whose projected position is within
|
|
19
|
+
* `radiusPx` CSS pixels of the projected cursor, nearest-first.
|
|
20
|
+
*
|
|
21
|
+
* It is the point-marker counterpart to **Shapes Under Cursor** and runs on the
|
|
22
|
+
* same pure Web Mercator substrate (`@trackunit/geo-json-utils`) fed by
|
|
23
|
+
* `useCameraState` + the RAF-throttled `pointermove` event — adapter-agnostic by
|
|
24
|
+
* construction (see ADR-0025). The hook owns radius geometry only; consumers
|
|
25
|
+
* decide why the ids are wanted (preload, highlight, …) and may cap the set to
|
|
26
|
+
* the nearest N via iteration order.
|
|
27
|
+
*
|
|
28
|
+
* The returned Set keeps a stable identity until either its membership or its
|
|
29
|
+
* nearest-first ordering changes, so wiring it straight into a memo or query
|
|
30
|
+
* does not churn while the cursor drifts within the same cluster of dots at the
|
|
31
|
+
* same relative distances. The host only re-renders when membership or ordering
|
|
32
|
+
* actually changes: the cursor lives in a ref and recomputes commit through an
|
|
33
|
+
* order-sensitive functional update that bails out (returns the previous Set)
|
|
34
|
+
* when nothing changed.
|
|
35
|
+
*/
|
|
36
|
+
export declare const useEntitiesNearCursor: (api: MapApi, options: UseEntitiesNearCursorOptions) => ReadonlySet<string>;
|
package/src/index.d.ts
CHANGED
|
@@ -38,6 +38,7 @@ export { MARKER_TUNING } from "./markers/model/markerTuningParams";
|
|
|
38
38
|
export { cvaMapMarker, cvaMarkerIndicator } from "./markers/shared/mapMarkerVariants";
|
|
39
39
|
export { MARKER_DARK_PILL, MARKER_LIGHT_PILL } from "./markers/shared/markerColors";
|
|
40
40
|
export type { MarkerColorConfig, MarkerColorCssVars, ResolvedMarkerColors } from "./markers/shared/markerColors";
|
|
41
|
+
export { useEntitiesNearCursor, type EntityNearCursor, type UseEntitiesNearCursorOptions, } from "./cursor/useEntitiesNearCursor";
|
|
41
42
|
export { type HoverPanelPreloadInitiator, type PanelPreloadInitiator, type PanelPreloadInitiatorType, type ProximityPanelPreloadInitiator, } from "./panel/preload/preloadInitiators";
|
|
42
43
|
export { usePanelPreload, type UsePanelPreloadOptions } from "./panel/preload/usePanelPreload";
|
|
43
44
|
export { type PanelStore } from "./panel/store/panels";
|
|
@@ -39,8 +39,6 @@ export type EdgeInsets = Readonly<{
|
|
|
39
39
|
export declare const clipSegmentToRect: (x0: number, y0: number, x1: number, y1: number, xMin: number, yMin: number, xMax: number, yMax: number) => ClippedSegment | null;
|
|
40
40
|
export { extractEdges } from "@trackunit/geo-json-utils";
|
|
41
41
|
export { computeGeometryCentroid } from "@trackunit/geo-json-utils";
|
|
42
|
-
export declare const lngLatToWebMercatorPx: (lng: number, lat: number, zoom: number, tileSize?: number) => readonly [number, number];
|
|
43
|
-
export declare const webMercatorPxToLngLat: (px: number, py: number, zoom: number, tileSize?: number) => readonly [number, number];
|
|
44
42
|
/**
|
|
45
43
|
* Compute the screen-space angle (in degrees) of a line segment, accounting for
|
|
46
44
|
* Web Mercator distortion. Normalized to [-90, 90] so text reads left-to-right.
|
|
@@ -60,11 +58,6 @@ export declare const edgeScreenAngleDeg: (x0: number, y0: number, x1: number, y1
|
|
|
60
58
|
* Latitude pixels scale by sec(lat).
|
|
61
59
|
*/
|
|
62
60
|
export declare const edgePixelLength: (x0: number, y0: number, x1: number, y1: number, zoom: number, midLatDeg: number, tileSize?: number) => number;
|
|
63
|
-
/**
|
|
64
|
-
* Convert a pixel distance to latitude degrees at a given zoom level,
|
|
65
|
-
* accounting for Web Mercator latitude distortion.
|
|
66
|
-
*/
|
|
67
|
-
export declare const pixelsToLatDegrees: (pixels: number, zoom: number, latDeg: number, tileSize?: number) => number;
|
|
68
61
|
/**
|
|
69
62
|
* Determine which side of an edge the geometry interior lies on, in screen space.
|
|
70
63
|
* Returns "above" if the label should extend above the edge (interior is below),
|