@trackunit/react-map 0.2.86 → 0.2.88
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
|
@@ -7757,9 +7757,16 @@ const shapesUnderCursor = (position, features, options) => {
|
|
|
7757
7757
|
/**
|
|
7758
7758
|
* Connected components of features that share fill area. Bbox prefilter then
|
|
7759
7759
|
* exact polygon intersection, mirroring `countOverlappingShapes`.
|
|
7760
|
+
*
|
|
7761
|
+
* `getGeoJsonPolygonIntersection` delegates to `polygon-clipping`, which can throw
|
|
7762
|
+
* `"Unable to complete output ring"` for degenerate but schema-valid geometry
|
|
7763
|
+
* (SAGA-743). A throw here is treated as "no overlap" — the safe degradation for
|
|
7764
|
+
* grouping — and recorded in `intersectionFailures` so the hook can report it
|
|
7765
|
+
* (mirrors how `droppedInvalidFeatures` surfaces the sibling failure mode).
|
|
7760
7766
|
*/
|
|
7761
7767
|
const buildOverlapGroups = (records) => {
|
|
7762
7768
|
const parent = records.map((_, index) => index);
|
|
7769
|
+
const intersectionFailures = [];
|
|
7763
7770
|
const find = (index) => {
|
|
7764
7771
|
let root = index;
|
|
7765
7772
|
while (parent[root] !== root)
|
|
@@ -7783,7 +7790,17 @@ const buildOverlapGroups = (records) => {
|
|
|
7783
7790
|
continue;
|
|
7784
7791
|
if (!bboxesIntersect$1(a.bbox, b.bbox))
|
|
7785
7792
|
continue;
|
|
7786
|
-
|
|
7793
|
+
let overlaps;
|
|
7794
|
+
try {
|
|
7795
|
+
overlaps = geoJsonUtils.getGeoJsonPolygonIntersection(a.geometry, b.geometry) !== null;
|
|
7796
|
+
}
|
|
7797
|
+
catch (error) {
|
|
7798
|
+
// polygon-clipping failed on this pair. Treat as no overlap (do not union)
|
|
7799
|
+
// so tiling degrades to full fills rather than crashing the map.
|
|
7800
|
+
intersectionFailures.push({ featureIdA: a.id, featureIdB: b.id, error });
|
|
7801
|
+
continue;
|
|
7802
|
+
}
|
|
7803
|
+
if (overlaps) {
|
|
7787
7804
|
union(i, j);
|
|
7788
7805
|
}
|
|
7789
7806
|
}
|
|
@@ -7799,7 +7816,7 @@ const buildOverlapGroups = (records) => {
|
|
|
7799
7816
|
group.push(record);
|
|
7800
7817
|
}
|
|
7801
7818
|
});
|
|
7802
|
-
return [...groups.values()].filter(group => group.length >= 2);
|
|
7819
|
+
return { groups: [...groups.values()].filter(group => group.length >= 2), intersectionFailures };
|
|
7803
7820
|
};
|
|
7804
7821
|
const buildContendedShapes = (group) => group.map(record => {
|
|
7805
7822
|
const containedIn = [];
|
|
@@ -7943,7 +7960,12 @@ const computeFillTiling = (input) => {
|
|
|
7943
7960
|
const fillGeometries = new Map();
|
|
7944
7961
|
const featureToGroupKey = new Map();
|
|
7945
7962
|
const featureZIndex = new Map();
|
|
7946
|
-
const overlapGroups = buildOverlapGroups(records);
|
|
7963
|
+
const { groups: overlapGroups, intersectionFailures } = buildOverlapGroups(records);
|
|
7964
|
+
if (intersectionFailures.length > 0) {
|
|
7965
|
+
const pairList = intersectionFailures.map(failure => `${failure.featureIdA}↔${failure.featureIdB}`).join(", ");
|
|
7966
|
+
// eslint-disable-next-line no-console -- Intentional: warn devs when polygon-clipping fails on schema-valid geometry (SAGA-743); the pair is treated as non-overlapping and full fills still render
|
|
7967
|
+
console.warn(`[computeFillTiling] polygon-clipping failed on ${intersectionFailures.length} feature pair(s); treated as non-overlapping (full fill still renders): ${pairList}`);
|
|
7968
|
+
}
|
|
7947
7969
|
for (const group of overlapGroups) {
|
|
7948
7970
|
const contended = buildContendedShapes(group);
|
|
7949
7971
|
const recordById = new Map(group.map(record => [record.id, record]));
|
|
@@ -8018,6 +8040,7 @@ const computeFillTiling = (input) => {
|
|
|
8018
8040
|
featureToGroupKey,
|
|
8019
8041
|
featureZIndex,
|
|
8020
8042
|
droppedInvalidFeatures,
|
|
8043
|
+
intersectionFailures,
|
|
8021
8044
|
};
|
|
8022
8045
|
};
|
|
8023
8046
|
|
|
@@ -8323,6 +8346,31 @@ const buildShapeLabelResolutionContext = (feature, args) => {
|
|
|
8323
8346
|
};
|
|
8324
8347
|
};
|
|
8325
8348
|
|
|
8349
|
+
/** Stable, order-independent key so the pair (a,b) dedupes to the same entry as (b,a). */
|
|
8350
|
+
const resolveIntersectionFailureDedupeKey = (layerHandleId, failure) => {
|
|
8351
|
+
const [first, second] = [failure.featureIdA, failure.featureIdB].sort();
|
|
8352
|
+
return `${layerHandleId}:${first}|${second}`;
|
|
8353
|
+
};
|
|
8354
|
+
/**
|
|
8355
|
+
* Report to Sentry each feature pair whose overlap test threw inside `polygon-clipping`
|
|
8356
|
+
* (SAGA-743), at most once per handle/pair per session.
|
|
8357
|
+
* Note: `computeFillTiling` emits `console.warn` separately.
|
|
8358
|
+
*/
|
|
8359
|
+
const captureFillTilingIntersectionFailures = (intersectionFailures, layerHandleId, sessionReportedDedupeKeys, report) => {
|
|
8360
|
+
for (const failure of intersectionFailures) {
|
|
8361
|
+
const key = resolveIntersectionFailureDedupeKey(layerHandleId, failure);
|
|
8362
|
+
if (sessionReportedDedupeKeys.has(key))
|
|
8363
|
+
continue;
|
|
8364
|
+
sessionReportedDedupeKeys.add(key);
|
|
8365
|
+
report({
|
|
8366
|
+
layerHandleId,
|
|
8367
|
+
featureIdA: failure.featureIdA,
|
|
8368
|
+
featureIdB: failure.featureIdB,
|
|
8369
|
+
message: failure.error instanceof Error ? failure.error.message : String(failure.error),
|
|
8370
|
+
});
|
|
8371
|
+
}
|
|
8372
|
+
};
|
|
8373
|
+
|
|
8326
8374
|
/**
|
|
8327
8375
|
* Extract ALL vertex coordinates from a geometry, walking every part of
|
|
8328
8376
|
* multi-geometries. Returns a flat array of [lng, lat] positions.
|
|
@@ -9577,6 +9625,7 @@ const bboxesIntersect = (a, b) => a[2] >= b[0] && a[0] <= b[2] && a[3] >= b[1] &
|
|
|
9577
9625
|
*/
|
|
9578
9626
|
const buildOverlappingShapesCountMap = (features, featureBboxes, viewportBounds) => {
|
|
9579
9627
|
const counts = new Map();
|
|
9628
|
+
const intersectionFailures = [];
|
|
9580
9629
|
for (const feature of features) {
|
|
9581
9630
|
counts.set(feature, 0);
|
|
9582
9631
|
}
|
|
@@ -9605,7 +9654,21 @@ const buildOverlappingShapesCountMap = (features, featureBboxes, viewportBounds)
|
|
|
9605
9654
|
const geomB = featureB.geometry;
|
|
9606
9655
|
const isPolygonalB = geomB !== null && (geomB.type === "Polygon" || geomB.type === "MultiPolygon");
|
|
9607
9656
|
if (isPolygonalA && isPolygonalB) {
|
|
9608
|
-
|
|
9657
|
+
let overlaps;
|
|
9658
|
+
try {
|
|
9659
|
+
overlaps = geoJsonUtils.getGeoJsonPolygonIntersection(geomA, geomB) !== null;
|
|
9660
|
+
}
|
|
9661
|
+
catch (error) {
|
|
9662
|
+
// polygon-clipping failed on this pair (SAGA-743). Treat as no overlap so
|
|
9663
|
+
// decoration counts degrade gracefully instead of crashing the map.
|
|
9664
|
+
intersectionFailures.push({
|
|
9665
|
+
featureIdA: featureA.id !== undefined ? String(featureA.id) : `index:${i}`,
|
|
9666
|
+
featureIdB: featureB.id !== undefined ? String(featureB.id) : `index:${j}`,
|
|
9667
|
+
error,
|
|
9668
|
+
});
|
|
9669
|
+
continue;
|
|
9670
|
+
}
|
|
9671
|
+
if (overlaps) {
|
|
9609
9672
|
if (geoJsonUtils.isFullyContainedInGeoJsonGeometry(geomA, geomB) !== true) {
|
|
9610
9673
|
counts.set(featureA, (counts.get(featureA) ?? 0) + 1);
|
|
9611
9674
|
}
|
|
@@ -9620,7 +9683,7 @@ const buildOverlappingShapesCountMap = (features, featureBboxes, viewportBounds)
|
|
|
9620
9683
|
}
|
|
9621
9684
|
}
|
|
9622
9685
|
}
|
|
9623
|
-
return counts;
|
|
9686
|
+
return { counts, intersectionFailures };
|
|
9624
9687
|
};
|
|
9625
9688
|
/**
|
|
9626
9689
|
* Value-based equality for the nested style overrides maps. Used to skip
|
|
@@ -9706,6 +9769,25 @@ const useShapeDecorations = ({ onDecorationsChange, onStyleOverridesChange, api,
|
|
|
9706
9769
|
const previousLayoutSides = react.useRef(new Map());
|
|
9707
9770
|
const prevStyleOverridesRef = react.useRef(new Map());
|
|
9708
9771
|
const prevDecorationLayersRef = react.useRef([]);
|
|
9772
|
+
const errorHandler = reactCoreHooks.useErrorHandlerOrNull();
|
|
9773
|
+
// Session-level dedupe so a recurring polygon-clipping failure (SAGA-743) reports
|
|
9774
|
+
// to Sentry at most once per handle/pair per session, not on every pan frame.
|
|
9775
|
+
const sessionReportedIntersectionKeysRef = react.useRef(new Set());
|
|
9776
|
+
const reportOverlapIntersectionFailures = react.useCallback((intersectionFailures, layerHandleId) => {
|
|
9777
|
+
if (intersectionFailures.length === 0)
|
|
9778
|
+
return;
|
|
9779
|
+
const pairList = intersectionFailures.map(failure => `${failure.featureIdA}↔${failure.featureIdB}`).join(", ");
|
|
9780
|
+
// eslint-disable-next-line no-console -- Intentional: warn devs when polygon-clipping fails on schema-valid geometry (SAGA-743); the pair is treated as non-overlapping and decorations still render
|
|
9781
|
+
console.warn(`[useShapeDecorations] polygon-clipping failed on ${intersectionFailures.length} feature pair(s); treated as non-overlapping: ${pairList}`);
|
|
9782
|
+
if (errorHandler === null)
|
|
9783
|
+
return;
|
|
9784
|
+
captureFillTilingIntersectionFailures(intersectionFailures, layerHandleId, sessionReportedIntersectionKeysRef.current, payload => {
|
|
9785
|
+
errorHandler.captureException(new Error(`polygon-clipping failed during decoration overlap counting: ${JSON.stringify(payload)}`), {
|
|
9786
|
+
level: "warning",
|
|
9787
|
+
fingerprint: ["react-map", "fill-tiling", "intersection-failure"],
|
|
9788
|
+
});
|
|
9789
|
+
});
|
|
9790
|
+
}, [errorHandler]);
|
|
9709
9791
|
/**
|
|
9710
9792
|
* Labels that just transitioned from absent (hidden) to placed. While in
|
|
9711
9793
|
* this set, edge identity is NOT stored — the algorithm gets one extra
|
|
@@ -9765,7 +9847,9 @@ const useShapeDecorations = ({ onDecorationsChange, onStyleOverridesChange, api,
|
|
|
9765
9847
|
let overlappingCountsCache = null;
|
|
9766
9848
|
const getOverlappingCount = (feature) => {
|
|
9767
9849
|
if (overlappingCountsCache === null) {
|
|
9768
|
-
|
|
9850
|
+
const { counts, intersectionFailures } = buildOverlappingShapesCountMap(handle.features.features, featureBboxes, bounds);
|
|
9851
|
+
overlappingCountsCache = counts;
|
|
9852
|
+
reportOverlapIntersectionFailures(intersectionFailures, handle.id);
|
|
9769
9853
|
}
|
|
9770
9854
|
return overlappingCountsCache.get(feature) ?? 0;
|
|
9771
9855
|
};
|
|
@@ -10026,6 +10110,7 @@ const useShapeDecorations = ({ onDecorationsChange, onStyleOverridesChange, api,
|
|
|
10026
10110
|
hasSourceReadiness,
|
|
10027
10111
|
renderedSourceIds,
|
|
10028
10112
|
isReady,
|
|
10113
|
+
reportOverlapIntersectionFailures,
|
|
10029
10114
|
]);
|
|
10030
10115
|
const updateDecorationsRef = react.useRef(updateDecorations);
|
|
10031
10116
|
react.useEffect(() => {
|
|
@@ -10558,7 +10643,7 @@ const useShapeFillTiling = ({ api, handles, onFillGeometriesChange, onZIndexOver
|
|
|
10558
10643
|
const nextRestingFills = new Map();
|
|
10559
10644
|
const nextRestingZIndices = new Map();
|
|
10560
10645
|
for (const handle of currentHandles) {
|
|
10561
|
-
const { fillGeometries, featureToGroupKey, featureZIndex, droppedInvalidFeatures } = computeFillTiling({
|
|
10646
|
+
const { fillGeometries, featureToGroupKey, featureZIndex, droppedInvalidFeatures, intersectionFailures } = computeFillTiling({
|
|
10562
10647
|
features: handle.features.features,
|
|
10563
10648
|
viewportBounds: GLOBAL_BOUNDS,
|
|
10564
10649
|
resolveStackOrder: handle.overlap?.resolveStackOrder ?? defaultShapeStackOrder,
|
|
@@ -10574,6 +10659,14 @@ const useShapeFillTiling = ({ api, handles, onFillGeometriesChange, onZIndexOver
|
|
|
10574
10659
|
});
|
|
10575
10660
|
});
|
|
10576
10661
|
}
|
|
10662
|
+
if (errorHandler !== null && intersectionFailures.length > 0) {
|
|
10663
|
+
captureFillTilingIntersectionFailures(intersectionFailures, handle.id, sessionReportedDedupeKeysRef.current, payload => {
|
|
10664
|
+
errorHandler.captureException(new Error(`polygon-clipping failed during fill-tiling overlap grouping: ${JSON.stringify(payload)}`), {
|
|
10665
|
+
level: "warning",
|
|
10666
|
+
fingerprint: ["react-map", "fill-tiling", "intersection-failure"],
|
|
10667
|
+
});
|
|
10668
|
+
});
|
|
10669
|
+
}
|
|
10577
10670
|
featureToGroupKeyByHandleRef.current.set(handle.id, new Map(featureToGroupKey));
|
|
10578
10671
|
if (fillGeometries.size > 0) {
|
|
10579
10672
|
nextRestingFills.set(handle.id, fillGeometries);
|
package/index.esm.js
CHANGED
|
@@ -7756,9 +7756,16 @@ const shapesUnderCursor = (position, features, options) => {
|
|
|
7756
7756
|
/**
|
|
7757
7757
|
* Connected components of features that share fill area. Bbox prefilter then
|
|
7758
7758
|
* exact polygon intersection, mirroring `countOverlappingShapes`.
|
|
7759
|
+
*
|
|
7760
|
+
* `getGeoJsonPolygonIntersection` delegates to `polygon-clipping`, which can throw
|
|
7761
|
+
* `"Unable to complete output ring"` for degenerate but schema-valid geometry
|
|
7762
|
+
* (SAGA-743). A throw here is treated as "no overlap" — the safe degradation for
|
|
7763
|
+
* grouping — and recorded in `intersectionFailures` so the hook can report it
|
|
7764
|
+
* (mirrors how `droppedInvalidFeatures` surfaces the sibling failure mode).
|
|
7759
7765
|
*/
|
|
7760
7766
|
const buildOverlapGroups = (records) => {
|
|
7761
7767
|
const parent = records.map((_, index) => index);
|
|
7768
|
+
const intersectionFailures = [];
|
|
7762
7769
|
const find = (index) => {
|
|
7763
7770
|
let root = index;
|
|
7764
7771
|
while (parent[root] !== root)
|
|
@@ -7782,7 +7789,17 @@ const buildOverlapGroups = (records) => {
|
|
|
7782
7789
|
continue;
|
|
7783
7790
|
if (!bboxesIntersect$1(a.bbox, b.bbox))
|
|
7784
7791
|
continue;
|
|
7785
|
-
|
|
7792
|
+
let overlaps;
|
|
7793
|
+
try {
|
|
7794
|
+
overlaps = getGeoJsonPolygonIntersection(a.geometry, b.geometry) !== null;
|
|
7795
|
+
}
|
|
7796
|
+
catch (error) {
|
|
7797
|
+
// polygon-clipping failed on this pair. Treat as no overlap (do not union)
|
|
7798
|
+
// so tiling degrades to full fills rather than crashing the map.
|
|
7799
|
+
intersectionFailures.push({ featureIdA: a.id, featureIdB: b.id, error });
|
|
7800
|
+
continue;
|
|
7801
|
+
}
|
|
7802
|
+
if (overlaps) {
|
|
7786
7803
|
union(i, j);
|
|
7787
7804
|
}
|
|
7788
7805
|
}
|
|
@@ -7798,7 +7815,7 @@ const buildOverlapGroups = (records) => {
|
|
|
7798
7815
|
group.push(record);
|
|
7799
7816
|
}
|
|
7800
7817
|
});
|
|
7801
|
-
return [...groups.values()].filter(group => group.length >= 2);
|
|
7818
|
+
return { groups: [...groups.values()].filter(group => group.length >= 2), intersectionFailures };
|
|
7802
7819
|
};
|
|
7803
7820
|
const buildContendedShapes = (group) => group.map(record => {
|
|
7804
7821
|
const containedIn = [];
|
|
@@ -7942,7 +7959,12 @@ const computeFillTiling = (input) => {
|
|
|
7942
7959
|
const fillGeometries = new Map();
|
|
7943
7960
|
const featureToGroupKey = new Map();
|
|
7944
7961
|
const featureZIndex = new Map();
|
|
7945
|
-
const overlapGroups = buildOverlapGroups(records);
|
|
7962
|
+
const { groups: overlapGroups, intersectionFailures } = buildOverlapGroups(records);
|
|
7963
|
+
if (intersectionFailures.length > 0) {
|
|
7964
|
+
const pairList = intersectionFailures.map(failure => `${failure.featureIdA}↔${failure.featureIdB}`).join(", ");
|
|
7965
|
+
// eslint-disable-next-line no-console -- Intentional: warn devs when polygon-clipping fails on schema-valid geometry (SAGA-743); the pair is treated as non-overlapping and full fills still render
|
|
7966
|
+
console.warn(`[computeFillTiling] polygon-clipping failed on ${intersectionFailures.length} feature pair(s); treated as non-overlapping (full fill still renders): ${pairList}`);
|
|
7967
|
+
}
|
|
7946
7968
|
for (const group of overlapGroups) {
|
|
7947
7969
|
const contended = buildContendedShapes(group);
|
|
7948
7970
|
const recordById = new Map(group.map(record => [record.id, record]));
|
|
@@ -8017,6 +8039,7 @@ const computeFillTiling = (input) => {
|
|
|
8017
8039
|
featureToGroupKey,
|
|
8018
8040
|
featureZIndex,
|
|
8019
8041
|
droppedInvalidFeatures,
|
|
8042
|
+
intersectionFailures,
|
|
8020
8043
|
};
|
|
8021
8044
|
};
|
|
8022
8045
|
|
|
@@ -8322,6 +8345,31 @@ const buildShapeLabelResolutionContext = (feature, args) => {
|
|
|
8322
8345
|
};
|
|
8323
8346
|
};
|
|
8324
8347
|
|
|
8348
|
+
/** Stable, order-independent key so the pair (a,b) dedupes to the same entry as (b,a). */
|
|
8349
|
+
const resolveIntersectionFailureDedupeKey = (layerHandleId, failure) => {
|
|
8350
|
+
const [first, second] = [failure.featureIdA, failure.featureIdB].sort();
|
|
8351
|
+
return `${layerHandleId}:${first}|${second}`;
|
|
8352
|
+
};
|
|
8353
|
+
/**
|
|
8354
|
+
* Report to Sentry each feature pair whose overlap test threw inside `polygon-clipping`
|
|
8355
|
+
* (SAGA-743), at most once per handle/pair per session.
|
|
8356
|
+
* Note: `computeFillTiling` emits `console.warn` separately.
|
|
8357
|
+
*/
|
|
8358
|
+
const captureFillTilingIntersectionFailures = (intersectionFailures, layerHandleId, sessionReportedDedupeKeys, report) => {
|
|
8359
|
+
for (const failure of intersectionFailures) {
|
|
8360
|
+
const key = resolveIntersectionFailureDedupeKey(layerHandleId, failure);
|
|
8361
|
+
if (sessionReportedDedupeKeys.has(key))
|
|
8362
|
+
continue;
|
|
8363
|
+
sessionReportedDedupeKeys.add(key);
|
|
8364
|
+
report({
|
|
8365
|
+
layerHandleId,
|
|
8366
|
+
featureIdA: failure.featureIdA,
|
|
8367
|
+
featureIdB: failure.featureIdB,
|
|
8368
|
+
message: failure.error instanceof Error ? failure.error.message : String(failure.error),
|
|
8369
|
+
});
|
|
8370
|
+
}
|
|
8371
|
+
};
|
|
8372
|
+
|
|
8325
8373
|
/**
|
|
8326
8374
|
* Extract ALL vertex coordinates from a geometry, walking every part of
|
|
8327
8375
|
* multi-geometries. Returns a flat array of [lng, lat] positions.
|
|
@@ -9576,6 +9624,7 @@ const bboxesIntersect = (a, b) => a[2] >= b[0] && a[0] <= b[2] && a[3] >= b[1] &
|
|
|
9576
9624
|
*/
|
|
9577
9625
|
const buildOverlappingShapesCountMap = (features, featureBboxes, viewportBounds) => {
|
|
9578
9626
|
const counts = new Map();
|
|
9627
|
+
const intersectionFailures = [];
|
|
9579
9628
|
for (const feature of features) {
|
|
9580
9629
|
counts.set(feature, 0);
|
|
9581
9630
|
}
|
|
@@ -9604,7 +9653,21 @@ const buildOverlappingShapesCountMap = (features, featureBboxes, viewportBounds)
|
|
|
9604
9653
|
const geomB = featureB.geometry;
|
|
9605
9654
|
const isPolygonalB = geomB !== null && (geomB.type === "Polygon" || geomB.type === "MultiPolygon");
|
|
9606
9655
|
if (isPolygonalA && isPolygonalB) {
|
|
9607
|
-
|
|
9656
|
+
let overlaps;
|
|
9657
|
+
try {
|
|
9658
|
+
overlaps = getGeoJsonPolygonIntersection(geomA, geomB) !== null;
|
|
9659
|
+
}
|
|
9660
|
+
catch (error) {
|
|
9661
|
+
// polygon-clipping failed on this pair (SAGA-743). Treat as no overlap so
|
|
9662
|
+
// decoration counts degrade gracefully instead of crashing the map.
|
|
9663
|
+
intersectionFailures.push({
|
|
9664
|
+
featureIdA: featureA.id !== undefined ? String(featureA.id) : `index:${i}`,
|
|
9665
|
+
featureIdB: featureB.id !== undefined ? String(featureB.id) : `index:${j}`,
|
|
9666
|
+
error,
|
|
9667
|
+
});
|
|
9668
|
+
continue;
|
|
9669
|
+
}
|
|
9670
|
+
if (overlaps) {
|
|
9608
9671
|
if (isFullyContainedInGeoJsonGeometry(geomA, geomB) !== true) {
|
|
9609
9672
|
counts.set(featureA, (counts.get(featureA) ?? 0) + 1);
|
|
9610
9673
|
}
|
|
@@ -9619,7 +9682,7 @@ const buildOverlappingShapesCountMap = (features, featureBboxes, viewportBounds)
|
|
|
9619
9682
|
}
|
|
9620
9683
|
}
|
|
9621
9684
|
}
|
|
9622
|
-
return counts;
|
|
9685
|
+
return { counts, intersectionFailures };
|
|
9623
9686
|
};
|
|
9624
9687
|
/**
|
|
9625
9688
|
* Value-based equality for the nested style overrides maps. Used to skip
|
|
@@ -9705,6 +9768,25 @@ const useShapeDecorations = ({ onDecorationsChange, onStyleOverridesChange, api,
|
|
|
9705
9768
|
const previousLayoutSides = useRef(new Map());
|
|
9706
9769
|
const prevStyleOverridesRef = useRef(new Map());
|
|
9707
9770
|
const prevDecorationLayersRef = useRef([]);
|
|
9771
|
+
const errorHandler = useErrorHandlerOrNull();
|
|
9772
|
+
// Session-level dedupe so a recurring polygon-clipping failure (SAGA-743) reports
|
|
9773
|
+
// to Sentry at most once per handle/pair per session, not on every pan frame.
|
|
9774
|
+
const sessionReportedIntersectionKeysRef = useRef(new Set());
|
|
9775
|
+
const reportOverlapIntersectionFailures = useCallback((intersectionFailures, layerHandleId) => {
|
|
9776
|
+
if (intersectionFailures.length === 0)
|
|
9777
|
+
return;
|
|
9778
|
+
const pairList = intersectionFailures.map(failure => `${failure.featureIdA}↔${failure.featureIdB}`).join(", ");
|
|
9779
|
+
// eslint-disable-next-line no-console -- Intentional: warn devs when polygon-clipping fails on schema-valid geometry (SAGA-743); the pair is treated as non-overlapping and decorations still render
|
|
9780
|
+
console.warn(`[useShapeDecorations] polygon-clipping failed on ${intersectionFailures.length} feature pair(s); treated as non-overlapping: ${pairList}`);
|
|
9781
|
+
if (errorHandler === null)
|
|
9782
|
+
return;
|
|
9783
|
+
captureFillTilingIntersectionFailures(intersectionFailures, layerHandleId, sessionReportedIntersectionKeysRef.current, payload => {
|
|
9784
|
+
errorHandler.captureException(new Error(`polygon-clipping failed during decoration overlap counting: ${JSON.stringify(payload)}`), {
|
|
9785
|
+
level: "warning",
|
|
9786
|
+
fingerprint: ["react-map", "fill-tiling", "intersection-failure"],
|
|
9787
|
+
});
|
|
9788
|
+
});
|
|
9789
|
+
}, [errorHandler]);
|
|
9708
9790
|
/**
|
|
9709
9791
|
* Labels that just transitioned from absent (hidden) to placed. While in
|
|
9710
9792
|
* this set, edge identity is NOT stored — the algorithm gets one extra
|
|
@@ -9764,7 +9846,9 @@ const useShapeDecorations = ({ onDecorationsChange, onStyleOverridesChange, api,
|
|
|
9764
9846
|
let overlappingCountsCache = null;
|
|
9765
9847
|
const getOverlappingCount = (feature) => {
|
|
9766
9848
|
if (overlappingCountsCache === null) {
|
|
9767
|
-
|
|
9849
|
+
const { counts, intersectionFailures } = buildOverlappingShapesCountMap(handle.features.features, featureBboxes, bounds);
|
|
9850
|
+
overlappingCountsCache = counts;
|
|
9851
|
+
reportOverlapIntersectionFailures(intersectionFailures, handle.id);
|
|
9768
9852
|
}
|
|
9769
9853
|
return overlappingCountsCache.get(feature) ?? 0;
|
|
9770
9854
|
};
|
|
@@ -10025,6 +10109,7 @@ const useShapeDecorations = ({ onDecorationsChange, onStyleOverridesChange, api,
|
|
|
10025
10109
|
hasSourceReadiness,
|
|
10026
10110
|
renderedSourceIds,
|
|
10027
10111
|
isReady,
|
|
10112
|
+
reportOverlapIntersectionFailures,
|
|
10028
10113
|
]);
|
|
10029
10114
|
const updateDecorationsRef = useRef(updateDecorations);
|
|
10030
10115
|
useEffect(() => {
|
|
@@ -10557,7 +10642,7 @@ const useShapeFillTiling = ({ api, handles, onFillGeometriesChange, onZIndexOver
|
|
|
10557
10642
|
const nextRestingFills = new Map();
|
|
10558
10643
|
const nextRestingZIndices = new Map();
|
|
10559
10644
|
for (const handle of currentHandles) {
|
|
10560
|
-
const { fillGeometries, featureToGroupKey, featureZIndex, droppedInvalidFeatures } = computeFillTiling({
|
|
10645
|
+
const { fillGeometries, featureToGroupKey, featureZIndex, droppedInvalidFeatures, intersectionFailures } = computeFillTiling({
|
|
10561
10646
|
features: handle.features.features,
|
|
10562
10647
|
viewportBounds: GLOBAL_BOUNDS,
|
|
10563
10648
|
resolveStackOrder: handle.overlap?.resolveStackOrder ?? defaultShapeStackOrder,
|
|
@@ -10573,6 +10658,14 @@ const useShapeFillTiling = ({ api, handles, onFillGeometriesChange, onZIndexOver
|
|
|
10573
10658
|
});
|
|
10574
10659
|
});
|
|
10575
10660
|
}
|
|
10661
|
+
if (errorHandler !== null && intersectionFailures.length > 0) {
|
|
10662
|
+
captureFillTilingIntersectionFailures(intersectionFailures, handle.id, sessionReportedDedupeKeysRef.current, payload => {
|
|
10663
|
+
errorHandler.captureException(new Error(`polygon-clipping failed during fill-tiling overlap grouping: ${JSON.stringify(payload)}`), {
|
|
10664
|
+
level: "warning",
|
|
10665
|
+
fingerprint: ["react-map", "fill-tiling", "intersection-failure"],
|
|
10666
|
+
});
|
|
10667
|
+
});
|
|
10668
|
+
}
|
|
10576
10669
|
featureToGroupKeyByHandleRef.current.set(handle.id, new Map(featureToGroupKey));
|
|
10577
10670
|
if (fillGeometries.size > 0) {
|
|
10578
10671
|
nextRestingFills.set(handle.id, fillGeometries);
|
package/package.json
CHANGED
|
@@ -1,20 +1,20 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@trackunit/react-map",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.88",
|
|
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.10.
|
|
10
|
+
"@trackunit/react-components": "2.10.9",
|
|
11
11
|
"@trackunit/css-class-variance-utilities": "1.14.17",
|
|
12
|
-
"@trackunit/react-form-components": "2.6.
|
|
13
|
-
"@trackunit/react-core-hooks": "1.21.
|
|
14
|
-
"@trackunit/geo-json-utils": "1.15.
|
|
15
|
-
"@trackunit/i18n-library-translation": "2.4.
|
|
12
|
+
"@trackunit/react-form-components": "2.6.14",
|
|
13
|
+
"@trackunit/react-core-hooks": "1.21.16",
|
|
14
|
+
"@trackunit/geo-json-utils": "1.15.19",
|
|
15
|
+
"@trackunit/i18n-library-translation": "2.4.16",
|
|
16
16
|
"react-minimal-pie-chart": "^8.4.0",
|
|
17
|
-
"@trackunit/react-map-adapter-shared": "0.0.
|
|
17
|
+
"@trackunit/react-map-adapter-shared": "0.0.99",
|
|
18
18
|
"@trackunit/react-map-color-utils": "0.0.80",
|
|
19
19
|
"@trackunit/ui-design-tokens": "1.15.7",
|
|
20
20
|
"@floating-ui/react": "^0.26.25",
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { FillTilingIntersectionFailure } from "./shapeFillTiling";
|
|
2
|
+
type ReportFillTilingIntersectionFailureEvent = (payload: Record<string, unknown>) => void;
|
|
3
|
+
/**
|
|
4
|
+
* Report to Sentry each feature pair whose overlap test threw inside `polygon-clipping`
|
|
5
|
+
* (SAGA-743), at most once per handle/pair per session.
|
|
6
|
+
* Note: `computeFillTiling` emits `console.warn` separately.
|
|
7
|
+
*/
|
|
8
|
+
export declare const captureFillTilingIntersectionFailures: (intersectionFailures: ReadonlyArray<FillTilingIntersectionFailure>, layerHandleId: string, sessionReportedDedupeKeys: Set<string>, report: ReportFillTilingIntersectionFailureEvent) => void;
|
|
9
|
+
export {};
|
|
@@ -70,12 +70,25 @@ export type FillTilingResult = Readonly<{
|
|
|
70
70
|
featureZIndex: ReadonlyMap<string, number>;
|
|
71
71
|
/** Features excluded from fill tiling when boundary schema validation fails. */
|
|
72
72
|
droppedInvalidFeatures: ReadonlyArray<DroppedInvalidFillTilingFeature>;
|
|
73
|
+
/**
|
|
74
|
+
* Feature pairs whose overlap test threw inside `polygon-clipping` (SAGA-743).
|
|
75
|
+
* The pair was treated as non-overlapping; surfaced here so the hook can report
|
|
76
|
+
* it to Sentry, since the geometry is schema-valid and never hits
|
|
77
|
+
* `droppedInvalidFeatures`.
|
|
78
|
+
*/
|
|
79
|
+
intersectionFailures: ReadonlyArray<FillTilingIntersectionFailure>;
|
|
73
80
|
}>;
|
|
74
81
|
export type DroppedInvalidFillTilingFeature = Readonly<{
|
|
75
82
|
feature: GeoJsonFeature;
|
|
76
83
|
/** `safeParse` failure details (`parsed.error.format()`). */
|
|
77
84
|
validationError: unknown;
|
|
78
85
|
}>;
|
|
86
|
+
export type FillTilingIntersectionFailure = Readonly<{
|
|
87
|
+
featureIdA: string;
|
|
88
|
+
featureIdB: string;
|
|
89
|
+
/** The error thrown by `polygon-clipping` (typically "Unable to complete output ring"). */
|
|
90
|
+
error: unknown;
|
|
91
|
+
}>;
|
|
79
92
|
/**
|
|
80
93
|
* Planar area of any GeoJSON geometry (outer rings minus holes). Returns 0 for
|
|
81
94
|
* non-polygonal types (Point, LineString, …). Relative magnitude only — not
|