@trackunit/react-map 0.1.24 → 0.1.25
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 +136 -38
- package/index.esm.js +118 -21
- 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
|
|
@@ -8111,7 +8227,6 @@ const DEG_TO_RAD = Math.PI / 180;
|
|
|
8111
8227
|
const RAD_TO_DEG$1 = 180 / Math.PI;
|
|
8112
8228
|
const DEFAULT_EDGE_LABEL_INSET_PX = 6;
|
|
8113
8229
|
const LABEL_HEIGHT_PX = 20;
|
|
8114
|
-
const MAX_WEB_MERCATOR_LAT = 85.05112878;
|
|
8115
8230
|
const EARTH_RADIUS_KM = 6371;
|
|
8116
8231
|
const GEODESIC_PLACEMENT_MAX_SEGMENT_RAD = reactMapAdapterShared.GEODESIC_MAX_SEGMENT_KM / EARTH_RADIUS_KM;
|
|
8117
8232
|
/**
|
|
@@ -8171,24 +8286,6 @@ const clipSegmentToRect = (x0, y0, x1, y1, xMin, yMin, xMax, yMax) => {
|
|
|
8171
8286
|
}
|
|
8172
8287
|
return [x0 + tMin * dx, y0 + tMin * dy, x0 + tMax * dx, y0 + tMax * dy];
|
|
8173
8288
|
};
|
|
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
8289
|
/**
|
|
8193
8290
|
* Determine which side of an edge the geometry interior lies on, in screen space.
|
|
8194
8291
|
* Returns "above" if the label should extend above the edge (interior is below),
|
|
@@ -8539,8 +8636,8 @@ const findBestEdgePosition = ({ features, viewportBounds, zoom, minPixelWidth, l
|
|
|
8539
8636
|
// edge — causing labels to float away from the line at mid-edge positions.
|
|
8540
8637
|
// Clipping in pixel space ensures clip endpoints lie exactly on the
|
|
8541
8638
|
// 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);
|
|
8639
|
+
const [pxViewMin, pyViewMin] = geoJsonUtils.lngLatToMercatorPxWS(minLon, maxLat, worldSize);
|
|
8640
|
+
const [pxViewMax, pyViewMax] = geoJsonUtils.lngLatToMercatorPxWS(maxLon, minLat, worldSize);
|
|
8544
8641
|
const viewportWidth = pxViewMax - pxViewMin;
|
|
8545
8642
|
const viewportHeight = pyViewMax - pyViewMin;
|
|
8546
8643
|
for (let edgeIdx = 0; edgeIdx < edges.length; edgeIdx++) {
|
|
@@ -8558,8 +8655,8 @@ const findBestEdgePosition = ({ features, viewportBounds, zoom, minPixelWidth, l
|
|
|
8558
8655
|
const segmentClips = [];
|
|
8559
8656
|
for (const segment of getPlacementSegments(edge[0], edge[1], geodesic)) {
|
|
8560
8657
|
const { start, end } = segment;
|
|
8561
|
-
const [startPxX, startPxY] = lngLatToMercatorPxWS(start[0], start[1], worldSize);
|
|
8562
|
-
const [endPxX, endPxY] = lngLatToMercatorPxWS(end[0], end[1], worldSize);
|
|
8658
|
+
const [startPxX, startPxY] = geoJsonUtils.lngLatToMercatorPxWS(start[0], start[1], worldSize);
|
|
8659
|
+
const [endPxX, endPxY] = geoJsonUtils.lngLatToMercatorPxWS(end[0], end[1], worldSize);
|
|
8563
8660
|
const clippedPx = clipSegmentToRect(startPxX, startPxY, endPxX, endPxY, pxViewMin, pyViewMin, pxViewMax, pyViewMax);
|
|
8564
8661
|
if (!clippedPx)
|
|
8565
8662
|
continue;
|
|
@@ -8575,8 +8672,8 @@ const findBestEdgePosition = ({ features, viewportBounds, zoom, minPixelWidth, l
|
|
|
8575
8672
|
: [-rawDx / segPxLen, -rawDy / segPxLen];
|
|
8576
8673
|
// Inverse-project pixel clip endpoints to geo for outward-side and
|
|
8577
8674
|
// 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);
|
|
8675
|
+
const [ex0, ey0] = geoJsonUtils.mercatorPxToLngLatWS(px0, py0, worldSize);
|
|
8676
|
+
const [ex1, ey1] = geoJsonUtils.mercatorPxToLngLatWS(px1, py1, worldSize);
|
|
8580
8677
|
const midLat = (ey0 + ey1) / 2;
|
|
8581
8678
|
const edgeOutwardSide = centroid !== null ? computeOutwardSide(ex0, ey0, ex1, ey1, centroid, midLat) : "above";
|
|
8582
8679
|
if (isLabelInsidePolygon(ex0, ey0, ex1, ey1, features, centroid))
|
|
@@ -8713,7 +8810,7 @@ const findBestEdgePosition = ({ features, viewportBounds, zoom, minPixelWidth, l
|
|
|
8713
8810
|
for (const matchingCandidate of candidates.filter(c => isSameEdge(previousEdgeIdentity, c.edge))) {
|
|
8714
8811
|
if (matchingCandidate.pxLen <= 0)
|
|
8715
8812
|
continue;
|
|
8716
|
-
const [prevAnchorAbsPx, prevAnchorAbsPy] = lngLatToMercatorPxWS(previousAnchorGeo[0], previousAnchorGeo[1], worldSize);
|
|
8813
|
+
const [prevAnchorAbsPx, prevAnchorAbsPy] = geoJsonUtils.lngLatToMercatorPxWS(previousAnchorGeo[0], previousAnchorGeo[1], worldSize);
|
|
8717
8814
|
const prevAnchorRelX = prevAnchorAbsPx - pxViewMin;
|
|
8718
8815
|
const prevAnchorRelY = prevAnchorAbsPy - pyViewMin;
|
|
8719
8816
|
const dx = prevAnchorRelX - matchingCandidate.readingStartPx[0];
|
|
@@ -8783,7 +8880,7 @@ const findBestEdgePosition = ({ features, viewportBounds, zoom, minPixelWidth, l
|
|
|
8783
8880
|
// great-circle arc by computing the arc fraction via angular distance
|
|
8784
8881
|
// from A. This places the label physically on the visible curved arc
|
|
8785
8882
|
// rather than on the straight Mercator chord.
|
|
8786
|
-
const [approxLng, approxLat] = mercatorPxToLngLatWS(anchorRelX + pxViewMin, anchorRelY + pyViewMin, worldSize);
|
|
8883
|
+
const [approxLng, approxLat] = geoJsonUtils.mercatorPxToLngLatWS(anchorRelX + pxViewMin, anchorRelY + pyViewMin, worldSize);
|
|
8787
8884
|
const tArc = Math.max(0, Math.min(1, reactMapAdapterShared.angularDistance(A[0], A[1], approxLng, approxLat) / delta));
|
|
8788
8885
|
const geoAnchor = reactMapAdapterShared.intermediatePoint(A, B, tArc, delta);
|
|
8789
8886
|
position = [geoAnchor[0], geoAnchor[1]];
|
|
@@ -8797,13 +8894,13 @@ const findBestEdgePosition = ({ features, viewportBounds, zoom, minPixelWidth, l
|
|
|
8797
8894
|
const centerOffsetFactor = resolvedAnchor === "left" ? 1 : resolvedAnchor === "right" ? -1 : 0;
|
|
8798
8895
|
const centerRelX = anchorRelX + centerOffsetFactor * halfLabelPx * best.directionPx[0];
|
|
8799
8896
|
const centerRelY = anchorRelY + centerOffsetFactor * halfLabelPx * best.directionPx[1];
|
|
8800
|
-
const [approxCenterLng, approxCenterLat] = mercatorPxToLngLatWS(centerRelX + pxViewMin, centerRelY + pyViewMin, worldSize);
|
|
8897
|
+
const [approxCenterLng, approxCenterLat] = geoJsonUtils.mercatorPxToLngLatWS(centerRelX + pxViewMin, centerRelY + pyViewMin, worldSize);
|
|
8801
8898
|
const tArcCenter = Math.max(0, Math.min(1, reactMapAdapterShared.angularDistance(A[0], A[1], approxCenterLng, approxCenterLat) / delta));
|
|
8802
8899
|
const TANGENT_EPS = 0.001;
|
|
8803
8900
|
const p0 = reactMapAdapterShared.intermediatePoint(A, B, Math.max(0, tArcCenter - TANGENT_EPS), delta);
|
|
8804
8901
|
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);
|
|
8902
|
+
const [bx, by] = geoJsonUtils.lngLatToMercatorPxWS(p0[0], p0[1], worldSize);
|
|
8903
|
+
const [ax, ay] = geoJsonUtils.lngLatToMercatorPxWS(p1[0], p1[1], worldSize);
|
|
8807
8904
|
const ddx = ax - bx;
|
|
8808
8905
|
const ddy = ay - by;
|
|
8809
8906
|
const tangentLen = Math.sqrt(ddx * ddx + ddy * ddy);
|
|
@@ -8820,13 +8917,13 @@ const findBestEdgePosition = ({ features, viewportBounds, zoom, minPixelWidth, l
|
|
|
8820
8917
|
}
|
|
8821
8918
|
else {
|
|
8822
8919
|
// Co-located vertices — fall back to Mercator straight-line.
|
|
8823
|
-
const [anchorLng, anchorLat] = mercatorPxToLngLatWS(anchorRelX + pxViewMin, anchorRelY + pyViewMin, worldSize);
|
|
8920
|
+
const [anchorLng, anchorLat] = geoJsonUtils.mercatorPxToLngLatWS(anchorRelX + pxViewMin, anchorRelY + pyViewMin, worldSize);
|
|
8824
8921
|
position = [anchorLng, anchorLat];
|
|
8825
8922
|
resolvedDirectionPx = best.directionPx;
|
|
8826
8923
|
}
|
|
8827
8924
|
}
|
|
8828
8925
|
else {
|
|
8829
|
-
const [anchorLng, anchorLat] = mercatorPxToLngLatWS(anchorRelX + pxViewMin, anchorRelY + pyViewMin, worldSize);
|
|
8926
|
+
const [anchorLng, anchorLat] = geoJsonUtils.mercatorPxToLngLatWS(anchorRelX + pxViewMin, anchorRelY + pyViewMin, worldSize);
|
|
8830
8927
|
position = [anchorLng, anchorLat];
|
|
8831
8928
|
resolvedDirectionPx = best.directionPx;
|
|
8832
8929
|
}
|
|
@@ -9014,19 +9111,19 @@ const computeEdgeProperties = (start, end, geometry, zoom, tileSize, geodesic =
|
|
|
9014
9111
|
const TANGENT_EPS = 0.001;
|
|
9015
9112
|
const p0 = reactMapAdapterShared.intermediatePoint(start, end, 0.5 - TANGENT_EPS, delta);
|
|
9016
9113
|
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);
|
|
9114
|
+
const [bx, by] = geoJsonUtils.lngLatToWebMercatorPx(p0[0], p0[1], zoom, tileSize);
|
|
9115
|
+
const [ax, ay] = geoJsonUtils.lngLatToWebMercatorPx(p1[0], p1[1], zoom, tileSize);
|
|
9019
9116
|
angleDeg = normalizeReadableAngle(Math.atan2(ay - by, ax - bx) * RAD_TO_DEG);
|
|
9020
9117
|
}
|
|
9021
9118
|
else {
|
|
9022
|
-
const [startPx, startPy] = lngLatToWebMercatorPx(start[0], start[1], zoom, tileSize);
|
|
9023
|
-
const [endPx, endPy] = lngLatToWebMercatorPx(end[0], end[1], zoom, tileSize);
|
|
9119
|
+
const [startPx, startPy] = geoJsonUtils.lngLatToWebMercatorPx(start[0], start[1], zoom, tileSize);
|
|
9120
|
+
const [endPx, endPy] = geoJsonUtils.lngLatToWebMercatorPx(end[0], end[1], zoom, tileSize);
|
|
9024
9121
|
angleDeg = normalizeReadableAngle(Math.atan2(endPy - startPy, endPx - startPx) * RAD_TO_DEG);
|
|
9025
9122
|
}
|
|
9026
9123
|
}
|
|
9027
9124
|
else {
|
|
9028
|
-
const [startPx, startPy] = lngLatToWebMercatorPx(start[0], start[1], zoom, tileSize);
|
|
9029
|
-
const [endPx, endPy] = lngLatToWebMercatorPx(end[0], end[1], zoom, tileSize);
|
|
9125
|
+
const [startPx, startPy] = geoJsonUtils.lngLatToWebMercatorPx(start[0], start[1], zoom, tileSize);
|
|
9126
|
+
const [endPx, endPy] = geoJsonUtils.lngLatToWebMercatorPx(end[0], end[1], zoom, tileSize);
|
|
9030
9127
|
angleDeg = normalizeReadableAngle(Math.atan2(endPy - startPy, endPx - startPx) * RAD_TO_DEG);
|
|
9031
9128
|
}
|
|
9032
9129
|
const pixelLength = geoJsonUtils.edgePixelLength(start[0], start[1], end[0], end[1], zoom, midLat, tileSize);
|
|
@@ -11671,6 +11768,7 @@ exports.useControlStack = useControlStack;
|
|
|
11671
11768
|
exports.useControls = useControls;
|
|
11672
11769
|
exports.useDefaultControls = useDefaultControls;
|
|
11673
11770
|
exports.useDirectionIndicator = useDirectionIndicator;
|
|
11771
|
+
exports.useEntitiesNearCursor = useEntitiesNearCursor;
|
|
11674
11772
|
exports.useExpandedIds = useExpandedIds;
|
|
11675
11773
|
exports.useFitFeatureBounds = useFitFeatureBounds;
|
|
11676
11774
|
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
|
|
@@ -8110,7 +8226,6 @@ const DEG_TO_RAD = Math.PI / 180;
|
|
|
8110
8226
|
const RAD_TO_DEG$1 = 180 / Math.PI;
|
|
8111
8227
|
const DEFAULT_EDGE_LABEL_INSET_PX = 6;
|
|
8112
8228
|
const LABEL_HEIGHT_PX = 20;
|
|
8113
|
-
const MAX_WEB_MERCATOR_LAT = 85.05112878;
|
|
8114
8229
|
const EARTH_RADIUS_KM = 6371;
|
|
8115
8230
|
const GEODESIC_PLACEMENT_MAX_SEGMENT_RAD = GEODESIC_MAX_SEGMENT_KM / EARTH_RADIUS_KM;
|
|
8116
8231
|
/**
|
|
@@ -8170,24 +8285,6 @@ const clipSegmentToRect = (x0, y0, x1, y1, xMin, yMin, xMax, yMax) => {
|
|
|
8170
8285
|
}
|
|
8171
8286
|
return [x0 + tMin * dx, y0 + tMin * dy, x0 + tMax * dx, y0 + tMax * dy];
|
|
8172
8287
|
};
|
|
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
8288
|
/**
|
|
8192
8289
|
* Determine which side of an edge the geometry interior lies on, in screen space.
|
|
8193
8290
|
* Returns "above" if the label should extend above the edge (interior is below),
|
|
@@ -11589,4 +11686,4 @@ const mockMapApi = (overrides) => {
|
|
|
11589
11686
|
*/
|
|
11590
11687
|
setupLibraryTranslations();
|
|
11591
11688
|
|
|
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 };
|
|
11689
|
+
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.25",
|
|
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.36",
|
|
11
|
+
"@trackunit/css-class-variance-utilities": "1.13.40",
|
|
12
|
+
"@trackunit/react-form-components": "2.1.38",
|
|
13
|
+
"@trackunit/react-core-hooks": "1.17.49",
|
|
14
|
+
"@trackunit/geo-json-utils": "1.14.43",
|
|
15
|
+
"@trackunit/i18n-library-translation": "2.0.37",
|
|
16
|
+
"@trackunit/react-modal": "2.1.39",
|
|
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.24",
|
|
19
|
+
"@trackunit/react-map-color-utils": "0.0.9",
|
|
20
|
+
"@trackunit/ui-design-tokens": "1.13.40",
|
|
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),
|