@dynatrace/strato-geo 3.6.0 → 3.7.0

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 (106) hide show
  1. package/esm/map/MapView.js +42 -34
  2. package/esm/map/MapView.js.map +2 -2
  3. package/esm/map/components/BubbleLayer/BubbleCircleLayer.js +2 -0
  4. package/esm/map/components/BubbleLayer/BubbleCircleLayer.js.map +2 -2
  5. package/esm/map/components/BubbleLayer/BubbleLayer.js +4 -1
  6. package/esm/map/components/BubbleLayer/BubbleLayer.js.map +2 -2
  7. package/esm/map/components/BubbleLayer/utils/parse-bubble-data-to-geo-json.js +5 -5
  8. package/esm/map/components/BubbleLayer/utils/parse-bubble-data-to-geo-json.js.map +2 -2
  9. package/esm/map/components/ChoroplethLayer/ChoroplethLayer.js +5 -2
  10. package/esm/map/components/ChoroplethLayer/ChoroplethLayer.js.map +2 -2
  11. package/esm/map/components/ChoroplethLayer/ChoroplethLayerTooltip.js.map +2 -2
  12. package/esm/map/components/ChoroplethLayer/ChoroplethOutlineLayer.js +3 -1
  13. package/esm/map/components/ChoroplethLayer/ChoroplethOutlineLayer.js.map +2 -2
  14. package/esm/map/components/ChoroplethLayer/utils/parse-region-data-to-geo-json.js +6 -5
  15. package/esm/map/components/ChoroplethLayer/utils/parse-region-data-to-geo-json.js.map +2 -2
  16. package/esm/map/components/ConnectionLayer/ConnectionLayer.js +1 -1
  17. package/esm/map/components/ConnectionLayer/ConnectionLayer.js.map +2 -2
  18. package/esm/map/components/ConnectionLayer/ConnectionLayerLine.js +3 -0
  19. package/esm/map/components/ConnectionLayer/ConnectionLayerLine.js.map +2 -2
  20. package/esm/map/components/ConnectionLayer/ConnectionLayerTooltip.js +7 -9
  21. package/esm/map/components/ConnectionLayer/ConnectionLayerTooltip.js.map +2 -2
  22. package/esm/map/components/ConnectionLayer/utils/parse-connection-data-to-geo-json.js +20 -18
  23. package/esm/map/components/ConnectionLayer/utils/parse-connection-data-to-geo-json.js.map +2 -2
  24. package/esm/map/components/DotLayer/DotLayer.js +4 -1
  25. package/esm/map/components/DotLayer/DotLayer.js.map +2 -2
  26. package/esm/map/components/DotLayer/DotLayerTooltip.js.map +2 -2
  27. package/esm/map/components/DotLayer/utils/parse-dot-data-to-geo-json.js +5 -5
  28. package/esm/map/components/DotLayer/utils/parse-dot-data-to-geo-json.js.map +2 -2
  29. package/esm/map/components/MapContent.js +22 -12
  30. package/esm/map/components/MapContent.js.map +2 -2
  31. package/esm/map/contexts/geo-data-lookup.context.js +8 -0
  32. package/esm/map/contexts/geo-data-lookup.context.js.map +7 -0
  33. package/esm/map/hooks/use-active-interaction.js +59 -43
  34. package/esm/map/hooks/use-active-interaction.js.map +2 -2
  35. package/esm/map/hooks/use-attach-image-from-icon.js +4 -2
  36. package/esm/map/hooks/use-attach-image-from-icon.js.map +2 -2
  37. package/esm/map/hooks/use-hover-interaction.js +59 -41
  38. package/esm/map/hooks/use-hover-interaction.js.map +2 -2
  39. package/esm/map/hooks/use-layer-before-id.js +24 -0
  40. package/esm/map/hooks/use-layer-before-id.js.map +7 -0
  41. package/esm/map/hooks/use-map-runtime-error.js +93 -0
  42. package/esm/map/hooks/use-map-runtime-error.js.map +7 -0
  43. package/esm/map/hooks/use-overlay-events.js +11 -2
  44. package/esm/map/hooks/use-overlay-events.js.map +2 -2
  45. package/esm/map/hooks/use-webgl-context-error.js +2 -1
  46. package/esm/map/hooks/use-webgl-context-error.js.map +2 -2
  47. package/esm/map/index.js.map +1 -1
  48. package/esm/map/slots/Tooltip.js.map +2 -2
  49. package/esm/map/utils/attach-image-from-shape.js +4 -2
  50. package/esm/map/utils/attach-image-from-shape.js.map +2 -2
  51. package/esm/map/utils/extract-layers-data.js +24 -15
  52. package/esm/map/utils/extract-layers-data.js.map +2 -2
  53. package/esm/map/utils/is-browser-firefox.js +7 -0
  54. package/esm/map/utils/is-browser-firefox.js.map +7 -0
  55. package/esm/map/utils/parse-tooltip-data.js +22 -7
  56. package/esm/map/utils/parse-tooltip-data.js.map +2 -2
  57. package/map/MapView.js +42 -34
  58. package/map/components/BubbleLayer/BubbleCircleLayer.d.ts +2 -1
  59. package/map/components/BubbleLayer/BubbleCircleLayer.js +2 -0
  60. package/map/components/BubbleLayer/BubbleLayer.js +4 -1
  61. package/map/components/BubbleLayer/utils/parse-bubble-data-to-geo-json.d.ts +3 -1
  62. package/map/components/BubbleLayer/utils/parse-bubble-data-to-geo-json.js +5 -5
  63. package/map/components/ChoroplethLayer/ChoroplethLayer.js +5 -2
  64. package/map/components/ChoroplethLayer/ChoroplethOutlineLayer.d.ts +1 -0
  65. package/map/components/ChoroplethLayer/ChoroplethOutlineLayer.js +3 -1
  66. package/map/components/ChoroplethLayer/utils/parse-region-data-to-geo-json.d.ts +3 -1
  67. package/map/components/ChoroplethLayer/utils/parse-region-data-to-geo-json.js +6 -5
  68. package/map/components/ConnectionLayer/ConnectionLayer.js +1 -1
  69. package/map/components/ConnectionLayer/ConnectionLayerLine.js +3 -0
  70. package/map/components/ConnectionLayer/ConnectionLayerTooltip.js +7 -9
  71. package/map/components/ConnectionLayer/utils/parse-connection-data-to-geo-json.d.ts +3 -1
  72. package/map/components/ConnectionLayer/utils/parse-connection-data-to-geo-json.js +20 -18
  73. package/map/components/DotLayer/DotLayer.js +4 -1
  74. package/map/components/DotLayer/utils/parse-dot-data-to-geo-json.d.ts +3 -1
  75. package/map/components/DotLayer/utils/parse-dot-data-to-geo-json.js +5 -5
  76. package/map/components/MapContent.js +21 -12
  77. package/map/contexts/geo-data-lookup.context.d.ts +9 -0
  78. package/map/{components/ConnectionLayer/utils/restore-null-props.js → contexts/geo-data-lookup.context.js} +8 -9
  79. package/map/hooks/use-active-interaction.d.ts +8 -1
  80. package/map/hooks/use-active-interaction.js +58 -42
  81. package/map/hooks/use-attach-image-from-icon.js +4 -2
  82. package/map/hooks/use-hover-interaction.d.ts +6 -2
  83. package/map/hooks/use-hover-interaction.js +52 -39
  84. package/map/hooks/use-layer-before-id.d.ts +13 -0
  85. package/map/hooks/{use-map-mouse-move.js → use-layer-before-id.js} +20 -15
  86. package/map/hooks/use-map-runtime-error.d.ts +34 -0
  87. package/map/hooks/use-map-runtime-error.js +112 -0
  88. package/map/hooks/use-overlay-events.js +11 -2
  89. package/map/hooks/use-webgl-context-error.js +2 -1
  90. package/map/slots/Tooltip.d.ts +2 -0
  91. package/map/types/connection-layer.d.ts +1 -8
  92. package/map/types/tooltip.d.ts +1 -0
  93. package/map/utils/attach-image-from-shape.js +4 -2
  94. package/map/utils/extract-layers-data.d.ts +2 -0
  95. package/map/utils/extract-layers-data.js +24 -15
  96. package/map/utils/is-browser-firefox.d.ts +5 -0
  97. package/map/utils/is-browser-firefox.js +26 -0
  98. package/map/utils/parse-tooltip-data.d.ts +11 -3
  99. package/map/utils/parse-tooltip-data.js +22 -7
  100. package/package.json +2 -2
  101. package/esm/map/components/ConnectionLayer/utils/restore-null-props.js +0 -9
  102. package/esm/map/components/ConnectionLayer/utils/restore-null-props.js.map +0 -7
  103. package/esm/map/hooks/use-map-mouse-move.js +0 -19
  104. package/esm/map/hooks/use-map-mouse-move.js.map +0 -7
  105. package/map/components/ConnectionLayer/utils/restore-null-props.d.ts +0 -2
  106. package/map/hooks/use-map-mouse-move.d.ts +0 -2
@@ -1,32 +1,45 @@
1
1
  import { useMap } from "@vis.gl/react-maplibre";
2
2
  import { isNil, isUndefined } from "lodash-es";
3
- import { useCallback, useEffect } from "react";
4
- import { BASE_LAYER_IDS } from "../constants.js";
3
+ import { useCallback, useEffect, useRef } from "react";
5
4
  import { getMinValueFeature } from "../utils/get-min-value-feature.js";
6
- const useActiveInteraction = () => {
5
+ const useActiveInteraction = (interactiveLayerIds) => {
7
6
  const map = useMap().current;
8
- let featureId;
9
- let sourceId;
7
+ const featureIdRef = useRef(void 0);
8
+ const sourceIdRef = useRef(void 0);
9
+ const interactiveLayerIdsRef = useRef(interactiveLayerIds);
10
+ interactiveLayerIdsRef.current = interactiveLayerIds;
10
11
  const handleClick = useCallback(
11
12
  ({ point }) => {
12
- const features = map.queryRenderedFeatures(point);
13
- const allFeatures = map.queryRenderedFeatures();
13
+ let features = [];
14
+ try {
15
+ features = map.queryRenderedFeatures(point, {
16
+ layers: interactiveLayerIdsRef.current
17
+ });
18
+ } catch {
19
+ return;
20
+ }
21
+ let allFeatures = [];
22
+ try {
23
+ allFeatures = map.queryRenderedFeatures(void 0, {
24
+ layers: interactiveLayerIdsRef.current
25
+ });
26
+ } catch {
27
+ }
14
28
  const layerId = features?.[0]?.layer?.id;
15
29
  const hasHoveredFeatures = !isNil(features) && features.length > 0 && !isUndefined(layerId);
16
- const isBaseLayer = BASE_LAYER_IDS.includes(layerId);
17
- if (hasHoveredFeatures && !isBaseLayer) {
18
- if (!isUndefined(featureId) && !isUndefined(sourceId)) {
30
+ if (hasHoveredFeatures) {
31
+ if (!isUndefined(featureIdRef.current) && !isUndefined(sourceIdRef.current)) {
19
32
  map.setFeatureState(
20
- { source: sourceId, id: featureId },
33
+ { source: sourceIdRef.current, id: featureIdRef.current },
21
34
  { active: false }
22
35
  );
23
36
  }
24
37
  const minFeature = getMinValueFeature(features);
25
- featureId = minFeature.id;
26
- sourceId = minFeature.layer.source;
38
+ featureIdRef.current = minFeature.id;
39
+ sourceIdRef.current = minFeature.layer.source;
27
40
  const activeState = features[0].state.active;
28
41
  map.setFeatureState(
29
- { source: sourceId, id: featureId },
42
+ { source: sourceIdRef.current, id: featureIdRef.current },
30
43
  { active: !activeState }
31
44
  );
32
45
  allFeatures.forEach((feature) => {
@@ -37,46 +50,49 @@ const useActiveInteraction = () => {
37
50
  );
38
51
  }
39
52
  });
40
- } else {
41
- if (!isUndefined(featureId) && !isUndefined(sourceId)) {
42
- map.setFeatureState(
43
- { source: sourceId, id: featureId },
44
- { active: false }
45
- );
46
- allFeatures.forEach((feature) => {
47
- if (feature.id !== void 0 && feature.layer.source !== void 0) {
48
- map.setFeatureState(
49
- { source: feature.layer.source, id: feature.id },
50
- { isAnyActive: false }
51
- );
52
- }
53
- });
54
- }
53
+ } else if (!isUndefined(featureIdRef.current) && !isUndefined(sourceIdRef.current)) {
54
+ map.setFeatureState(
55
+ { source: sourceIdRef.current, id: featureIdRef.current },
56
+ { active: false }
57
+ );
58
+ allFeatures.forEach((feature) => {
59
+ if (feature.id !== void 0 && feature.layer.source !== void 0) {
60
+ map.setFeatureState(
61
+ { source: feature.layer.source, id: feature.id },
62
+ { isAnyActive: false }
63
+ );
64
+ }
65
+ });
55
66
  }
56
67
  },
57
- [featureId, sourceId, map]
68
+ [map]
58
69
  );
59
70
  const onEscapeKeyPressedHandler = useCallback(
60
71
  (event) => {
61
72
  if (event.key === "Escape") {
62
- if (!isUndefined(featureId) && !isUndefined(sourceId)) {
73
+ if (!isUndefined(featureIdRef.current) && !isUndefined(sourceIdRef.current)) {
63
74
  map.setFeatureState(
64
- { source: sourceId, id: featureId },
75
+ { source: sourceIdRef.current, id: featureIdRef.current },
65
76
  { active: false }
66
77
  );
67
- const allFeatures = map.queryRenderedFeatures();
68
- allFeatures.forEach((feature) => {
69
- if (feature.id !== void 0 && feature.layer.source !== void 0) {
70
- map.setFeatureState(
71
- { source: feature.layer.source, id: feature.id },
72
- { isAnyActive: false }
73
- );
74
- }
75
- });
78
+ try {
79
+ const allFeatures = map.queryRenderedFeatures(void 0, {
80
+ layers: interactiveLayerIdsRef.current
81
+ });
82
+ allFeatures.forEach((feature) => {
83
+ if (feature.id !== void 0 && feature.layer.source !== void 0) {
84
+ map.setFeatureState(
85
+ { source: feature.layer.source, id: feature.id },
86
+ { isAnyActive: false }
87
+ );
88
+ }
89
+ });
90
+ } catch {
91
+ }
76
92
  }
77
93
  }
78
94
  },
79
- [featureId, sourceId, map]
95
+ [map]
80
96
  );
81
97
  useEffect(() => {
82
98
  map.on("click", handleClick);
@@ -85,7 +101,7 @@ const useActiveInteraction = () => {
85
101
  map.off("click", handleClick);
86
102
  window.removeEventListener("keyup", onEscapeKeyPressedHandler);
87
103
  };
88
- }, []);
104
+ }, [map, handleClick, onEscapeKeyPressedHandler]);
89
105
  };
90
106
  export {
91
107
  useActiveInteraction
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../../../src/map/hooks/use-active-interaction.ts"],
4
- "sourcesContent": ["import { useMap } from '@vis.gl/react-maplibre';\nimport { isNil, isUndefined } from 'lodash-es';\nimport type { MapLayerMouseEvent } from 'maplibre-gl';\nimport { useCallback, useEffect } from 'react';\n\nimport { BASE_LAYER_IDS } from '../constants.js';\nimport { getMinValueFeature } from '../utils/get-min-value-feature.js';\n\nexport const useActiveInteraction = () => {\n // eslint-disable-next-line @typescript-eslint/no-non-null-assertion\n const map = useMap().current!;\n\n let featureId: string | number | undefined;\n let sourceId: string | undefined;\n\n const handleClick = useCallback(\n ({ point }: MapLayerMouseEvent) => {\n const features = map.queryRenderedFeatures(point);\n const allFeatures = map.queryRenderedFeatures();\n\n const layerId = features?.[0]?.layer?.id;\n\n const hasHoveredFeatures =\n !isNil(features) && features.length > 0 && !isUndefined(layerId);\n const isBaseLayer = BASE_LAYER_IDS.includes(layerId);\n\n if (hasHoveredFeatures && !isBaseLayer) {\n if (!isUndefined(featureId) && !isUndefined(sourceId)) {\n // if there's already an active feature, remove the active state\n map.setFeatureState(\n { source: sourceId, id: featureId },\n { active: false },\n );\n }\n\n const minFeature = getMinValueFeature(features);\n\n featureId = minFeature.id;\n sourceId = minFeature.layer.source;\n const activeState = features[0].state.active;\n\n // add the active state to the closest feature\n map.setFeatureState(\n { source: sourceId, id: featureId },\n { active: !activeState },\n );\n allFeatures.forEach((feature) => {\n if (feature.id !== undefined && feature.layer.source !== undefined) {\n map.setFeatureState(\n { source: feature.layer.source, id: feature.id },\n { isAnyActive: !activeState },\n );\n }\n }); //TODO: change to inactive\n } else {\n if (!isUndefined(featureId) && !isUndefined(sourceId)) {\n // remove the active state from the last active feature\n map.setFeatureState(\n { source: sourceId, id: featureId },\n { active: false },\n );\n allFeatures.forEach((feature) => {\n if (\n feature.id !== undefined &&\n feature.layer.source !== undefined\n ) {\n map.setFeatureState(\n { source: feature.layer.source, id: feature.id },\n { isAnyActive: false },\n );\n }\n });\n }\n }\n },\n [featureId, sourceId, map],\n );\n\n const onEscapeKeyPressedHandler = useCallback(\n (event: KeyboardEvent) => {\n if (event.key === 'Escape') {\n if (!isUndefined(featureId) && !isUndefined(sourceId)) {\n map.setFeatureState(\n { source: sourceId, id: featureId },\n { active: false },\n );\n\n const allFeatures = map.queryRenderedFeatures();\n allFeatures.forEach((feature) => {\n if (\n feature.id !== undefined &&\n feature.layer.source !== undefined\n ) {\n map.setFeatureState(\n { source: feature.layer.source, id: feature.id },\n { isAnyActive: false },\n );\n }\n });\n }\n }\n },\n [featureId, sourceId, map],\n );\n\n useEffect(() => {\n map.on('click', handleClick);\n window.addEventListener('keyup', onEscapeKeyPressedHandler);\n return () => {\n map.off('click', handleClick);\n window.removeEventListener('keyup', onEscapeKeyPressedHandler);\n };\n }, []);\n};\n"],
5
- "mappings": "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",
4
+ "sourcesContent": ["import { useMap } from '@vis.gl/react-maplibre';\nimport { isNil, isUndefined } from 'lodash-es';\nimport type { MapLayerMouseEvent } from 'maplibre-gl';\nimport { useCallback, useEffect, useRef } from 'react';\n\nimport { getMinValueFeature } from '../utils/get-min-value-feature.js';\n\n/**\n * Handles click-to-activate interactions on map features.\n *\n * Scopes `queryRenderedFeatures` to `interactiveLayerIds` to prevent accidental\n * activation of base/background layers. Both query calls are try-caught to\n * contain maplibre-gl runtime errors without crashing the component tree.\n */\nexport const useActiveInteraction = (interactiveLayerIds: string[]) => {\n // eslint-disable-next-line @typescript-eslint/no-non-null-assertion\n const map = useMap().current!;\n\n /** Tracks the ID of the currently active feature across renders. */\n const featureIdRef = useRef<string | number | undefined>(undefined);\n /** Tracks the source of the currently active feature across renders. */\n const sourceIdRef = useRef<string | undefined>(undefined);\n /**\n * Kept in a ref so that stable callbacks always read the latest layer list\n * without needing to be recreated when the array reference changes.\n */\n const interactiveLayerIdsRef = useRef(interactiveLayerIds);\n interactiveLayerIdsRef.current = interactiveLayerIds;\n\n const handleClick = useCallback(\n ({ point }: MapLayerMouseEvent) => {\n let features: ReturnType<typeof map.queryRenderedFeatures> = [];\n\n try {\n features = map.queryRenderedFeatures(point, {\n layers: interactiveLayerIdsRef.current,\n });\n } catch {\n return;\n }\n\n // Keep the broad query for isAnyActive so all interactive features receive the flag.\n let allFeatures: ReturnType<typeof map.queryRenderedFeatures> = [];\n try {\n allFeatures = map.queryRenderedFeatures(undefined, {\n layers: interactiveLayerIdsRef.current,\n });\n } catch {\n // falls through with empty allFeatures\n }\n\n const layerId = features?.[0]?.layer?.id;\n\n const hasHoveredFeatures =\n !isNil(features) && features.length > 0 && !isUndefined(layerId);\n\n if (hasHoveredFeatures) {\n if (\n !isUndefined(featureIdRef.current) &&\n !isUndefined(sourceIdRef.current)\n ) {\n // If there's already an active feature, remove the active state.\n map.setFeatureState(\n { source: sourceIdRef.current, id: featureIdRef.current },\n { active: false },\n );\n }\n\n const minFeature = getMinValueFeature(features);\n\n featureIdRef.current = minFeature.id;\n sourceIdRef.current = minFeature.layer.source;\n const activeState = features[0].state.active;\n\n // Add the active state to the closest feature.\n map.setFeatureState(\n { source: sourceIdRef.current, id: featureIdRef.current },\n { active: !activeState },\n );\n allFeatures.forEach((feature) => {\n if (feature.id !== undefined && feature.layer.source !== undefined) {\n map.setFeatureState(\n { source: feature.layer.source, id: feature.id },\n { isAnyActive: !activeState },\n );\n }\n }); //TODO: change to inactive\n } else if (\n !isUndefined(featureIdRef.current) &&\n !isUndefined(sourceIdRef.current)\n ) {\n // Remove the active state from the last active feature.\n map.setFeatureState(\n { source: sourceIdRef.current, id: featureIdRef.current },\n { active: false },\n );\n allFeatures.forEach((feature) => {\n if (feature.id !== undefined && feature.layer.source !== undefined) {\n map.setFeatureState(\n { source: feature.layer.source, id: feature.id },\n { isAnyActive: false },\n );\n }\n });\n }\n },\n [map],\n );\n\n const onEscapeKeyPressedHandler = useCallback(\n (event: KeyboardEvent) => {\n if (event.key === 'Escape') {\n if (\n !isUndefined(featureIdRef.current) &&\n !isUndefined(sourceIdRef.current)\n ) {\n map.setFeatureState(\n { source: sourceIdRef.current, id: featureIdRef.current },\n { active: false },\n );\n\n try {\n const allFeatures = map.queryRenderedFeatures(undefined, {\n layers: interactiveLayerIdsRef.current,\n });\n allFeatures.forEach((feature) => {\n if (\n feature.id !== undefined &&\n feature.layer.source !== undefined\n ) {\n map.setFeatureState(\n { source: feature.layer.source, id: feature.id },\n { isAnyActive: false },\n );\n }\n });\n } catch {\n // falls through \u2014 active state is already cleared above\n }\n }\n }\n },\n [map],\n );\n\n useEffect(() => {\n map.on('click', handleClick);\n window.addEventListener('keyup', onEscapeKeyPressedHandler);\n return () => {\n map.off('click', handleClick);\n window.removeEventListener('keyup', onEscapeKeyPressedHandler);\n };\n }, [map, handleClick, onEscapeKeyPressedHandler]);\n};\n"],
5
+ "mappings": "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",
6
6
  "names": []
7
7
  }
@@ -8,6 +8,7 @@ import { attachImageToMap } from "../utils/attach-image-to-map.js";
8
8
  import { createBitmapConfigOptions } from "../utils/create-bitmap-config-options.js";
9
9
  import { getDataUri } from "../utils/get-data-uri.js";
10
10
  import { getScaledSymbolSize } from "../utils/get-scaled-symbol-size.js";
11
+ import { isFirefox } from "../utils/is-browser-firefox.js";
11
12
  const useAttachImageFromIcon = (icon, suffix, outputSize) => {
12
13
  const { elementAsString, ref } = useSafeSvgParser();
13
14
  const defaultScaledIconSize = getScaledSymbolSize();
@@ -25,14 +26,15 @@ const useAttachImageFromIcon = (icon, suffix, outputSize) => {
25
26
  fakeDomContainer.remove();
26
27
  const stringUrl = getDataUri(elementAsString);
27
28
  img.addEventListener("load", () => {
28
- createImageBitmap(
29
+ const bitmapPromise = isFirefox() ? createImageBitmap(img, createBitmapConfigOptions(outputSize)) : createImageBitmap(
29
30
  img,
30
31
  0,
31
32
  0,
32
33
  DEFAULT_INPUT_ICON_SIZE,
33
34
  DEFAULT_INPUT_ICON_SIZE,
34
35
  createBitmapConfigOptions(outputSize)
35
- ).then((bitmap) => {
36
+ );
37
+ bitmapPromise.then((bitmap) => {
36
38
  if (!map) {
37
39
  return;
38
40
  }
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../../../src/map/hooks/use-attach-image-from-icon.ts"],
4
- "sourcesContent": ["import { useMap } from '@vis.gl/react-maplibre';\nimport { isString } from 'lodash-es';\nimport { createElement, type ReactNode, useLayoutEffect } from 'react';\nimport { createRoot } from 'react-dom/client';\n\nimport { useSafeSvgParser } from './use-safe-svg-parser.js';\nimport { DEFAULT_INPUT_ICON_SIZE } from '../constants.js';\nimport type { MapShape } from '../types/shapes.js';\nimport { attachImageToMap } from '../utils/attach-image-to-map.js';\nimport { createBitmapConfigOptions } from '../utils/create-bitmap-config-options.js';\nimport { getDataUri } from '../utils/get-data-uri.js';\nimport { getScaledSymbolSize } from '../utils/get-scaled-symbol-size.js';\n\nexport const useAttachImageFromIcon = (\n icon: string | MapShape | ReactNode,\n suffix: string,\n outputSize?: number,\n) => {\n const { elementAsString, ref } = useSafeSvgParser();\n const defaultScaledIconSize = getScaledSymbolSize();\n\n // eslint-disable-next-line @typescript-eslint/no-non-null-assertion\n const { current: map } = useMap()!;\n const img = new Image(defaultScaledIconSize, defaultScaledIconSize);\n\n /*\n * As we can only create a reference in a React Node, but we need a virtual\n * 'root' element in the DOM to attach it, we create a 'div'\n * container that lately will hold the React Node. This is done to ensure\n * everything is cached prior any manipulation.\n */\n // Create a div element that acts as a container node in the DOM\n const fakeDomContainer = document.createElement('div');\n useLayoutEffect(() => {\n if (isString(icon)) {\n return;\n }\n // Create a React Node that will have a ref callback to be run when loaded and\n // the input icon as a children\n const reactNodeRefContainer = createElement('div', { ref }, icon);\n // Attach React Node with the ref callback into DOM Node\n const root = createRoot(fakeDomContainer);\n root.render(reactNodeRefContainer);\n }, [icon]);\n\n // Remove cached element to free resources\n fakeDomContainer.remove();\n\n const stringUrl = getDataUri(elementAsString);\n\n img.addEventListener('load', () => {\n createImageBitmap(\n img,\n 0,\n 0,\n DEFAULT_INPUT_ICON_SIZE,\n DEFAULT_INPUT_ICON_SIZE,\n createBitmapConfigOptions(outputSize),\n ).then((bitmap) => {\n if (!map) {\n return;\n }\n const iconName = `custom-icon-${suffix}`;\n attachImageToMap(map, bitmap, iconName);\n });\n });\n img.src = stringUrl;\n\n return !isString(icon);\n};\n"],
5
- "mappings": "AAAA,SAAS,cAAc;AACvB,SAAS,gBAAgB;AACzB,SAAS,eAA+B,uBAAuB;AAC/D,SAAS,kBAAkB;AAE3B,SAAS,wBAAwB;AACjC,SAAS,+BAA+B;AAExC,SAAS,wBAAwB;AACjC,SAAS,iCAAiC;AAC1C,SAAS,kBAAkB;AAC3B,SAAS,2BAA2B;AAE7B,MAAM,yBAAyB,CACpC,MACA,QACA,eACG;AACH,QAAM,EAAE,iBAAiB,IAAI,IAAI,iBAAiB;AAClD,QAAM,wBAAwB,oBAAoB;AAGlD,QAAM,EAAE,SAAS,IAAI,IAAI,OAAO;AAChC,QAAM,MAAM,IAAI,MAAM,uBAAuB,qBAAqB;AASlE,QAAM,mBAAmB,SAAS,cAAc,KAAK;AACrD,kBAAgB,MAAM;AACpB,QAAI,SAAS,IAAI,GAAG;AAClB;AAAA,IACF;AAGA,UAAM,wBAAwB,cAAc,OAAO,EAAE,IAAI,GAAG,IAAI;AAEhE,UAAM,OAAO,WAAW,gBAAgB;AACxC,SAAK,OAAO,qBAAqB;AAAA,EACnC,GAAG,CAAC,IAAI,CAAC;AAGT,mBAAiB,OAAO;AAExB,QAAM,YAAY,WAAW,eAAe;AAE5C,MAAI,iBAAiB,QAAQ,MAAM;AACjC;AAAA,MACE;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,MACA,0BAA0B,UAAU;AAAA,IACtC,EAAE,KAAK,CAAC,WAAW;AACjB,UAAI,CAAC,KAAK;AACR;AAAA,MACF;AACA,YAAM,WAAW,eAAe,MAAM;AACtC,uBAAiB,KAAK,QAAQ,QAAQ;AAAA,IACxC,CAAC;AAAA,EACH,CAAC;AACD,MAAI,MAAM;AAEV,SAAO,CAAC,SAAS,IAAI;AACvB;",
4
+ "sourcesContent": ["import { useMap } from '@vis.gl/react-maplibre';\nimport { isString } from 'lodash-es';\nimport { createElement, type ReactNode, useLayoutEffect } from 'react';\nimport { createRoot } from 'react-dom/client';\n\nimport { useSafeSvgParser } from './use-safe-svg-parser.js';\nimport { DEFAULT_INPUT_ICON_SIZE } from '../constants.js';\nimport type { MapShape } from '../types/shapes.js';\nimport { attachImageToMap } from '../utils/attach-image-to-map.js';\nimport { createBitmapConfigOptions } from '../utils/create-bitmap-config-options.js';\nimport { getDataUri } from '../utils/get-data-uri.js';\nimport { getScaledSymbolSize } from '../utils/get-scaled-symbol-size.js';\nimport { isFirefox } from '../utils/is-browser-firefox.js';\n\nexport const useAttachImageFromIcon = (\n icon: string | MapShape | ReactNode,\n suffix: string,\n outputSize?: number,\n) => {\n const { elementAsString, ref } = useSafeSvgParser();\n const defaultScaledIconSize = getScaledSymbolSize();\n\n // eslint-disable-next-line @typescript-eslint/no-non-null-assertion\n const { current: map } = useMap()!;\n const img = new Image(defaultScaledIconSize, defaultScaledIconSize);\n\n /*\n * As we can only create a reference in a React Node, but we need a virtual\n * 'root' element in the DOM to attach it, we create a 'div'\n * container that lately will hold the React Node. This is done to ensure\n * everything is cached prior any manipulation.\n */\n // Create a div element that acts as a container node in the DOM\n const fakeDomContainer = document.createElement('div');\n useLayoutEffect(() => {\n if (isString(icon)) {\n return;\n }\n // Create a React Node that will have a ref callback to be run when loaded and\n // the input icon as a children\n const reactNodeRefContainer = createElement('div', { ref }, icon);\n // Attach React Node with the ref callback into DOM Node\n const root = createRoot(fakeDomContainer);\n root.render(reactNodeRefContainer);\n }, [icon]);\n\n // Remove cached element to free resources\n fakeDomContainer.remove();\n\n const stringUrl = getDataUri(elementAsString);\n\n img.addEventListener('load', () => {\n // Firefox rasterizes SVG sources at the Image element's display size, not\n // at the SVG's intrinsic dimensions, so an explicit source rect clips the\n // icon. Drop the rect on Firefox only \u2014 other browsers keep the original\n // path.\n const bitmapPromise = isFirefox()\n ? createImageBitmap(img, createBitmapConfigOptions(outputSize))\n : createImageBitmap(\n img,\n 0,\n 0,\n DEFAULT_INPUT_ICON_SIZE,\n DEFAULT_INPUT_ICON_SIZE,\n createBitmapConfigOptions(outputSize),\n );\n bitmapPromise.then((bitmap) => {\n if (!map) {\n return;\n }\n const iconName = `custom-icon-${suffix}`;\n attachImageToMap(map, bitmap, iconName);\n });\n });\n img.src = stringUrl;\n\n return !isString(icon);\n};\n"],
5
+ "mappings": "AAAA,SAAS,cAAc;AACvB,SAAS,gBAAgB;AACzB,SAAS,eAA+B,uBAAuB;AAC/D,SAAS,kBAAkB;AAE3B,SAAS,wBAAwB;AACjC,SAAS,+BAA+B;AAExC,SAAS,wBAAwB;AACjC,SAAS,iCAAiC;AAC1C,SAAS,kBAAkB;AAC3B,SAAS,2BAA2B;AACpC,SAAS,iBAAiB;AAEnB,MAAM,yBAAyB,CACpC,MACA,QACA,eACG;AACH,QAAM,EAAE,iBAAiB,IAAI,IAAI,iBAAiB;AAClD,QAAM,wBAAwB,oBAAoB;AAGlD,QAAM,EAAE,SAAS,IAAI,IAAI,OAAO;AAChC,QAAM,MAAM,IAAI,MAAM,uBAAuB,qBAAqB;AASlE,QAAM,mBAAmB,SAAS,cAAc,KAAK;AACrD,kBAAgB,MAAM;AACpB,QAAI,SAAS,IAAI,GAAG;AAClB;AAAA,IACF;AAGA,UAAM,wBAAwB,cAAc,OAAO,EAAE,IAAI,GAAG,IAAI;AAEhE,UAAM,OAAO,WAAW,gBAAgB;AACxC,SAAK,OAAO,qBAAqB;AAAA,EACnC,GAAG,CAAC,IAAI,CAAC;AAGT,mBAAiB,OAAO;AAExB,QAAM,YAAY,WAAW,eAAe;AAE5C,MAAI,iBAAiB,QAAQ,MAAM;AAKjC,UAAM,gBAAgB,UAAU,IAC5B,kBAAkB,KAAK,0BAA0B,UAAU,CAAC,IAC5D;AAAA,MACE;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,MACA,0BAA0B,UAAU;AAAA,IACtC;AACJ,kBAAc,KAAK,CAAC,WAAW;AAC7B,UAAI,CAAC,KAAK;AACR;AAAA,MACF;AACA,YAAM,WAAW,eAAe,MAAM;AACtC,uBAAiB,KAAK,QAAQ,QAAQ;AAAA,IACxC,CAAC;AAAA,EACH,CAAC;AACD,MAAI,MAAM;AAEV,SAAO,CAAC,SAAS,IAAI;AACvB;",
6
6
  "names": []
7
7
  }
@@ -1,13 +1,25 @@
1
1
  import { useMap } from "@vis.gl/react-maplibre";
2
- import { isNil, isString, isUndefined } from "lodash-es";
3
- import { useCallback, useEffect } from "react";
4
- import { BASE_LAYER_IDS } from "../constants.js";
2
+ import { isNil, isString, isUndefined, throttle } from "lodash-es";
3
+ import {
4
+ useCallback,
5
+ useEffect,
6
+ useMemo,
7
+ useRef
8
+ } from "react";
5
9
  import {
6
10
  getAssociatedFeatures,
7
11
  hasAssociatedFeatures,
8
12
  isAssociatedFeature
9
13
  } from "../utils/associated-features.js";
10
14
  import { getMinValueFeature } from "../utils/get-min-value-feature.js";
15
+ const MOUSEMOVE_THROTTLE_MS = 16;
16
+ const blurTrackedFeature = (map, featureIdRef, sourceIdRef) => {
17
+ if (!isNil(featureIdRef.current) && !isNil(sourceIdRef.current)) {
18
+ blurFeature(map, sourceIdRef.current, featureIdRef.current);
19
+ featureIdRef.current = void 0;
20
+ sourceIdRef.current = void 0;
21
+ }
22
+ };
11
23
  const featureExists = (map, source, id) => {
12
24
  const isSourcePresent = map.getSource(source) !== void 0;
13
25
  return isSourcePresent && map.getFeatureState({
@@ -49,55 +61,61 @@ const hoverFeature = (map, source, id) => {
49
61
  }
50
62
  }
51
63
  };
52
- const useHoverInteraction = () => {
64
+ const useHoverInteraction = (interactiveLayerIds) => {
53
65
  const map = useMap().current;
54
- let featureId;
55
- let sourceId;
56
- const handleMouseOut = useCallback(
57
- ({ point }) => {
58
- if (!isNil(map) && !isUndefined(featureId) && !isUndefined(sourceId)) {
59
- blurFeature(map, sourceId, featureId);
60
- }
61
- },
62
- [featureId, sourceId, map]
63
- );
64
- const handleMouseMove = useCallback(
65
- ({ point }) => {
66
- if (!isNil(map)) {
67
- const features = map.queryRenderedFeatures(point).filter((feature) => !isAssociatedFeature(feature.properties.id));
68
- map.getCanvas().style.cursor = "grab";
66
+ const featureIdRef = useRef(void 0);
67
+ const sourceIdRef = useRef(void 0);
68
+ const interactiveLayerIdsRef = useRef(interactiveLayerIds);
69
+ interactiveLayerIdsRef.current = interactiveLayerIds;
70
+ const handleMouseOut = useCallback(() => {
71
+ if (!isNil(map)) {
72
+ blurTrackedFeature(map, featureIdRef, sourceIdRef);
73
+ }
74
+ }, [map]);
75
+ const throttledMouseMove = useMemo(
76
+ () => throttle(
77
+ ({ point }) => {
78
+ if (isNil(map)) {
79
+ return;
80
+ }
81
+ let features;
82
+ try {
83
+ features = map.queryRenderedFeatures(point, {
84
+ layers: interactiveLayerIdsRef.current
85
+ }).filter((feature) => !isAssociatedFeature(feature.properties.id));
86
+ } catch {
87
+ return;
88
+ }
69
89
  const layerId = features?.[0]?.layer?.id;
70
90
  const hasHoveredFeatures = !isNil(features) && features.length > 0 && !isUndefined(layerId);
71
- const isBaseLayer = BASE_LAYER_IDS.includes(layerId);
72
- if (hasHoveredFeatures && !isBaseLayer) {
73
- map.getCanvas().style.cursor = "pointer";
74
- if (!isUndefined(featureId) && !isUndefined(sourceId)) {
75
- blurFeature(map, sourceId, featureId);
76
- }
77
- const minFeature = getMinValueFeature(features);
78
- featureId = minFeature.id;
79
- sourceId = minFeature.layer.source;
80
- if (!isUndefined(featureId)) {
81
- hoverFeature(map, sourceId, featureId);
82
- }
83
- } else {
91
+ if (!hasHoveredFeatures) {
84
92
  map.getCanvas().style.cursor = "grab";
85
- if (!isUndefined(featureId) && !isUndefined(sourceId)) {
86
- blurFeature(map, sourceId, featureId);
87
- }
93
+ blurTrackedFeature(map, featureIdRef, sourceIdRef);
94
+ return;
88
95
  }
89
- }
90
- },
91
- [featureId, sourceId, map]
96
+ map.getCanvas().style.cursor = "pointer";
97
+ blurTrackedFeature(map, featureIdRef, sourceIdRef);
98
+ const minFeature = getMinValueFeature(features);
99
+ featureIdRef.current = minFeature.id;
100
+ sourceIdRef.current = minFeature.layer.source;
101
+ if (!isUndefined(featureIdRef.current)) {
102
+ hoverFeature(map, sourceIdRef.current, featureIdRef.current);
103
+ }
104
+ },
105
+ MOUSEMOVE_THROTTLE_MS,
106
+ { trailing: true }
107
+ ),
108
+ [map]
92
109
  );
93
110
  useEffect(() => {
94
- map?.on("mousemove", handleMouseMove);
111
+ map?.on("mousemove", throttledMouseMove);
95
112
  map?.on("mouseout", handleMouseOut);
96
113
  return () => {
97
- map?.off("mousemove", handleMouseMove);
114
+ throttledMouseMove.cancel();
115
+ map?.off("mousemove", throttledMouseMove);
98
116
  map?.off("mouseout", handleMouseOut);
99
117
  };
100
- }, []);
118
+ }, [map, throttledMouseMove, handleMouseOut]);
101
119
  };
102
120
  export {
103
121
  useHoverInteraction
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../../../src/map/hooks/use-hover-interaction.ts"],
4
- "sourcesContent": ["import type { MapRef } from '@vis.gl/react-maplibre';\nimport { useMap } from '@vis.gl/react-maplibre';\nimport { isNil, isString, isUndefined } from 'lodash-es';\nimport type { MapLayerMouseEvent } from 'maplibre-gl';\nimport { useCallback, useEffect } from 'react';\n\nimport { BASE_LAYER_IDS } from '../constants.js';\nimport {\n getAssociatedFeatures,\n hasAssociatedFeatures,\n isAssociatedFeature,\n} from '../utils/associated-features.js';\nimport { getMinValueFeature } from '../utils/get-min-value-feature.js';\n\n/**\n * Checks whether a feature exists from a given source\n *\n * @param map -\n * @param source -\n * @param id -\n */\nconst featureExists = (\n map: MapRef,\n source: string,\n id: string | number,\n): boolean => {\n const isSourcePresent = map.getSource(source) !== undefined;\n\n return (\n isSourcePresent &&\n map.getFeatureState({\n source,\n id,\n }) !== undefined\n );\n};\n\n/**\n * Removes hovered state from a feature and its associated features from a given source\n * @param map -\n * @param source -\n * @param id -\n */\nconst blurFeature = (map: MapRef, source: string, id: string | number) => {\n map.setFeatureState({ source, id }, { hover: false });\n\n if (isString(id) && hasAssociatedFeatures(id)) {\n for (const associatedFeature of getAssociatedFeatures('connection')) {\n const associatedSource = `${source}-direction`;\n const associatedId = `${id}-${associatedFeature}`;\n\n if (featureExists(map, associatedSource, associatedId)) {\n map.setFeatureState(\n { source: associatedSource, id: associatedId },\n {\n hover: false,\n },\n );\n }\n }\n }\n};\n\n/**\n * Sets hovered state to a feature and its associated features from a given source\n * @param map -\n * @param source -\n * @param id -\n */\nconst hoverFeature = (map: MapRef, source: string, id: string | number) => {\n map.setFeatureState({ source, id }, { hover: true });\n\n if (isString(id) && hasAssociatedFeatures(id)) {\n for (const associatedFeature of getAssociatedFeatures('connection')) {\n const associatedSource = `${source}-direction`;\n const associatedId = `${id}-${associatedFeature}`;\n\n if (featureExists(map, associatedSource, associatedId)) {\n map.setFeatureState(\n { source: `${source}-direction`, id: `${id}-${associatedFeature}` },\n {\n hover: true,\n },\n );\n }\n }\n }\n};\n\n/**\n * Sets and removes hovered state to the features depending on mouse position\n */\nexport const useHoverInteraction = () => {\n const map = useMap().current;\n\n let featureId: string | number | undefined;\n let sourceId: string | undefined;\n\n const handleMouseOut = useCallback(\n ({ point }: MapLayerMouseEvent) => {\n if (!isNil(map) && !isUndefined(featureId) && !isUndefined(sourceId)) {\n blurFeature(map, sourceId, featureId);\n }\n },\n [featureId, sourceId, map],\n );\n\n const handleMouseMove = useCallback(\n ({ point }: MapLayerMouseEvent) => {\n if (!isNil(map)) {\n const features = map\n .queryRenderedFeatures(point)\n .filter((feature) => !isAssociatedFeature(feature.properties.id)); // associated features should only have hover state when the main feature is hovered\n\n map.getCanvas().style.cursor = 'grab';\n const layerId = features?.[0]?.layer?.id;\n\n const hasHoveredFeatures =\n !isNil(features) && features.length > 0 && !isUndefined(layerId);\n const isBaseLayer = BASE_LAYER_IDS.includes(layerId);\n\n if (hasHoveredFeatures && !isBaseLayer) {\n map.getCanvas().style.cursor = 'pointer';\n if (!isUndefined(featureId) && !isUndefined(sourceId)) {\n // if there's already a hovered feature, remove the hover state\n blurFeature(map, sourceId, featureId);\n }\n\n const minFeature = getMinValueFeature(features);\n\n featureId = minFeature.id;\n sourceId = minFeature.layer.source;\n\n // add the hover state to the closest feature\n if (!isUndefined(featureId)) {\n hoverFeature(map, sourceId, featureId);\n }\n } else {\n map.getCanvas().style.cursor = 'grab';\n\n if (!isUndefined(featureId) && !isUndefined(sourceId)) {\n // remove the active state from the last hovered feature\n blurFeature(map, sourceId, featureId);\n }\n }\n }\n },\n [featureId, sourceId, map],\n );\n\n useEffect(() => {\n map?.on('mousemove', handleMouseMove);\n map?.on('mouseout', handleMouseOut);\n return () => {\n map?.off('mousemove', handleMouseMove);\n map?.off('mouseout', handleMouseOut);\n };\n }, []);\n};\n"],
5
- "mappings": "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",
4
+ "sourcesContent": ["import type { MapRef } from '@vis.gl/react-maplibre';\nimport { useMap } from '@vis.gl/react-maplibre';\nimport { isNil, isString, isUndefined, throttle } from 'lodash-es';\nimport type { MapLayerMouseEvent } from 'maplibre-gl';\nimport {\n type MutableRefObject,\n useCallback,\n useEffect,\n useMemo,\n useRef,\n} from 'react';\n\nimport {\n getAssociatedFeatures,\n hasAssociatedFeatures,\n isAssociatedFeature,\n} from '../utils/associated-features.js';\nimport { getMinValueFeature } from '../utils/get-min-value-feature.js';\n\n/** Minimum milliseconds between processed mousemove events (~1 frame at 60 fps). */\nconst MOUSEMOVE_THROTTLE_MS = 16;\n\nconst blurTrackedFeature = (\n map: MapRef,\n featureIdRef: MutableRefObject<string | number | undefined>,\n sourceIdRef: MutableRefObject<string | undefined>,\n) => {\n if (!isNil(featureIdRef.current) && !isNil(sourceIdRef.current)) {\n blurFeature(map, sourceIdRef.current, featureIdRef.current);\n featureIdRef.current = undefined;\n sourceIdRef.current = undefined;\n }\n};\n\n/**\n * Checks whether a feature exists from a given source\n *\n * @param map -\n * @param source -\n * @param id -\n */\nconst featureExists = (\n map: MapRef,\n source: string,\n id: string | number,\n): boolean => {\n const isSourcePresent = map.getSource(source) !== undefined;\n\n return (\n isSourcePresent &&\n map.getFeatureState({\n source,\n id,\n }) !== undefined\n );\n};\n\n/**\n * Removes hovered state from a feature and its associated features from a given source\n * @param map -\n * @param source -\n * @param id -\n */\nconst blurFeature = (map: MapRef, source: string, id: string | number) => {\n map.setFeatureState({ source, id }, { hover: false });\n\n if (isString(id) && hasAssociatedFeatures(id)) {\n for (const associatedFeature of getAssociatedFeatures('connection')) {\n const associatedSource = `${source}-direction`;\n const associatedId = `${id}-${associatedFeature}`;\n\n if (featureExists(map, associatedSource, associatedId)) {\n map.setFeatureState(\n { source: associatedSource, id: associatedId },\n {\n hover: false,\n },\n );\n }\n }\n }\n};\n\n/**\n * Sets hovered state to a feature and its associated features from a given source\n * @param map -\n * @param source -\n * @param id -\n */\nconst hoverFeature = (map: MapRef, source: string, id: string | number) => {\n map.setFeatureState({ source, id }, { hover: true });\n\n if (isString(id) && hasAssociatedFeatures(id)) {\n for (const associatedFeature of getAssociatedFeatures('connection')) {\n const associatedSource = `${source}-direction`;\n const associatedId = `${id}-${associatedFeature}`;\n\n if (featureExists(map, associatedSource, associatedId)) {\n map.setFeatureState(\n { source: `${source}-direction`, id: `${id}-${associatedFeature}` },\n {\n hover: true,\n },\n );\n }\n }\n }\n};\n\n/**\n * Sets and removes hovered state to the features depending on mouse position.\n *\n * @param interactiveLayerIds - maplibre-gl layer IDs to scope the feature query.\n * Scoping to data layers prevents touching base-map vector tiles whose internal\n * feature index may be in a transitional state (root cause of APPDEV-17854).\n */\nexport const useHoverInteraction = (interactiveLayerIds: string[]) => {\n const map = useMap().current;\n\n const featureIdRef = useRef<string | number | undefined>(undefined);\n const sourceIdRef = useRef<string | undefined>(undefined);\n\n // Keep a ref so the stable callback always sees the latest layer IDs even\n // if the consumer updates them after the first render.\n const interactiveLayerIdsRef = useRef(interactiveLayerIds);\n interactiveLayerIdsRef.current = interactiveLayerIds;\n\n const handleMouseOut = useCallback(() => {\n if (!isNil(map)) {\n blurTrackedFeature(map, featureIdRef, sourceIdRef);\n }\n }, [map]);\n\n const throttledMouseMove = useMemo(\n () =>\n throttle(\n ({ point }: MapLayerMouseEvent) => {\n if (isNil(map)) {\n return;\n }\n\n let features;\n try {\n features = map\n .queryRenderedFeatures(point, {\n layers: interactiveLayerIdsRef.current,\n })\n .filter((feature) => !isAssociatedFeature(feature.properties.id));\n } catch {\n return;\n }\n\n const layerId = features?.[0]?.layer?.id;\n const hasHoveredFeatures =\n !isNil(features) && features.length > 0 && !isUndefined(layerId);\n\n // No BASE_LAYER_IDS check needed: querying only interactiveLayerIds\n // already excludes base-map layers entirely.\n if (!hasHoveredFeatures) {\n map.getCanvas().style.cursor = 'grab';\n blurTrackedFeature(map, featureIdRef, sourceIdRef);\n return;\n }\n\n map.getCanvas().style.cursor = 'pointer';\n blurTrackedFeature(map, featureIdRef, sourceIdRef);\n\n const minFeature = getMinValueFeature(features);\n featureIdRef.current = minFeature.id;\n sourceIdRef.current = minFeature.layer.source;\n\n if (!isUndefined(featureIdRef.current)) {\n hoverFeature(map, sourceIdRef.current, featureIdRef.current);\n }\n },\n MOUSEMOVE_THROTTLE_MS,\n { trailing: true },\n ),\n [map],\n );\n\n useEffect(() => {\n map?.on('mousemove', throttledMouseMove);\n map?.on('mouseout', handleMouseOut);\n return () => {\n throttledMouseMove.cancel();\n map?.off('mousemove', throttledMouseMove);\n map?.off('mouseout', handleMouseOut);\n };\n }, [map, throttledMouseMove, handleMouseOut]);\n};\n"],
5
+ "mappings": "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",
6
6
  "names": []
7
7
  }
@@ -0,0 +1,24 @@
1
+ import { useMap } from "@vis.gl/react-maplibre";
2
+ import { useContext } from "react";
3
+ import { LayerIdsContext } from "../contexts/layer-ids.context.js";
4
+ const useLayerBeforeId = (layerId) => {
5
+ const layerIds = useContext(LayerIdsContext);
6
+ const { current: map } = useMap();
7
+ if (!map) {
8
+ return void 0;
9
+ }
10
+ const currentIndex = layerIds.indexOf(layerId);
11
+ if (currentIndex === -1) {
12
+ return void 0;
13
+ }
14
+ for (let i = currentIndex + 1; i < layerIds.length; i++) {
15
+ if (map.getLayer(layerIds[i])) {
16
+ return layerIds[i];
17
+ }
18
+ }
19
+ return void 0;
20
+ };
21
+ export {
22
+ useLayerBeforeId
23
+ };
24
+ //# sourceMappingURL=use-layer-before-id.js.map
@@ -0,0 +1,7 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../../../../src/map/hooks/use-layer-before-id.ts"],
4
+ "sourcesContent": ["import { useMap } from '@vis.gl/react-maplibre';\nimport { useContext } from 'react';\n\nimport { LayerIdsContext } from '../contexts/layer-ids.context.js';\n\n/**\n * Returns the maplibre layer id that the current layer should be inserted before,\n * based on the JSX order of `MapView` children.\n *\n * Without this, layers that mount asynchronously (e.g. `{apiData && <ChoroplethLayer>}`)\n * are appended to the top of the maplibre stack and end up visually above siblings\n * that mounted earlier \u2014 even when JSX puts them first. Passing the returned value as\n * `beforeId` to a `<Layer>` keeps the rendered stack aligned with JSX order.\n *\n * Only returns an id that actually exists in the map right now, so the result is\n * always safe to pass to `map.addLayer(opts, beforeId)`.\n */\nexport const useLayerBeforeId = (layerId: string): string | undefined => {\n const layerIds = useContext(LayerIdsContext);\n const { current: map } = useMap();\n\n if (!map) {\n return undefined;\n }\n\n const currentIndex = layerIds.indexOf(layerId);\n if (currentIndex === -1) {\n return undefined;\n }\n\n for (let i = currentIndex + 1; i < layerIds.length; i++) {\n if (map.getLayer(layerIds[i])) {\n return layerIds[i];\n }\n }\n\n return undefined;\n};\n"],
5
+ "mappings": "AAAA,SAAS,cAAc;AACvB,SAAS,kBAAkB;AAE3B,SAAS,uBAAuB;AAczB,MAAM,mBAAmB,CAAC,YAAwC;AACvE,QAAM,WAAW,WAAW,eAAe;AAC3C,QAAM,EAAE,SAAS,IAAI,IAAI,OAAO;AAEhC,MAAI,CAAC,KAAK;AACR,WAAO;AAAA,EACT;AAEA,QAAM,eAAe,SAAS,QAAQ,OAAO;AAC7C,MAAI,iBAAiB,IAAI;AACvB,WAAO;AAAA,EACT;AAEA,WAAS,IAAI,eAAe,GAAG,IAAI,SAAS,QAAQ,KAAK;AACvD,QAAI,IAAI,SAAS,SAAS,CAAC,CAAC,GAAG;AAC7B,aAAO,SAAS,CAAC;AAAA,IACnB;AAAA,EACF;AAEA,SAAO;AACT;",
6
+ "names": []
7
+ }
@@ -0,0 +1,93 @@
1
+ import { useMap } from "@vis.gl/react-maplibre";
2
+ import { useCallback, useEffect, useRef } from "react";
3
+ import { useWebGLContextError } from "./use-webgl-context-error.js";
4
+ const MAX_RETRIES = 3;
5
+ const ERROR_DEBOUNCE_MS = 500;
6
+ const MAPLIBRE_RUNTIME_ERROR_PATTERNS = [
7
+ "Could not compile fragment shader",
8
+ "Could not compile vertex shader",
9
+ "Program failed to link",
10
+ "feature index out of bounds",
11
+ "Out of bounds. Index requested"
12
+ ];
13
+ const isMaplibreRuntimeError = (message) => MAPLIBRE_RUNTIME_ERROR_PATTERNS.some((pattern) => message.includes(pattern));
14
+ const useMapRuntimeError = ({
15
+ onError,
16
+ onRetry
17
+ }) => {
18
+ const { current: mapRef } = useMap();
19
+ const retryCountRef = useRef(0);
20
+ const lastErrorTimeRef = useRef(0);
21
+ const isMapActiveRef = useRef(true);
22
+ const onErrorRef = useRef(onError);
23
+ onErrorRef.current = onError;
24
+ const onRetryRef = useRef(onRetry);
25
+ onRetryRef.current = onRetry;
26
+ const handleError = useCallback(() => {
27
+ const now = Date.now();
28
+ if (now - lastErrorTimeRef.current < ERROR_DEBOUNCE_MS) {
29
+ return;
30
+ }
31
+ lastErrorTimeRef.current = now;
32
+ retryCountRef.current += 1;
33
+ if (retryCountRef.current >= MAX_RETRIES) {
34
+ onErrorRef.current();
35
+ } else {
36
+ onRetryRef.current?.(retryCountRef.current);
37
+ }
38
+ }, []);
39
+ useWebGLContextError(handleError);
40
+ useEffect(() => {
41
+ const map = mapRef?.getMap();
42
+ if (!map) {
43
+ return;
44
+ }
45
+ const handleMapError = ({ error }) => {
46
+ if (isMaplibreRuntimeError(error.message)) {
47
+ handleError();
48
+ }
49
+ };
50
+ map.on("error", handleMapError);
51
+ return () => {
52
+ map.off("error", handleMapError);
53
+ };
54
+ }, [mapRef, handleError]);
55
+ useEffect(() => {
56
+ const handleWindowError = (event) => {
57
+ if (!isMapActiveRef.current) {
58
+ return;
59
+ }
60
+ if (isMaplibreRuntimeError(event.message)) {
61
+ event.preventDefault();
62
+ handleError();
63
+ }
64
+ };
65
+ window.addEventListener("error", handleWindowError);
66
+ return () => {
67
+ window.removeEventListener("error", handleWindowError);
68
+ };
69
+ }, [handleError]);
70
+ useEffect(() => {
71
+ const map = mapRef?.getMap();
72
+ if (!map) {
73
+ return;
74
+ }
75
+ const handleRender = () => {
76
+ isMapActiveRef.current = true;
77
+ };
78
+ const handleIdle = () => {
79
+ isMapActiveRef.current = false;
80
+ retryCountRef.current = 0;
81
+ };
82
+ map.on("render", handleRender);
83
+ map.on("idle", handleIdle);
84
+ return () => {
85
+ map.off("render", handleRender);
86
+ map.off("idle", handleIdle);
87
+ };
88
+ }, [mapRef]);
89
+ };
90
+ export {
91
+ useMapRuntimeError
92
+ };
93
+ //# sourceMappingURL=use-map-runtime-error.js.map
@@ -0,0 +1,7 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../../../../src/map/hooks/use-map-runtime-error.ts"],
4
+ "sourcesContent": ["import { useMap } from '@vis.gl/react-maplibre';\nimport { useCallback, useEffect, useRef } from 'react';\n\nimport { useWebGLContextError } from './use-webgl-context-error.js';\n\nconst MAX_RETRIES = 3;\n\n/** Prevents a burst of identical errors from exhausting the retry budget in one frame. */\nconst ERROR_DEBOUNCE_MS = 500;\n\n/**\n * Patterns that identify errors thrown from the maplibre-gl internal render\n * pipeline (shader compilation, program linking, vector-tile feature decoding).\n * These originate from rAF callbacks or from maplibre's own error event and\n * are not caused by consumer code.\n *\n * APPDEV-17938\n */\nconst MAPLIBRE_RUNTIME_ERROR_PATTERNS: readonly string[] = [\n 'Could not compile fragment shader',\n 'Could not compile vertex shader',\n 'Program failed to link',\n 'feature index out of bounds',\n 'Out of bounds. Index requested',\n];\n\nconst isMaplibreRuntimeError = (message: string): boolean =>\n MAPLIBRE_RUNTIME_ERROR_PATTERNS.some((pattern) => message.includes(pattern));\n\nexport interface UseMapRuntimeErrorOptions {\n /**\n * Called when the retry budget (3 attempts) is exhausted and the map cannot\n * recover. Throwing inside this callback will propagate to the nearest React\n * ErrorBoundary, showing the fallback UI.\n */\n onError: () => void;\n /**\n * Optional callback invoked on each recoverable error attempt before the\n * budget is exhausted. The `attempt` argument is 1-indexed.\n */\n onRetry?: (attempt: number) => void;\n}\n\n/**\n * Unified hook that guards MapView against maplibre-gl runtime errors from\n * three distinct sources:\n *\n * 1. **WebGL context loss** (`webglcontextlost` on the canvas) \u2014 composed via\n * `useWebGLContextError`.\n * 2. **maplibre's own error events** (`map.on('error', ...)`), covering tile\n * load failures and other internally dispatched errors.\n * 3. **Synchronous throws from the rAF render loop** that escape to\n * `window.onerror` \u2014 shader compilation, program link, and feature-index\n * out-of-bounds errors that maplibre does not catch internally.\n *\n * A shared retry counter (max 3) is maintained across all sources. Error\n * counting is debounced at 500 ms so that a burst of errors within a single\n * render frame counts as one occurrence. The counter resets whenever the map\n * fires an `idle` event (indicating a successful render). Once the budget is\n * exhausted, `onError` is invoked.\n *\n * APPDEV-17938\n */\nexport const useMapRuntimeError = ({\n onError,\n onRetry,\n}: UseMapRuntimeErrorOptions) => {\n const { current: mapRef } = useMap();\n const retryCountRef = useRef(0);\n const lastErrorTimeRef = useRef(0);\n // Tracks whether this map instance is actively rendering. Without this guard,\n // all mounted <MapView> instances would increment on a single window error\n // event. Initialised to true so early errors (before first idle) are caught.\n const isMapActiveRef = useRef(true);\n\n const onErrorRef = useRef(onError);\n onErrorRef.current = onError;\n const onRetryRef = useRef(onRetry);\n onRetryRef.current = onRetry;\n\n const handleError = useCallback(() => {\n const now = Date.now();\n\n if (now - lastErrorTimeRef.current < ERROR_DEBOUNCE_MS) {\n return;\n }\n\n lastErrorTimeRef.current = now;\n retryCountRef.current += 1;\n\n if (retryCountRef.current >= MAX_RETRIES) {\n onErrorRef.current();\n } else {\n onRetryRef.current?.(retryCountRef.current);\n }\n }, []);\n\n useWebGLContextError(handleError);\n\n useEffect(() => {\n const map = mapRef?.getMap();\n if (!map) {\n return;\n }\n\n const handleMapError = ({ error }: { error: Error }) => {\n if (isMaplibreRuntimeError(error.message)) {\n handleError();\n }\n };\n\n map.on('error', handleMapError);\n return () => {\n map.off('error', handleMapError);\n };\n }, [mapRef, handleError]);\n\n // rAF render-loop throws escape to window because they run outside React's\n // render cycle and are not caught by component-level error boundaries.\n useEffect(() => {\n const handleWindowError = (event: ErrorEvent) => {\n if (!isMapActiveRef.current) {\n return;\n }\n if (isMaplibreRuntimeError(event.message)) {\n // Stops the red console overlay in dev and top-level error boundaries.\n event.preventDefault();\n handleError();\n }\n };\n\n window.addEventListener('error', handleWindowError);\n return () => {\n window.removeEventListener('error', handleWindowError);\n };\n }, [handleError]);\n\n useEffect(() => {\n const map = mapRef?.getMap();\n if (!map) {\n return;\n }\n\n const handleRender = () => {\n isMapActiveRef.current = true;\n };\n\n const handleIdle = () => {\n isMapActiveRef.current = false;\n retryCountRef.current = 0;\n };\n\n map.on('render', handleRender);\n map.on('idle', handleIdle);\n return () => {\n map.off('render', handleRender);\n map.off('idle', handleIdle);\n };\n }, [mapRef]);\n};\n"],
5
+ "mappings": "AAAA,SAAS,cAAc;AACvB,SAAS,aAAa,WAAW,cAAc;AAE/C,SAAS,4BAA4B;AAErC,MAAM,cAAc;AAGpB,MAAM,oBAAoB;AAU1B,MAAM,kCAAqD;AAAA,EACzD;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AACF;AAEA,MAAM,yBAAyB,CAAC,YAC9B,gCAAgC,KAAK,CAAC,YAAY,QAAQ,SAAS,OAAO,CAAC;AAoCtE,MAAM,qBAAqB,CAAC;AAAA,EACjC;AAAA,EACA;AACF,MAAiC;AAC/B,QAAM,EAAE,SAAS,OAAO,IAAI,OAAO;AACnC,QAAM,gBAAgB,OAAO,CAAC;AAC9B,QAAM,mBAAmB,OAAO,CAAC;AAIjC,QAAM,iBAAiB,OAAO,IAAI;AAElC,QAAM,aAAa,OAAO,OAAO;AACjC,aAAW,UAAU;AACrB,QAAM,aAAa,OAAO,OAAO;AACjC,aAAW,UAAU;AAErB,QAAM,cAAc,YAAY,MAAM;AACpC,UAAM,MAAM,KAAK,IAAI;AAErB,QAAI,MAAM,iBAAiB,UAAU,mBAAmB;AACtD;AAAA,IACF;AAEA,qBAAiB,UAAU;AAC3B,kBAAc,WAAW;AAEzB,QAAI,cAAc,WAAW,aAAa;AACxC,iBAAW,QAAQ;AAAA,IACrB,OAAO;AACL,iBAAW,UAAU,cAAc,OAAO;AAAA,IAC5C;AAAA,EACF,GAAG,CAAC,CAAC;AAEL,uBAAqB,WAAW;AAEhC,YAAU,MAAM;AACd,UAAM,MAAM,QAAQ,OAAO;AAC3B,QAAI,CAAC,KAAK;AACR;AAAA,IACF;AAEA,UAAM,iBAAiB,CAAC,EAAE,MAAM,MAAwB;AACtD,UAAI,uBAAuB,MAAM,OAAO,GAAG;AACzC,oBAAY;AAAA,MACd;AAAA,IACF;AAEA,QAAI,GAAG,SAAS,cAAc;AAC9B,WAAO,MAAM;AACX,UAAI,IAAI,SAAS,cAAc;AAAA,IACjC;AAAA,EACF,GAAG,CAAC,QAAQ,WAAW,CAAC;AAIxB,YAAU,MAAM;AACd,UAAM,oBAAoB,CAAC,UAAsB;AAC/C,UAAI,CAAC,eAAe,SAAS;AAC3B;AAAA,MACF;AACA,UAAI,uBAAuB,MAAM,OAAO,GAAG;AAEzC,cAAM,eAAe;AACrB,oBAAY;AAAA,MACd;AAAA,IACF;AAEA,WAAO,iBAAiB,SAAS,iBAAiB;AAClD,WAAO,MAAM;AACX,aAAO,oBAAoB,SAAS,iBAAiB;AAAA,IACvD;AAAA,EACF,GAAG,CAAC,WAAW,CAAC;AAEhB,YAAU,MAAM;AACd,UAAM,MAAM,QAAQ,OAAO;AAC3B,QAAI,CAAC,KAAK;AACR;AAAA,IACF;AAEA,UAAM,eAAe,MAAM;AACzB,qBAAe,UAAU;AAAA,IAC3B;AAEA,UAAM,aAAa,MAAM;AACvB,qBAAe,UAAU;AACzB,oBAAc,UAAU;AAAA,IAC1B;AAEA,QAAI,GAAG,UAAU,YAAY;AAC7B,QAAI,GAAG,QAAQ,UAAU;AACzB,WAAO,MAAM;AACX,UAAI,IAAI,UAAU,YAAY;AAC9B,UAAI,IAAI,QAAQ,UAAU;AAAA,IAC5B;AAAA,EACF,GAAG,CAAC,MAAM,CAAC;AACb;",
6
+ "names": []
7
+ }
@@ -1,8 +1,10 @@
1
+ import { useContext } from "react";
1
2
  import {
2
3
  _useOverlayTooltipReducer as useTooltipReducer,
3
4
  _useOverlayTooltipStore as useOverlayTooltipStore,
4
5
  _useOverlayChart as useOverlayChart
5
6
  } from "@dynatrace/strato-components/charts";
7
+ import { GeoDataLookupContext } from "../contexts/geo-data-lookup.context.js";
6
8
  import { useSetStateOverlay, useSetState } from "../store/store.js";
7
9
  import { buildGeoTooltipState } from "../utils/build-geo-tooltip-state.js";
8
10
  import { extractDataFromEvent } from "../utils/parse-tooltip-data.js";
@@ -19,6 +21,7 @@ const layerIdToGeometry = (layerId) => {
19
21
  return "geoDot";
20
22
  };
21
23
  const useOverlayEvents = () => {
24
+ const dataLookupRegistry = useContext(GeoDataLookupContext);
22
25
  const setOverlayState = useSetStateOverlay();
23
26
  const setState = useSetState();
24
27
  const dispatch = useTooltipReducer();
@@ -53,7 +56,10 @@ const useOverlayEvents = () => {
53
56
  if (currentState.pinned) {
54
57
  return;
55
58
  }
56
- const { data, hoveredLayerId } = extractDataFromEvent(event);
59
+ const { data, hoveredLayerId } = extractDataFromEvent(
60
+ event,
61
+ dataLookupRegistry
62
+ );
57
63
  overlay.clear();
58
64
  setTooltipMarker(hoveredLayerId, event.lngLat);
59
65
  const pos = getAbsolutePosition(event);
@@ -83,7 +89,10 @@ const useOverlayEvents = () => {
83
89
  }
84
90
  };
85
91
  const handleMouseClick = (event) => {
86
- const { data, featureId, hoveredLayerId } = extractDataFromEvent(event);
92
+ const { data, featureId, hoveredLayerId } = extractDataFromEvent(
93
+ event,
94
+ dataLookupRegistry
95
+ );
87
96
  if (!featureId) {
88
97
  hideTooltip();
89
98
  return;