@trackunit/react-map 0.2.226 → 0.2.230

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.
Files changed (48) hide show
  1. package/index.cjs.js +110 -52
  2. package/index.esm.js +110 -52
  3. package/package.json +11 -11
  4. package/src/draw/useDraw.d.ts +2 -1
  5. package/src/layers/shared/consoleWarnMap.d.ts +7 -0
  6. package/src/layers/shared/useDevWarnings.d.ts +14 -0
  7. package/src/layers/useHeatmap/useHeatmap.d.ts +2 -1
  8. package/src/layers/useHeatmap/utils/normalizeHeatmapData.d.ts +4 -1
  9. package/src/layers/useHeatmap/utils/resolveHeatmapStyle.d.ts +8 -2
  10. package/src/layers/useShapes/captureDroppedInvalidFillTilingFeatures.d.ts +2 -1
  11. package/src/layers/useShapes/captureFillTilingIntersectionFailures.d.ts +2 -1
  12. package/src/layers/useShapes/utils/formatDroppedInvalidFeatureIds.d.ts +3 -0
  13. package/src/layers/useShapes/utils/formatIntersectionFailurePairs.d.ts +3 -0
  14. package/translation.cjs.js +2 -0
  15. package/translation.esm.js +2 -0
  16. package/translation10.cjs.js +2 -0
  17. package/translation10.esm.js +2 -0
  18. package/translation11.cjs.js +2 -0
  19. package/translation11.esm.js +2 -0
  20. package/translation12.cjs.js +2 -0
  21. package/translation12.esm.js +2 -0
  22. package/translation13.cjs.js +2 -0
  23. package/translation13.esm.js +2 -0
  24. package/translation14.cjs.js +2 -0
  25. package/translation14.esm.js +2 -0
  26. package/translation15.cjs.js +2 -0
  27. package/translation15.esm.js +2 -0
  28. package/translation16.cjs.js +2 -0
  29. package/translation16.esm.js +2 -0
  30. package/translation17.cjs.js +2 -0
  31. package/translation17.esm.js +2 -0
  32. package/translation2.cjs.js +2 -0
  33. package/translation2.esm.js +2 -0
  34. package/translation3.cjs.js +2 -0
  35. package/translation3.esm.js +2 -0
  36. package/translation4.cjs.js +2 -0
  37. package/translation4.esm.js +2 -0
  38. package/translation5.cjs.js +2 -0
  39. package/translation5.esm.js +2 -0
  40. package/translation6.cjs.js +2 -0
  41. package/translation6.esm.js +2 -0
  42. package/translation7.cjs.js +2 -0
  43. package/translation7.esm.js +2 -0
  44. package/translation8.cjs.js +2 -0
  45. package/translation8.esm.js +2 -0
  46. package/translation9.cjs.js +2 -0
  47. package/translation9.esm.js +2 -0
  48. package/migrations/entry.js.map +0 -1
package/index.esm.js CHANGED
@@ -3454,8 +3454,17 @@ const useControls = (base, areas) => {
3454
3454
  /**
3455
3455
  * Single dev-warning channel for react-map hooks (invalid data, invalid style, unhandled render errors).
3456
3456
  * Messages are prefixed with the emitting hook, e.g. `[useHeatmap]`.
3457
+ *
3458
+ * Skipped in production builds: the warnings describe consumer mistakes a developer fixes
3459
+ * once, and with polled data they would otherwise log on every data change. Vitest runs
3460
+ * with `NODE_ENV` set to `test`, so specs can still spy on `console.warn`.
3461
+ *
3462
+ * Call it from effects, handlers and callbacks, never during render: pure normalizers
3463
+ * return their warnings and `useDevWarnings` emits them once per result.
3457
3464
  */
3458
3465
  const consoleWarnMap = (source, message, ...details) => {
3466
+ if (process.env.NODE_ENV === "production")
3467
+ return;
3459
3468
  // eslint-disable-next-line no-console -- Intentional: the one react-map warning channel; invalid input is dropped or defaulted and render errors without onError must stay visible
3460
3469
  console.warn(`[${source}] ${message}`, ...details);
3461
3470
  };
@@ -5912,17 +5921,39 @@ const createLazyGetter = (compute) => {
5912
5921
  };
5913
5922
  };
5914
5923
 
5924
+ /** Shared empty result so hooks with nothing to report keep a stable reference. */
5925
+ const NO_DEV_WARNINGS = [];
5926
+ /**
5927
+ * Emits a normalizer's warnings through the react-map dev-warning channel, from an
5928
+ * effect rather than during render.
5929
+ *
5930
+ * Pure normalizers (`filterPositioned`, `normalizeHeatmapData`, ...) run inside render
5931
+ * and must not log: StrictMode and discarded concurrent renders would log them twice.
5932
+ * They return their warnings instead, and this hook emits each list once. The list's
5933
+ * identity is the dedupe key, so StrictMode's double effect run and re-renders that
5934
+ * keep the same normalized result add nothing; a new normalization pass (a new list)
5935
+ * warns again, which is what a developer feeding new invalid data expects.
5936
+ */
5937
+ const useDevWarnings = (source, warnings) => {
5938
+ const emittedRef = useRef(null);
5939
+ useEffect(() => {
5940
+ if (warnings.length === 0 || emittedRef.current === warnings)
5941
+ return;
5942
+ emittedRef.current = warnings;
5943
+ for (const message of warnings) {
5944
+ consoleWarnMap(source, message);
5945
+ }
5946
+ }, [source, warnings]);
5947
+ };
5948
+
5915
5949
  // ============================================================================
5916
5950
  // Constants
5917
5951
  // ============================================================================
5918
5952
  const EMPTY_CONTROLS$3 = {};
5919
5953
  const EMPTY_FEATURES$1 = { type: "FeatureCollection", features: [] };
5920
- // ============================================================================
5921
- // Internal helpers
5922
- // ============================================================================
5923
5954
  /**
5924
5955
  * Filter waypoints to only valid GeoJSON positions. Invalid entries are dropped.
5925
- * Logs console.warn once when any waypoints are filtered.
5956
+ * Pure: it runs during render, so it returns one grouped warning instead of logging.
5926
5957
  */
5927
5958
  const filterValidWaypoints = (waypoints) => {
5928
5959
  const valid = [];
@@ -5932,12 +5963,11 @@ const filterValidWaypoints = (waypoints) => {
5932
5963
  valid.push(result.data);
5933
5964
  }
5934
5965
  }
5935
- if (valid.length < waypoints.length) {
5936
- const filtered = waypoints.length - valid.length;
5937
- // eslint-disable-next-line no-console -- Intentional: warn devs when invalid waypoints are passed; invalid entries are filtered out
5938
- console.warn(`[useRoute] Filtered ${filtered} invalid waypoint(s). Expected [longitude, latitude] with latitude between -90 and 90.`);
5939
- }
5940
- return valid;
5966
+ const filtered = waypoints.length - valid.length;
5967
+ const warnings = filtered > 0
5968
+ ? [`Filtered ${filtered} invalid waypoint(s). Expected [longitude, latitude] with latitude between -90 and 90.`]
5969
+ : NO_DEV_WARNINGS;
5970
+ return { waypoints: valid, warnings };
5941
5971
  };
5942
5972
  /**
5943
5973
  * Convert waypoints to a GeoJSON FeatureCollection with a single LineString.
@@ -5985,7 +6015,9 @@ const useRoute = (options) => {
5985
6015
  // ---- Data readiness tracking ----
5986
6016
  const layerReady = useLayerReady(loading);
5987
6017
  // ---- Filter to valid waypoints only ----
5988
- const waypoints = useMemo(() => filterValidWaypoints(rawWaypoints), [rawWaypoints]);
6018
+ const filtered = useMemo(() => filterValidWaypoints(rawWaypoints), [rawWaypoints]);
6019
+ const waypoints = filtered.waypoints;
6020
+ useDevWarnings("useRoute", filtered.warnings);
5989
6021
  // ---- Convert to GeoJSON ----
5990
6022
  const features = useMemo(() => toLineStringFeatureCollection(id, waypoints), [id, waypoints]);
5991
6023
  // ---- Lazy getBounds with cache ----
@@ -6100,8 +6132,7 @@ const useFitToContent = (api, handles, options) => {
6100
6132
  const stillPending = filterHandles(handlesRef.current, "initial").filter(h => h.loadsData && h.loading);
6101
6133
  if (stillPending.length > 0) {
6102
6134
  const names = stillPending.map(h => h.name).join(", ");
6103
- // eslint-disable-next-line no-console -- Intentional: warn devs when layers are too slow to load
6104
- console.warn(`[useFitToContent] Timeout (${timeout}ms) reached. Fitting with available data. Still loading: ${names}`);
6135
+ consoleWarnMap("useFitToContent", `Timeout (${timeout}ms) reached. Fitting with available data. Still loading: ${names}`);
6105
6136
  }
6106
6137
  performFit();
6107
6138
  }, timeout);
@@ -6133,7 +6164,8 @@ const useFitToContent = (api, handles, options) => {
6133
6164
  * Converts consumer items into weighted GeoJSON `Point` features.
6134
6165
  *
6135
6166
  * Null positions and zero weights are dropped silently. Invalid positions and
6136
- * negative or non-finite weights are dropped with one grouped warning per pass.
6167
+ * negative or non-finite weights are dropped and reported as one grouped warning
6168
+ * per pass in `warnings`; the hook emits it from an effect, never during render.
6137
6169
  */
6138
6170
  const normalizeHeatmapData = ({ data, getPosition, getWeight, }) => {
6139
6171
  const features = [];
@@ -6163,12 +6195,15 @@ const normalizeHeatmapData = ({ data, getPosition, getWeight, }) => {
6163
6195
  properties: { weight },
6164
6196
  });
6165
6197
  }
6166
- if (invalidPositionCount > 0 || invalidWeightCount > 0) {
6167
- consoleWarnMap("useHeatmap", `Filtered ${invalidPositionCount} item(s) with invalid position and ${invalidWeightCount} item(s) with invalid weight. Expected [longitude, latitude] with longitude between -180 and 180, latitude between -90 and 90, and a finite weight >= 0.`);
6168
- }
6198
+ const warnings = invalidPositionCount > 0 || invalidWeightCount > 0
6199
+ ? [
6200
+ `Filtered ${invalidPositionCount} item(s) with invalid position and ${invalidWeightCount} item(s) with invalid weight. Expected [longitude, latitude] with longitude between -180 and 180, latitude between -90 and 90, and a finite weight >= 0.`,
6201
+ ]
6202
+ : NO_DEV_WARNINGS;
6169
6203
  return {
6170
6204
  features: { type: "FeatureCollection", features },
6171
6205
  positions,
6206
+ warnings,
6172
6207
  };
6173
6208
  };
6174
6209
 
@@ -6222,13 +6257,15 @@ const DEFAULT_HEATMAP_COLOR_RANGE = [
6222
6257
  const DEFAULT_PARSED_COLOR_RANGE = DEFAULT_HEATMAP_COLOR_RANGE.map(parseHeatmapColor).filter(nonNullable);
6223
6258
  const resolveColorRange = (colorRange) => {
6224
6259
  if (colorRange === undefined)
6225
- return DEFAULT_PARSED_COLOR_RANGE;
6260
+ return { colorRange: DEFAULT_PARSED_COLOR_RANGE, warning: null };
6226
6261
  const parsed = colorRange.map(parseHeatmapColor);
6227
6262
  const valid = parsed.filter(nonNullable);
6228
6263
  if (colorRange.length >= 2 && valid.length === parsed.length)
6229
- return valid;
6230
- consoleWarnMap("useHeatmap", "colorRange needs at least two supported colours (#rgb, #rrggbb, #rrggbbaa, rgb(), rgba()); using the default.", colorRange);
6231
- return DEFAULT_PARSED_COLOR_RANGE;
6264
+ return { colorRange: valid, warning: null };
6265
+ return {
6266
+ colorRange: DEFAULT_PARSED_COLOR_RANGE,
6267
+ warning: `colorRange needs at least two supported colours (#rgb, #rrggbb, #rrggbbaa, rgb(), rgba()); using the default. Received: ${JSON.stringify(colorRange)}`,
6268
+ };
6232
6269
  };
6233
6270
  const HEATMAP_STYLE_RULES = {
6234
6271
  radiusPixels: { defaultValue: 30, min: 1, max: 100 },
@@ -6238,7 +6275,8 @@ const HEATMAP_STYLE_RULES = {
6238
6275
  /**
6239
6276
  * Applies defaults and supported ranges so both adapters receive identical style values.
6240
6277
  *
6241
- * Finite values are clamped; non-finite values fall back to the default with one warning.
6278
+ * Finite values are clamped; non-finite values fall back to the default with one grouped
6279
+ * warning. Pure: it runs inside a render-time `useMemo`, so it returns its warnings.
6242
6280
  */
6243
6281
  const resolveHeatmapStyle = (style) => {
6244
6282
  const nonFiniteKeys = [];
@@ -6253,16 +6291,20 @@ const resolveHeatmapStyle = (style) => {
6253
6291
  }
6254
6292
  return Math.min(rule.max, Math.max(rule.min, value));
6255
6293
  };
6294
+ const colorRange = resolveColorRange(style?.colorRange);
6256
6295
  const resolved = {
6257
6296
  radiusPixels: resolveValue("radiusPixels"),
6258
6297
  intensity: resolveValue("intensity"),
6259
6298
  opacity: resolveValue("opacity"),
6260
- colorRange: resolveColorRange(style?.colorRange),
6299
+ colorRange: colorRange.colorRange,
6261
6300
  };
6301
+ const warnings = [];
6262
6302
  if (nonFiniteKeys.length > 0) {
6263
- consoleWarnMap("useHeatmap", `Non-finite style value(s) for ${nonFiniteKeys.join(", ")} replaced with defaults.`);
6303
+ warnings.push(`Non-finite style value(s) for ${nonFiniteKeys.join(", ")} replaced with defaults.`);
6264
6304
  }
6265
- return resolved;
6305
+ if (colorRange.warning !== null)
6306
+ warnings.push(colorRange.warning);
6307
+ return { style: resolved, warnings: warnings.length === 0 ? NO_DEV_WARNINGS : warnings };
6266
6308
  };
6267
6309
 
6268
6310
  const EMPTY_CONTROLS$2 = {};
@@ -6324,12 +6366,17 @@ const useHeatmap = (options) => {
6324
6366
  if (style?.colorRange !== colorRange && !isEqual(style?.colorRange, colorRange)) {
6325
6367
  setColorRange(style?.colorRange);
6326
6368
  }
6327
- const resolvedStyle = useMemo(() => resolveHeatmapStyle({
6369
+ const resolved = useMemo(() => resolveHeatmapStyle({
6328
6370
  radiusPixels: style?.radiusPixels,
6329
6371
  intensity: style?.intensity,
6330
6372
  opacity: style?.opacity,
6331
6373
  colorRange,
6332
6374
  }), [style?.radiusPixels, style?.intensity, style?.opacity, colorRange]);
6375
+ const resolvedStyle = resolved.style;
6376
+ // Normalization and style resolution run during render and stay pure; their
6377
+ // warnings are emitted once per pass from an effect (SAGA-907).
6378
+ useDevWarnings("useHeatmap", normalized.warnings);
6379
+ useDevWarnings("useHeatmap", resolved.warnings);
6333
6380
  const getBounds = useMemo(() => createLazyGetter(() => computeBoundsFromPositions(normalized.positions)), [normalized.positions]);
6334
6381
  const counts = useMemo(() => ({ total: data.length, points: normalized.features.features.length }), [data.length, normalized.features.features.length]);
6335
6382
  return useMemo(() => ({
@@ -7395,7 +7442,8 @@ const EMPTY_FEATURES = { type: "FeatureCollection", features: [] };
7395
7442
  const EMPTY_CONTROLS$1 = {};
7396
7443
  /**
7397
7444
  * Filter items to only those with valid positions and extract id + position.
7398
- * Invalid positions (e.g. latitude out of range) are skipped. Logs once when any are filtered.
7445
+ * Invalid positions (e.g. latitude out of range) are skipped. Pure: it runs during
7446
+ * render, so it returns one grouped warning instead of logging.
7399
7447
  */
7400
7448
  const filterPositioned = (data, getPosition, getId) => {
7401
7449
  const result = [];
@@ -7412,11 +7460,12 @@ const filterPositioned = (data, getPosition, getId) => {
7412
7460
  }
7413
7461
  }
7414
7462
  }
7415
- if (invalidCount > 0) {
7416
- // eslint-disable-next-line no-console -- Intentional: warn devs when getPosition returns invalid GeoJSON; items are filtered out
7417
- console.warn(`[useMarkers] Filtered ${invalidCount} item(s) with invalid position. Expected [longitude, latitude] with latitude between -90 and 90.`);
7418
- }
7419
- return result;
7463
+ const warnings = invalidCount > 0
7464
+ ? [
7465
+ `Filtered ${invalidCount} item(s) with invalid position. Expected [longitude, latitude] with latitude between -90 and 90.`,
7466
+ ]
7467
+ : NO_DEV_WARNINGS;
7468
+ return { items: result, warnings };
7420
7469
  };
7421
7470
  /**
7422
7471
  * Convert positioned items to a GeoJSON FeatureCollection of Points.
@@ -7520,11 +7569,13 @@ const useMarkers = (options) => {
7520
7569
  // structurally-identical inline arrays do NOT trigger infinite re-renders.
7521
7570
  // The reference check short-circuits isEqual for the common stable-ref case.
7522
7571
  const [prevData, setPrevData] = useState(data);
7523
- const [positionedItems, setPositionedItems] = useState(() => filterPositioned(data, getPosition, getId));
7572
+ const [positioned, setPositioned] = useState(() => filterPositioned(data, getPosition, getId));
7524
7573
  if (data !== prevData && !isEqual(data, prevData)) {
7525
7574
  setPrevData(data);
7526
- setPositionedItems(filterPositioned(data, getPosition, getId));
7575
+ setPositioned(filterPositioned(data, getPosition, getId));
7527
7576
  }
7577
+ const positionedItems = positioned.items;
7578
+ useDevWarnings("useMarkers", positioned.warnings);
7528
7579
  // ---- Convert to GeoJSON FeatureCollection ----
7529
7580
  const features = useMemo(() => toPointFeatureCollection(positionedItems), [positionedItems]);
7530
7581
  // ---- Resolved server clusters (after resolveGroups entity-shape decisions) ----
@@ -9378,14 +9429,8 @@ const computeFillTiling = (input) => {
9378
9429
  }
9379
9430
  validFeatures.push(feature);
9380
9431
  }
9381
- if (droppedInvalidFeatures.length > 0) {
9382
- const idList = droppedInvalidFeatures
9383
- .map(entry => (entry.feature.id !== undefined ? String(entry.feature.id) : undefined))
9384
- .filter((id) => id !== undefined)
9385
- .join(", ") || "(no feature ids)";
9386
- // eslint-disable-next-line no-console -- Intentional: warn devs when schema-invalid polygonal geometry is excluded from fill tiling
9387
- console.warn(`[computeFillTiling] Excluded ${droppedInvalidFeatures.length} feature(s) with invalid polygonal geometry from fill tiling (full fill still renders; overlap clipping is skipped): ${idList}`);
9388
- }
9432
+ // Dropped features are returned, not logged: this function runs inside the hook's
9433
+ // recompute path and stays pure; the hook emits one dev warning per pass.
9389
9434
  // Densify before any polygon/boundary math so clip geometry follows great-circle
9390
9435
  // arcs on large polygons (mirrors the ADR-0024 precedent for edge labels). Always
9391
9436
  // defer to densifyGeodesicFeatures' per-feature resolution (geodesic override >
@@ -9418,11 +9463,7 @@ const computeFillTiling = (input) => {
9418
9463
  const featureToGroupKey = new Map();
9419
9464
  const featureZIndex = new Map();
9420
9465
  const { groups: overlapGroups, intersectionFailures } = buildOverlapGroups(records);
9421
- if (intersectionFailures.length > 0) {
9422
- const pairList = intersectionFailures.map(failure => `${failure.featureIdA}↔${failure.featureIdB}`).join(", ");
9423
- // 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
9424
- console.warn(`[computeFillTiling] polygon-clipping failed on ${intersectionFailures.length} feature pair(s); treated as non-overlapping (full fill still renders): ${pairList}`);
9425
- }
9466
+ // Failures are returned for the hook to warn about and report (SAGA-743).
9426
9467
  for (const group of overlapGroups) {
9427
9468
  const contended = buildContendedShapes(group);
9428
9469
  const recordById = new Map(group.map(record => [record.id, record]));
@@ -9501,6 +9542,9 @@ const computeFillTiling = (input) => {
9501
9542
  };
9502
9543
  };
9503
9544
 
9545
+ /** Renders failing feature pairs as `a↔b, c↔d` for dev warnings and error reports. */
9546
+ const formatIntersectionFailurePairs = (failures) => failures.map(failure => `${failure.featureIdA}↔${failure.featureIdB}`).join(", ");
9547
+
9504
9548
  const DEFAULT_ZOOM = 0;
9505
9549
  const DEFAULT_TILE_SIZE = 256;
9506
9550
  /**
@@ -9811,7 +9855,8 @@ const resolveIntersectionFailureDedupeKey = (layerHandleId, failure) => {
9811
9855
  /**
9812
9856
  * Report to Sentry each feature pair whose overlap test threw inside `polygon-clipping`
9813
9857
  * (SAGA-743), at most once per handle/pair per session.
9814
- * Note: `computeFillTiling` emits `console.warn` separately.
9858
+ * Note: the dev warning for this case is emitted by `useShapeFillTiling` from its recompute
9859
+ * path (`computeFillTiling` itself is pure and logs nothing).
9815
9860
  */
9816
9861
  const captureFillTilingIntersectionFailures = (intersectionFailures, layerHandleId, sessionReportedDedupeKeys, report) => {
9817
9862
  for (const failure of intersectionFailures) {
@@ -11233,9 +11278,7 @@ const useShapeDecorations = ({ onDecorationsChange, onStyleOverridesChange, api,
11233
11278
  const reportOverlapIntersectionFailures = useCallback((intersectionFailures, layerHandleId) => {
11234
11279
  if (intersectionFailures.length === 0)
11235
11280
  return;
11236
- const pairList = intersectionFailures.map(failure => `${failure.featureIdA}↔${failure.featureIdB}`).join(", ");
11237
- // 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
11238
- console.warn(`[useShapeDecorations] polygon-clipping failed on ${intersectionFailures.length} feature pair(s); treated as non-overlapping: ${pairList}`);
11281
+ consoleWarnMap("useShapeDecorations", `polygon-clipping failed on ${intersectionFailures.length} feature pair(s); treated as non-overlapping: ${formatIntersectionFailurePairs(intersectionFailures)}`);
11239
11282
  if (errorHandler === null)
11240
11283
  return;
11241
11284
  captureFillTilingIntersectionFailures(intersectionFailures, layerHandleId, sessionReportedIntersectionKeysRef.current, payload => {
@@ -11658,7 +11701,8 @@ const captureOnce = (key, report, sessionReportedDedupeKeys, payload) => {
11658
11701
  };
11659
11702
  /**
11660
11703
  * Report to Sentry each dropped invalid feature via `report`, at most once per handle/feature per session.
11661
- * Note: `computeFillTiling` emits `console.warn` separately.
11704
+ * Note: the dev warning for this case is emitted by `useShapeFillTiling` from its recompute
11705
+ * path (`computeFillTiling` itself is pure and logs nothing).
11662
11706
  */
11663
11707
  const captureDroppedInvalidFillTilingFeatures = (droppedInvalidFeatures, layerHandleId, sessionReportedDedupeKeys, report) => {
11664
11708
  for (const entry of droppedInvalidFeatures) {
@@ -11672,6 +11716,12 @@ const captureDroppedInvalidFillTilingFeatures = (droppedInvalidFeatures, layerHa
11672
11716
  }
11673
11717
  };
11674
11718
 
11719
+ /** Lists the ids of features dropped from fill tiling, or a placeholder when none carry an id. */
11720
+ const formatDroppedInvalidFeatureIds = (dropped) => dropped
11721
+ .map(entry => (entry.feature.id !== undefined ? String(entry.feature.id) : undefined))
11722
+ .filter(nonNullable)
11723
+ .join(", ") || "(no feature ids)";
11724
+
11675
11725
  // ============================================================================
11676
11726
  // Default resolver
11677
11727
  // ============================================================================
@@ -12108,6 +12158,14 @@ const useShapeFillTiling = ({ api, handles, onFillGeometriesChange, onZIndexOver
12108
12158
  layerGeodesic: handle.style.geodesic,
12109
12159
  featureStyles: handle.featureStyles,
12110
12160
  });
12161
+ // The dev warnings live here, not in computeFillTiling: recompute runs from
12162
+ // effects and idle callbacks, so the pure function stays silent (SAGA-907).
12163
+ if (droppedInvalidFeatures.length > 0) {
12164
+ consoleWarnMap("useShapes", `Excluded ${droppedInvalidFeatures.length} feature(s) with invalid polygonal geometry from fill tiling (full fill still renders; overlap clipping is skipped): ${formatDroppedInvalidFeatureIds(droppedInvalidFeatures)}`);
12165
+ }
12166
+ if (intersectionFailures.length > 0) {
12167
+ consoleWarnMap("useShapes", `polygon-clipping failed on ${intersectionFailures.length} feature pair(s); treated as non-overlapping (full fill still renders): ${formatIntersectionFailurePairs(intersectionFailures)}`);
12168
+ }
12111
12169
  if (errorHandler !== null && droppedInvalidFeatures.length > 0) {
12112
12170
  captureDroppedInvalidFillTilingFeatures(droppedInvalidFeatures, handle.id, sessionReportedDedupeKeysRef.current, payload => {
12113
12171
  errorHandler.captureException(new Error(`Schema-invalid polygonal geometry excluded from fill tiling: ${JSON.stringify(payload)}`), {
package/package.json CHANGED
@@ -1,23 +1,23 @@
1
1
  {
2
2
  "name": "@trackunit/react-map",
3
- "version": "0.2.226",
3
+ "version": "0.2.230",
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.11.0",
11
- "@trackunit/css-class-variance-utilities": "2.6.0",
12
- "@trackunit/react-form-components": "2.13.0",
13
- "@trackunit/react-core-hooks": "1.28.0",
14
- "@trackunit/geo-json-utils": "1.21.0",
15
- "@trackunit/i18n-library-translation": "2.11.0",
10
+ "@trackunit/react-components": "3.11.2",
11
+ "@trackunit/css-class-variance-utilities": "2.6.1",
12
+ "@trackunit/react-form-components": "2.13.2",
13
+ "@trackunit/react-core-hooks": "1.28.2",
14
+ "@trackunit/geo-json-utils": "1.21.1",
15
+ "@trackunit/i18n-library-translation": "2.11.2",
16
16
  "react-minimal-pie-chart": "^8.4.0",
17
- "@trackunit/react-map-adapter-shared": "0.0.167",
18
- "@trackunit/react-map-color-utils": "0.0.141",
19
- "@trackunit/shared-utils": "1.22.0",
20
- "@trackunit/ui-design-tokens": "1.21.0",
17
+ "@trackunit/react-map-adapter-shared": "0.0.168",
18
+ "@trackunit/react-map-color-utils": "0.0.142",
19
+ "@trackunit/shared-utils": "1.22.1",
20
+ "@trackunit/ui-design-tokens": "1.21.1",
21
21
  "@floating-ui/react": "^0.26.25",
22
22
  "es-toolkit": "^1.52.0",
23
23
  "tailwind-merge": "^3.7.0",
@@ -26,7 +26,8 @@ export type UseDrawOptions = Readonly<{
26
26
  * engine failed to load or update, or part of a committed value could not be
27
27
  * rendered (`value-rejected`, with the indices of the features that are not on the
28
28
  * map). The committed `value` is never changed by an error. Without a handler the
29
- * error is logged with `console.warn`; it is reported to the app's error handler
29
+ * error is logged as a development warning (silent in production builds); it is
30
+ * reported to the app's error handler
30
31
  * either way, when the host provides one, at most once per code per mount.
31
32
  */
32
33
  onError?: (error: DrawError) => void;
@@ -1,5 +1,12 @@
1
1
  /**
2
2
  * Single dev-warning channel for react-map hooks (invalid data, invalid style, unhandled render errors).
3
3
  * Messages are prefixed with the emitting hook, e.g. `[useHeatmap]`.
4
+ *
5
+ * Skipped in production builds: the warnings describe consumer mistakes a developer fixes
6
+ * once, and with polled data they would otherwise log on every data change. Vitest runs
7
+ * with `NODE_ENV` set to `test`, so specs can still spy on `console.warn`.
8
+ *
9
+ * Call it from effects, handlers and callbacks, never during render: pure normalizers
10
+ * return their warnings and `useDevWarnings` emits them once per result.
4
11
  */
5
12
  export declare const consoleWarnMap: (source: string, message: string, ...details: ReadonlyArray<unknown>) => void;
@@ -0,0 +1,14 @@
1
+ /** Shared empty result so hooks with nothing to report keep a stable reference. */
2
+ export declare const NO_DEV_WARNINGS: ReadonlyArray<string>;
3
+ /**
4
+ * Emits a normalizer's warnings through the react-map dev-warning channel, from an
5
+ * effect rather than during render.
6
+ *
7
+ * Pure normalizers (`filterPositioned`, `normalizeHeatmapData`, ...) run inside render
8
+ * and must not log: StrictMode and discarded concurrent renders would log them twice.
9
+ * They return their warnings instead, and this hook emits each list once. The list's
10
+ * identity is the dedupe key, so StrictMode's double effect run and re-renders that
11
+ * keep the same normalized result add nothing; a new normalization pass (a new list)
12
+ * warns again, which is what a developer feeding new invalid data expects.
13
+ */
14
+ export declare const useDevWarnings: (source: string, warnings: ReadonlyArray<string>) => void;
@@ -24,7 +24,8 @@ export type UseHeatmapOptions<TItem> = Readonly<{
24
24
  style?: HeatmapStyle;
25
25
  /**
26
26
  * Called once per failed renderer load or update (for example when the Google deck.gl chunk fails to load).
27
- * The basemap and other layers keep rendering. Without it, failures are logged with `console.warn`.
27
+ * The basemap and other layers keep rendering. Without it, failures are logged as a development warning
28
+ * (silent in production builds).
28
29
  * Failures are reported to the app's error handler (Sentry) either way.
29
30
  * After a renderer failure (for example WebGL context loss on Google), the heatmap stays hidden until the next map
30
31
  * layer or interaction change triggers a retry; nothing retries on an idle map.
@@ -3,6 +3,8 @@ import type { GeoJsonFeatureCollection } from "../../types";
3
3
  export type NormalizedHeatmapData = Readonly<{
4
4
  features: GeoJsonFeatureCollection;
5
5
  positions: ReadonlyArray<GeoJsonPosition>;
6
+ /** One grouped message per pass when items were dropped; emitted by `useDevWarnings`. */
7
+ warnings: ReadonlyArray<string>;
6
8
  }>;
7
9
  type NormalizeHeatmapDataOptions<TItem> = Readonly<{
8
10
  data: ReadonlyArray<TItem>;
@@ -13,7 +15,8 @@ type NormalizeHeatmapDataOptions<TItem> = Readonly<{
13
15
  * Converts consumer items into weighted GeoJSON `Point` features.
14
16
  *
15
17
  * Null positions and zero weights are dropped silently. Invalid positions and
16
- * negative or non-finite weights are dropped with one grouped warning per pass.
18
+ * negative or non-finite weights are dropped and reported as one grouped warning
19
+ * per pass in `warnings`; the hook emits it from an effect, never during render.
17
20
  */
18
21
  export declare const normalizeHeatmapData: <TItem>({ data, getPosition, getWeight, }: NormalizeHeatmapDataOptions<TItem>) => NormalizedHeatmapData;
19
22
  export {};
@@ -7,10 +7,16 @@ type HeatmapStyleInput = Readonly<{
7
7
  }>;
8
8
  /** Movement page gradient (legacy Google heatmap), low to high density. */
9
9
  export declare const DEFAULT_HEATMAP_COLOR_RANGE: ReadonlyArray<string>;
10
+ export type ResolvedHeatmapStyle = Readonly<{
11
+ style: HeatmapRenderStyle;
12
+ /** Grouped messages for values that were replaced; emitted by `useDevWarnings`, never here. */
13
+ warnings: ReadonlyArray<string>;
14
+ }>;
10
15
  /**
11
16
  * Applies defaults and supported ranges so both adapters receive identical style values.
12
17
  *
13
- * Finite values are clamped; non-finite values fall back to the default with one warning.
18
+ * Finite values are clamped; non-finite values fall back to the default with one grouped
19
+ * warning. Pure: it runs inside a render-time `useMemo`, so it returns its warnings.
14
20
  */
15
- export declare const resolveHeatmapStyle: (style: HeatmapStyleInput | undefined) => HeatmapRenderStyle;
21
+ export declare const resolveHeatmapStyle: (style: HeatmapStyleInput | undefined) => ResolvedHeatmapStyle;
16
22
  export {};
@@ -2,7 +2,8 @@ import type { DroppedInvalidFillTilingFeature } from "./shapeFillTiling";
2
2
  type ReportDroppedInvalidFillTilingEvent = (payload: Record<string, unknown>) => void;
3
3
  /**
4
4
  * Report to Sentry each dropped invalid feature via `report`, at most once per handle/feature per session.
5
- * Note: `computeFillTiling` emits `console.warn` separately.
5
+ * Note: the dev warning for this case is emitted by `useShapeFillTiling` from its recompute
6
+ * path (`computeFillTiling` itself is pure and logs nothing).
6
7
  */
7
8
  export declare const captureDroppedInvalidFillTilingFeatures: (droppedInvalidFeatures: ReadonlyArray<DroppedInvalidFillTilingFeature>, layerHandleId: string, sessionReportedDedupeKeys: Set<string>, report: ReportDroppedInvalidFillTilingEvent) => void;
8
9
  export {};
@@ -3,7 +3,8 @@ type ReportFillTilingIntersectionFailureEvent = (payload: Record<string, unknown
3
3
  /**
4
4
  * Report to Sentry each feature pair whose overlap test threw inside `polygon-clipping`
5
5
  * (SAGA-743), at most once per handle/pair per session.
6
- * Note: `computeFillTiling` emits `console.warn` separately.
6
+ * Note: the dev warning for this case is emitted by `useShapeFillTiling` from its recompute
7
+ * path (`computeFillTiling` itself is pure and logs nothing).
7
8
  */
8
9
  export declare const captureFillTilingIntersectionFailures: (intersectionFailures: ReadonlyArray<FillTilingIntersectionFailure>, layerHandleId: string, sessionReportedDedupeKeys: Set<string>, report: ReportFillTilingIntersectionFailureEvent) => void;
9
10
  export {};
@@ -0,0 +1,3 @@
1
+ import type { DroppedInvalidFillTilingFeature } from "../shapeFillTiling";
2
+ /** Lists the ids of features dropped from fill tiling, or a placeholder when none carry an id. */
3
+ export declare const formatDroppedInvalidFeatureIds: (dropped: ReadonlyArray<DroppedInvalidFillTilingFeature>) => string;
@@ -0,0 +1,3 @@
1
+ import type { FillTilingIntersectionFailure } from "../shapeFillTiling";
2
+ /** Renders failing feature pairs as `a↔b, c↔d` for dev warnings and error reports. */
3
+ export declare const formatIntersectionFailurePairs: (failures: ReadonlyArray<FillTilingIntersectionFailure>) => string;
@@ -14,6 +14,7 @@ var translation = {
14
14
  "controls.stepper.increase": "Vergrößern",
15
15
  "controls.zoom": "Zoom",
16
16
  "draw.clear": "Clear drawing",
17
+ "draw.limitReached": "Polygon limit reached. Clear the drawing to draw again",
17
18
  "draw.polygon": "Draw polygon",
18
19
  "draw.select": "Edit polygon",
19
20
  "draw.selectDisabled": "Draw a polygon first",
@@ -22,6 +23,7 @@ var translation = {
22
23
  "map.loadingErrorMessage": "Die Karte konnte nicht geladen werden. Bitte versuchen Sie es später erneut.",
23
24
  "multiGeometry.badge.tooltip": "Teil eines zusammengesetzten Objekts",
24
25
  "settings.mapStyle.dark": "Dunkel",
26
+ "settings.mapStyle.disabledWhileDrawing": "Finish drawing to change the map style",
25
27
  "settings.mapStyle.light": "Hell",
26
28
  "settings.mapStyle.roadmap": "Straßenkarte",
27
29
  "settings.mapStyle.roads": "Straßen",
@@ -12,6 +12,7 @@ var translation = {
12
12
  "controls.stepper.increase": "Vergrößern",
13
13
  "controls.zoom": "Zoom",
14
14
  "draw.clear": "Clear drawing",
15
+ "draw.limitReached": "Polygon limit reached. Clear the drawing to draw again",
15
16
  "draw.polygon": "Draw polygon",
16
17
  "draw.select": "Edit polygon",
17
18
  "draw.selectDisabled": "Draw a polygon first",
@@ -20,6 +21,7 @@ var translation = {
20
21
  "map.loadingErrorMessage": "Die Karte konnte nicht geladen werden. Bitte versuchen Sie es später erneut.",
21
22
  "multiGeometry.badge.tooltip": "Teil eines zusammengesetzten Objekts",
22
23
  "settings.mapStyle.dark": "Dunkel",
24
+ "settings.mapStyle.disabledWhileDrawing": "Finish drawing to change the map style",
23
25
  "settings.mapStyle.light": "Hell",
24
26
  "settings.mapStyle.roadmap": "Straßenkarte",
25
27
  "settings.mapStyle.roads": "Straßen",
@@ -14,6 +14,7 @@ var translation = {
14
14
  "controls.stepper.increase": "Zwiększ",
15
15
  "controls.zoom": "Powiększ",
16
16
  "draw.clear": "Clear drawing",
17
+ "draw.limitReached": "Polygon limit reached. Clear the drawing to draw again",
17
18
  "draw.polygon": "Draw polygon",
18
19
  "draw.select": "Edit polygon",
19
20
  "draw.selectDisabled": "Draw a polygon first",
@@ -22,6 +23,7 @@ var translation = {
22
23
  "map.loadingErrorMessage": "Nie można załadować mapy. Spróbuj ponownie później.",
23
24
  "multiGeometry.badge.tooltip": "Część kształtu wieloczęściowego",
24
25
  "settings.mapStyle.dark": "Ciemny",
26
+ "settings.mapStyle.disabledWhileDrawing": "Finish drawing to change the map style",
25
27
  "settings.mapStyle.light": "Jasny",
26
28
  "settings.mapStyle.roadmap": "Mapa drogowa",
27
29
  "settings.mapStyle.roads": "Drogi",
@@ -12,6 +12,7 @@ var translation = {
12
12
  "controls.stepper.increase": "Zwiększ",
13
13
  "controls.zoom": "Powiększ",
14
14
  "draw.clear": "Clear drawing",
15
+ "draw.limitReached": "Polygon limit reached. Clear the drawing to draw again",
15
16
  "draw.polygon": "Draw polygon",
16
17
  "draw.select": "Edit polygon",
17
18
  "draw.selectDisabled": "Draw a polygon first",
@@ -20,6 +21,7 @@ var translation = {
20
21
  "map.loadingErrorMessage": "Nie można załadować mapy. Spróbuj ponownie później.",
21
22
  "multiGeometry.badge.tooltip": "Część kształtu wieloczęściowego",
22
23
  "settings.mapStyle.dark": "Ciemny",
24
+ "settings.mapStyle.disabledWhileDrawing": "Finish drawing to change the map style",
23
25
  "settings.mapStyle.light": "Jasny",
24
26
  "settings.mapStyle.roadmap": "Mapa drogowa",
25
27
  "settings.mapStyle.roads": "Drogi",
@@ -14,6 +14,7 @@ var translation = {
14
14
  "controls.stepper.increase": "Aumentar",
15
15
  "controls.zoom": "Ampliar",
16
16
  "draw.clear": "Clear drawing",
17
+ "draw.limitReached": "Polygon limit reached. Clear the drawing to draw again",
17
18
  "draw.polygon": "Draw polygon",
18
19
  "draw.select": "Edit polygon",
19
20
  "draw.selectDisabled": "Draw a polygon first",
@@ -22,6 +23,7 @@ var translation = {
22
23
  "map.loadingErrorMessage": "Não foi possível carregar o mapa. Tente novamente mais tarde.",
23
24
  "multiGeometry.badge.tooltip": "Parte de uma forma de várias partes",
24
25
  "settings.mapStyle.dark": "Escuro",
26
+ "settings.mapStyle.disabledWhileDrawing": "Finish drawing to change the map style",
25
27
  "settings.mapStyle.light": "Claro",
26
28
  "settings.mapStyle.roadmap": "Mapa de estrada",
27
29
  "settings.mapStyle.roads": "Estradas",
@@ -12,6 +12,7 @@ var translation = {
12
12
  "controls.stepper.increase": "Aumentar",
13
13
  "controls.zoom": "Ampliar",
14
14
  "draw.clear": "Clear drawing",
15
+ "draw.limitReached": "Polygon limit reached. Clear the drawing to draw again",
15
16
  "draw.polygon": "Draw polygon",
16
17
  "draw.select": "Edit polygon",
17
18
  "draw.selectDisabled": "Draw a polygon first",
@@ -20,6 +21,7 @@ var translation = {
20
21
  "map.loadingErrorMessage": "Não foi possível carregar o mapa. Tente novamente mais tarde.",
21
22
  "multiGeometry.badge.tooltip": "Parte de uma forma de várias partes",
22
23
  "settings.mapStyle.dark": "Escuro",
24
+ "settings.mapStyle.disabledWhileDrawing": "Finish drawing to change the map style",
23
25
  "settings.mapStyle.light": "Claro",
24
26
  "settings.mapStyle.roadmap": "Mapa de estrada",
25
27
  "settings.mapStyle.roads": "Estradas",
@@ -14,6 +14,7 @@ var translation = {
14
14
  "controls.stepper.increase": "Увеличить",
15
15
  "controls.zoom": "Масштаб",
16
16
  "draw.clear": "Clear drawing",
17
+ "draw.limitReached": "Polygon limit reached. Clear the drawing to draw again",
17
18
  "draw.polygon": "Draw polygon",
18
19
  "draw.select": "Edit polygon",
19
20
  "draw.selectDisabled": "Draw a polygon first",
@@ -22,6 +23,7 @@ var translation = {
22
23
  "map.loadingErrorMessage": "Не удается загрузить карту. Повторите попытку позже.",
23
24
  "multiGeometry.badge.tooltip": "Часть составной фигуры",
24
25
  "settings.mapStyle.dark": "Темная",
26
+ "settings.mapStyle.disabledWhileDrawing": "Finish drawing to change the map style",
25
27
  "settings.mapStyle.light": "Светлая",
26
28
  "settings.mapStyle.roadmap": "Карта маршрута",
27
29
  "settings.mapStyle.roads": "Дороги",