@trackunit/react-map 0.2.187 → 0.2.190
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
CHANGED
|
@@ -6116,7 +6116,7 @@ const disperseSelect = (pool, count, seeds) => {
|
|
|
6116
6116
|
* antimeridian is handled correctly. Deterministic: ties break by `getId` ascending, so the result is
|
|
6117
6117
|
* independent of server return order. Returns a placement map keyed by id.
|
|
6118
6118
|
*/
|
|
6119
|
-
const buildLabelPlacement = ({ items, getId, getPosition, focus, bounds, footprint, degreesPerPixel, ceiling, tierACount, mustShowIds, previousIds, previousStickOffsets, }) => {
|
|
6119
|
+
const buildLabelPlacement = ({ items, getId, getPosition, focus, bounds, footprint, clusterPositions, clusterFootprint, degreesPerPixel, ceiling, tierACount, mustShowIds, previousIds, previousStickOffsets, }) => {
|
|
6120
6120
|
const placements = new Map();
|
|
6121
6121
|
const placed = [];
|
|
6122
6122
|
// Reference longitude for unwrapping: the view-box centre when clipping, else the lowest-id
|
|
@@ -6149,6 +6149,18 @@ const buildLabelPlacement = ({ items, getId, getPosition, focus, bounds, footpri
|
|
|
6149
6149
|
: [unwrapLng(bounds[0], referenceLng), bounds[1], unwrapLng(bounds[2], referenceLng), bounds[3]];
|
|
6150
6150
|
// Below this squared distance a point is inside every Tier-B footprint, so it always overlaps.
|
|
6151
6151
|
const minFootprintSq = Math.min(footprint.widthDeg, footprint.heightDeg) ** 2;
|
|
6152
|
+
// ---- Seed cluster bubbles as hard obstacles (SAGA-736) ----
|
|
6153
|
+
// Pushed into `placed` before any label, so every collision test (Tier-A anchor, stick candidates,
|
|
6154
|
+
// Tier-B anchor) already avoids clusters — no per-tier special-casing needed. Seeded as given, with
|
|
6155
|
+
// no view-box clip: a cluster whose centre is just off-screen still shows its bubble on-screen and can
|
|
6156
|
+
// be overlapped by an in-view edge label (anchor labels are not view-box clipped), so clipping by
|
|
6157
|
+
// centre would reopen that gap. Cluster counts are small, and a genuinely far cluster is simply too
|
|
6158
|
+
// distant to overlap any label. Unwrapped around the same reference longitude as items.
|
|
6159
|
+
if (clusterPositions !== undefined && clusterFootprint !== undefined) {
|
|
6160
|
+
for (const position of clusterPositions) {
|
|
6161
|
+
placed.push({ point: [unwrapLng(position[0], referenceLng), position[1]], footprint: clusterFootprint });
|
|
6162
|
+
}
|
|
6163
|
+
}
|
|
6152
6164
|
// ---- Tier A membership ----
|
|
6153
6165
|
const mustShow = mustShowIds ?? EMPTY_IDS;
|
|
6154
6166
|
// Must-show is guaranteed even if it matches no tier (e.g. a selected asset with null state).
|
|
@@ -6316,8 +6328,23 @@ const labelFootprintDeg = ({ zoom, latitudeDeg, paddingPx, labelWidthPx, }) => {
|
|
|
6316
6328
|
const perPixel = degreesPerPixel$1(zoom, latitudeDeg);
|
|
6317
6329
|
return { widthDeg: widthPx * perPixel.lng, heightDeg: heightPx * perPixel.lat };
|
|
6318
6330
|
};
|
|
6331
|
+
/**
|
|
6332
|
+
* Geo footprint for a circular on-screen obstacle (a cluster bubble or a marker pin) from its pixel
|
|
6333
|
+
* diameter, so labels can collision-avoid it. A square box of side `diameterPx` centred on the
|
|
6334
|
+
* obstacle — slightly conservative vs the inscribed circle, which is the safe side for a keep-out.
|
|
6335
|
+
*/
|
|
6336
|
+
const obstacleFootprintDeg = ({ zoom, latitudeDeg, diameterPx, }) => {
|
|
6337
|
+
const perPixel = degreesPerPixel$1(zoom, latitudeDeg);
|
|
6338
|
+
return { widthDeg: diameterPx * perPixel.lng, heightDeg: diameterPx * perPixel.lat };
|
|
6339
|
+
};
|
|
6319
6340
|
|
|
6320
6341
|
const EMPTY_PLACEMENTS = new Map();
|
|
6342
|
+
/**
|
|
6343
|
+
* Assumed cluster-bubble diameter (px) for the obstacle footprint — a conservative constant that
|
|
6344
|
+
* covers up to a 3-digit count. Slightly over-reserving is the safe side for a hard "never cover a
|
|
6345
|
+
* cluster" keep-out. Tunable in the browser.
|
|
6346
|
+
*/
|
|
6347
|
+
const CLUSTER_OBSTACLE_DIAMETER_PX = 44;
|
|
6321
6348
|
const samePlacement = (a, b) => {
|
|
6322
6349
|
if (a.mode === "anchor")
|
|
6323
6350
|
return b.mode === "anchor";
|
|
@@ -6361,7 +6388,7 @@ const stickOffsetsFrom = (placements) => {
|
|
|
6361
6388
|
* not on viewport change. Returns the previous map reference when content is unchanged so downstream
|
|
6362
6389
|
* `useMemo`/callbacks stay stable. Same render-phase-setState pattern as `useExpandedIds`.
|
|
6363
6390
|
*/
|
|
6364
|
-
const useLabelPlacement = ({ enabled, items, getId, getPosition, focus, bounds, zoom, paddingPx, labelWidthPx, ceiling, tierACount, mustShowIds, frozen, }) => {
|
|
6391
|
+
const useLabelPlacement = ({ enabled, items, getId, getPosition, focus, bounds, zoom, paddingPx, labelWidthPx, ceiling, tierACount, mustShowIds, clusterPositions, frozen, }) => {
|
|
6365
6392
|
const [snapshot, setSnapshot] = react.useState(() => ({
|
|
6366
6393
|
focusId: focus.id,
|
|
6367
6394
|
placements: EMPTY_PLACEMENTS,
|
|
@@ -6389,6 +6416,10 @@ const useLabelPlacement = ({ enabled, items, getId, getPosition, focus, bounds,
|
|
|
6389
6416
|
focus,
|
|
6390
6417
|
bounds,
|
|
6391
6418
|
footprint,
|
|
6419
|
+
clusterPositions,
|
|
6420
|
+
clusterFootprint: clusterPositions === undefined
|
|
6421
|
+
? undefined
|
|
6422
|
+
: obstacleFootprintDeg({ zoom, latitudeDeg, diameterPx: CLUSTER_OBSTACLE_DIAMETER_PX }),
|
|
6392
6423
|
degreesPerPixel: degreesPerPixel$1(zoom, latitudeDeg),
|
|
6393
6424
|
ceiling,
|
|
6394
6425
|
tierACount,
|
|
@@ -6411,6 +6442,7 @@ const useLabelPlacement = ({ enabled, items, getId, getPosition, focus, bounds,
|
|
|
6411
6442
|
ceiling,
|
|
6412
6443
|
tierACount,
|
|
6413
6444
|
mustShowIds,
|
|
6445
|
+
clusterPositions,
|
|
6414
6446
|
prev,
|
|
6415
6447
|
]);
|
|
6416
6448
|
// Return the previous reference when content is identical — keeps downstream caches stable.
|
package/index.esm.js
CHANGED
|
@@ -6115,7 +6115,7 @@ const disperseSelect = (pool, count, seeds) => {
|
|
|
6115
6115
|
* antimeridian is handled correctly. Deterministic: ties break by `getId` ascending, so the result is
|
|
6116
6116
|
* independent of server return order. Returns a placement map keyed by id.
|
|
6117
6117
|
*/
|
|
6118
|
-
const buildLabelPlacement = ({ items, getId, getPosition, focus, bounds, footprint, degreesPerPixel, ceiling, tierACount, mustShowIds, previousIds, previousStickOffsets, }) => {
|
|
6118
|
+
const buildLabelPlacement = ({ items, getId, getPosition, focus, bounds, footprint, clusterPositions, clusterFootprint, degreesPerPixel, ceiling, tierACount, mustShowIds, previousIds, previousStickOffsets, }) => {
|
|
6119
6119
|
const placements = new Map();
|
|
6120
6120
|
const placed = [];
|
|
6121
6121
|
// Reference longitude for unwrapping: the view-box centre when clipping, else the lowest-id
|
|
@@ -6148,6 +6148,18 @@ const buildLabelPlacement = ({ items, getId, getPosition, focus, bounds, footpri
|
|
|
6148
6148
|
: [unwrapLng(bounds[0], referenceLng), bounds[1], unwrapLng(bounds[2], referenceLng), bounds[3]];
|
|
6149
6149
|
// Below this squared distance a point is inside every Tier-B footprint, so it always overlaps.
|
|
6150
6150
|
const minFootprintSq = Math.min(footprint.widthDeg, footprint.heightDeg) ** 2;
|
|
6151
|
+
// ---- Seed cluster bubbles as hard obstacles (SAGA-736) ----
|
|
6152
|
+
// Pushed into `placed` before any label, so every collision test (Tier-A anchor, stick candidates,
|
|
6153
|
+
// Tier-B anchor) already avoids clusters — no per-tier special-casing needed. Seeded as given, with
|
|
6154
|
+
// no view-box clip: a cluster whose centre is just off-screen still shows its bubble on-screen and can
|
|
6155
|
+
// be overlapped by an in-view edge label (anchor labels are not view-box clipped), so clipping by
|
|
6156
|
+
// centre would reopen that gap. Cluster counts are small, and a genuinely far cluster is simply too
|
|
6157
|
+
// distant to overlap any label. Unwrapped around the same reference longitude as items.
|
|
6158
|
+
if (clusterPositions !== undefined && clusterFootprint !== undefined) {
|
|
6159
|
+
for (const position of clusterPositions) {
|
|
6160
|
+
placed.push({ point: [unwrapLng(position[0], referenceLng), position[1]], footprint: clusterFootprint });
|
|
6161
|
+
}
|
|
6162
|
+
}
|
|
6151
6163
|
// ---- Tier A membership ----
|
|
6152
6164
|
const mustShow = mustShowIds ?? EMPTY_IDS;
|
|
6153
6165
|
// Must-show is guaranteed even if it matches no tier (e.g. a selected asset with null state).
|
|
@@ -6315,8 +6327,23 @@ const labelFootprintDeg = ({ zoom, latitudeDeg, paddingPx, labelWidthPx, }) => {
|
|
|
6315
6327
|
const perPixel = degreesPerPixel$1(zoom, latitudeDeg);
|
|
6316
6328
|
return { widthDeg: widthPx * perPixel.lng, heightDeg: heightPx * perPixel.lat };
|
|
6317
6329
|
};
|
|
6330
|
+
/**
|
|
6331
|
+
* Geo footprint for a circular on-screen obstacle (a cluster bubble or a marker pin) from its pixel
|
|
6332
|
+
* diameter, so labels can collision-avoid it. A square box of side `diameterPx` centred on the
|
|
6333
|
+
* obstacle — slightly conservative vs the inscribed circle, which is the safe side for a keep-out.
|
|
6334
|
+
*/
|
|
6335
|
+
const obstacleFootprintDeg = ({ zoom, latitudeDeg, diameterPx, }) => {
|
|
6336
|
+
const perPixel = degreesPerPixel$1(zoom, latitudeDeg);
|
|
6337
|
+
return { widthDeg: diameterPx * perPixel.lng, heightDeg: diameterPx * perPixel.lat };
|
|
6338
|
+
};
|
|
6318
6339
|
|
|
6319
6340
|
const EMPTY_PLACEMENTS = new Map();
|
|
6341
|
+
/**
|
|
6342
|
+
* Assumed cluster-bubble diameter (px) for the obstacle footprint — a conservative constant that
|
|
6343
|
+
* covers up to a 3-digit count. Slightly over-reserving is the safe side for a hard "never cover a
|
|
6344
|
+
* cluster" keep-out. Tunable in the browser.
|
|
6345
|
+
*/
|
|
6346
|
+
const CLUSTER_OBSTACLE_DIAMETER_PX = 44;
|
|
6320
6347
|
const samePlacement = (a, b) => {
|
|
6321
6348
|
if (a.mode === "anchor")
|
|
6322
6349
|
return b.mode === "anchor";
|
|
@@ -6360,7 +6387,7 @@ const stickOffsetsFrom = (placements) => {
|
|
|
6360
6387
|
* not on viewport change. Returns the previous map reference when content is unchanged so downstream
|
|
6361
6388
|
* `useMemo`/callbacks stay stable. Same render-phase-setState pattern as `useExpandedIds`.
|
|
6362
6389
|
*/
|
|
6363
|
-
const useLabelPlacement = ({ enabled, items, getId, getPosition, focus, bounds, zoom, paddingPx, labelWidthPx, ceiling, tierACount, mustShowIds, frozen, }) => {
|
|
6390
|
+
const useLabelPlacement = ({ enabled, items, getId, getPosition, focus, bounds, zoom, paddingPx, labelWidthPx, ceiling, tierACount, mustShowIds, clusterPositions, frozen, }) => {
|
|
6364
6391
|
const [snapshot, setSnapshot] = useState(() => ({
|
|
6365
6392
|
focusId: focus.id,
|
|
6366
6393
|
placements: EMPTY_PLACEMENTS,
|
|
@@ -6388,6 +6415,10 @@ const useLabelPlacement = ({ enabled, items, getId, getPosition, focus, bounds,
|
|
|
6388
6415
|
focus,
|
|
6389
6416
|
bounds,
|
|
6390
6417
|
footprint,
|
|
6418
|
+
clusterPositions,
|
|
6419
|
+
clusterFootprint: clusterPositions === undefined
|
|
6420
|
+
? undefined
|
|
6421
|
+
: obstacleFootprintDeg({ zoom, latitudeDeg, diameterPx: CLUSTER_OBSTACLE_DIAMETER_PX }),
|
|
6391
6422
|
degreesPerPixel: degreesPerPixel$1(zoom, latitudeDeg),
|
|
6392
6423
|
ceiling,
|
|
6393
6424
|
tierACount,
|
|
@@ -6410,6 +6441,7 @@ const useLabelPlacement = ({ enabled, items, getId, getPosition, focus, bounds,
|
|
|
6410
6441
|
ceiling,
|
|
6411
6442
|
tierACount,
|
|
6412
6443
|
mustShowIds,
|
|
6444
|
+
clusterPositions,
|
|
6413
6445
|
prev,
|
|
6414
6446
|
]);
|
|
6415
6447
|
// Return the previous reference when content is identical — keeps downstream caches stable.
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"entry.js","sourceRoot":"","sources":["../../../../../libs/react/map/migrations/entry.ts"],"names":[],"mappings":"","sourcesContent":["export {};\n"]}
|
package/package.json
CHANGED
|
@@ -1,25 +1,25 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@trackunit/react-map",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.190",
|
|
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": "3.5.
|
|
11
|
-
"@trackunit/css-class-variance-utilities": "2.2.
|
|
12
|
-
"@trackunit/react-form-components": "2.9.
|
|
13
|
-
"@trackunit/react-core-hooks": "1.24.
|
|
14
|
-
"@trackunit/geo-json-utils": "1.17.
|
|
15
|
-
"@trackunit/i18n-library-translation": "2.7.
|
|
10
|
+
"@trackunit/react-components": "3.5.2",
|
|
11
|
+
"@trackunit/css-class-variance-utilities": "2.2.2",
|
|
12
|
+
"@trackunit/react-form-components": "2.9.2",
|
|
13
|
+
"@trackunit/react-core-hooks": "1.24.2",
|
|
14
|
+
"@trackunit/geo-json-utils": "1.17.2",
|
|
15
|
+
"@trackunit/i18n-library-translation": "2.7.2",
|
|
16
16
|
"react-minimal-pie-chart": "^8.4.0",
|
|
17
|
-
"@trackunit/react-map-adapter-shared": "0.0.
|
|
18
|
-
"@trackunit/react-map-color-utils": "0.0.
|
|
19
|
-
"@trackunit/shared-utils": "1.18.
|
|
20
|
-
"@trackunit/ui-design-tokens": "1.17.
|
|
17
|
+
"@trackunit/react-map-adapter-shared": "0.0.149",
|
|
18
|
+
"@trackunit/react-map-color-utils": "0.0.124",
|
|
19
|
+
"@trackunit/shared-utils": "1.18.2",
|
|
20
|
+
"@trackunit/ui-design-tokens": "1.17.2",
|
|
21
21
|
"@floating-ui/react": "^0.26.25",
|
|
22
|
-
"es-toolkit": "^1.
|
|
22
|
+
"es-toolkit": "^1.52.0",
|
|
23
23
|
"tailwind-merge": "^2.0.0",
|
|
24
24
|
"zod": "^3.25.76"
|
|
25
25
|
},
|
|
@@ -21,6 +21,17 @@ export type BuildLabelPlacementParams<TAsset> = Readonly<{
|
|
|
21
21
|
bounds: Readonly<GeoJsonBbox> | null;
|
|
22
22
|
/** Geo footprint every label occupies for collision (may be narrowed for density). */
|
|
23
23
|
footprint: LabelFootprintDeg;
|
|
24
|
+
/**
|
|
25
|
+
* Cluster bubble positions (raw lng/lat), seeded as **hard** obstacles so no label of any tier is
|
|
26
|
+
* placed over a cluster. Unwrapped like items and seeded as given — the caller passes the in-view /
|
|
27
|
+
* near-view set; not re-clipped here, so an edge cluster whose bubble is partly on-screen is still
|
|
28
|
+
* avoided (clipping by centre would let an in-view edge label cover it). Marker pins are deliberately
|
|
29
|
+
* NOT obstacles — routing labels around a dense pin field is self-defeating and not the industry norm
|
|
30
|
+
* (deck.gl collision groups / Maplex soft weight ignore point symbols); see SAGA-736.
|
|
31
|
+
*/
|
|
32
|
+
clusterPositions?: ReadonlyArray<Position>;
|
|
33
|
+
/** Geo footprint each cluster obstacle occupies (bubble size). Required when `clusterPositions` is set. */
|
|
34
|
+
clusterFootprint?: LabelFootprintDeg;
|
|
24
35
|
/** Degrees per screen pixel at the current zoom/latitude — maps the px stick ring into geo. */
|
|
25
36
|
degreesPerPixel: DegreesPerPixel;
|
|
26
37
|
/**
|
|
@@ -56,5 +67,5 @@ export type BuildLabelPlacementParams<TAsset> = Readonly<{
|
|
|
56
67
|
* antimeridian is handled correctly. Deterministic: ties break by `getId` ascending, so the result is
|
|
57
68
|
* independent of server return order. Returns a placement map keyed by id.
|
|
58
69
|
*/
|
|
59
|
-
export declare const buildLabelPlacement: <TAsset>({ items, getId, getPosition, focus, bounds, footprint, degreesPerPixel, ceiling, tierACount, mustShowIds, previousIds, previousStickOffsets, }: BuildLabelPlacementParams<TAsset>) => ReadonlyMap<string, LabelPlacement>;
|
|
70
|
+
export declare const buildLabelPlacement: <TAsset>({ items, getId, getPosition, focus, bounds, footprint, clusterPositions, clusterFootprint, degreesPerPixel, ceiling, tierACount, mustShowIds, previousIds, previousStickOffsets, }: BuildLabelPlacementParams<TAsset>) => ReadonlyMap<string, LabelPlacement>;
|
|
60
71
|
export {};
|
|
@@ -42,3 +42,13 @@ export type LabelFootprintParams = Readonly<{
|
|
|
42
42
|
* is otherwise tuned via `paddingPx`.
|
|
43
43
|
*/
|
|
44
44
|
export declare const labelFootprintDeg: ({ zoom, latitudeDeg, paddingPx, labelWidthPx, }: LabelFootprintParams) => LabelFootprintDeg;
|
|
45
|
+
/**
|
|
46
|
+
* Geo footprint for a circular on-screen obstacle (a cluster bubble or a marker pin) from its pixel
|
|
47
|
+
* diameter, so labels can collision-avoid it. A square box of side `diameterPx` centred on the
|
|
48
|
+
* obstacle — slightly conservative vs the inscribed circle, which is the safe side for a keep-out.
|
|
49
|
+
*/
|
|
50
|
+
export declare const obstacleFootprintDeg: ({ zoom, latitudeDeg, diameterPx, }: Readonly<{
|
|
51
|
+
zoom: number;
|
|
52
|
+
latitudeDeg: number;
|
|
53
|
+
diameterPx: number;
|
|
54
|
+
}>) => LabelFootprintDeg;
|
|
@@ -27,6 +27,8 @@ export type UseLabelPlacementParams<TAsset> = Readonly<{
|
|
|
27
27
|
tierACount: number;
|
|
28
28
|
/** Always-guaranteed ids (selected/hovered/proximity) — Tier A regardless of priority rank. */
|
|
29
29
|
mustShowIds?: ReadonlySet<string>;
|
|
30
|
+
/** Cluster bubble positions (raw lng/lat) — seeded as hard obstacles so labels never cover a cluster. */
|
|
31
|
+
clusterPositions?: ReadonlyArray<Position>;
|
|
30
32
|
/**
|
|
31
33
|
* When true, hold the last placement unchanged instead of recomputing — for use while a viewport
|
|
32
34
|
* refetch is in flight, so labels resettle once (when data lands) rather than blinking as the
|
|
@@ -45,5 +47,5 @@ export type UseLabelPlacementParams<TAsset> = Readonly<{
|
|
|
45
47
|
* not on viewport change. Returns the previous map reference when content is unchanged so downstream
|
|
46
48
|
* `useMemo`/callbacks stay stable. Same render-phase-setState pattern as `useExpandedIds`.
|
|
47
49
|
*/
|
|
48
|
-
export declare const useLabelPlacement: <TAsset>({ enabled, items, getId, getPosition, focus, bounds, zoom, paddingPx, labelWidthPx, ceiling, tierACount, mustShowIds, frozen, }: UseLabelPlacementParams<TAsset>) => ReadonlyMap<string, LabelPlacement>;
|
|
50
|
+
export declare const useLabelPlacement: <TAsset>({ enabled, items, getId, getPosition, focus, bounds, zoom, paddingPx, labelWidthPx, ceiling, tierACount, mustShowIds, clusterPositions, frozen, }: UseLabelPlacementParams<TAsset>) => ReadonlyMap<string, LabelPlacement>;
|
|
49
51
|
export {};
|