@defra/interactive-map 0.0.3-alpha → 0.0.5-alpha

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 (407) hide show
  1. package/README.md +16 -57
  2. package/dist/css/index.css +1 -1
  3. package/dist/esm/im-core.js +1 -1
  4. package/dist/esm/index.js +1 -1
  5. package/dist/umd/im-core.js +1 -1
  6. package/dist/umd/index.js +1 -1
  7. package/docs/api/button-definition.md +176 -0
  8. package/docs/api/context.md +124 -0
  9. package/docs/api/control-definition.md +72 -0
  10. package/docs/api/icon-definition.md +28 -0
  11. package/docs/api/map-style-config.md +76 -0
  12. package/docs/api/marker-config.md +51 -0
  13. package/docs/api/panel-definition.md +117 -0
  14. package/docs/api/slots.md +25 -0
  15. package/docs/api.md +721 -0
  16. package/docs/architecture.md +139 -0
  17. package/docs/building-a-plugin.md +149 -0
  18. package/docs/getting-started.md +46 -0
  19. package/docs/govuk-prototype.md +0 -0
  20. package/docs/plugins/interact.md +205 -0
  21. package/docs/plugins/map-styles.md +84 -0
  22. package/docs/plugins/plugin-context.md +67 -0
  23. package/docs/plugins/plugin-descriptor.md +71 -0
  24. package/docs/plugins/plugin-manifest.md +118 -0
  25. package/docs/plugins/scale-bar.md +62 -0
  26. package/docs/plugins/search.md +162 -0
  27. package/docs/plugins.md +116 -0
  28. package/govuk-prototype-kit.config.json +7 -7
  29. package/package.json +16 -2
  30. package/plugins/beta/datasets/dist/esm/im-datasets-plugin.js +2 -0
  31. package/plugins/beta/datasets/dist/esm/index.js +2 -0
  32. package/plugins/beta/datasets/dist/umd/im-datasets-plugin.js +2 -0
  33. package/plugins/beta/datasets/dist/umd/index.js +2 -0
  34. package/plugins/beta/datasets/src/datasets.js +113 -0
  35. package/plugins/beta/datasets/src/fetch/createDynamicSource.js +206 -0
  36. package/plugins/beta/datasets/src/fetch/fetchGeoJSON.js +38 -0
  37. package/plugins/{datasets → beta/datasets}/src/handleSetMapStyle.js +8 -1
  38. package/plugins/{datasets → beta/datasets}/src/manifest.js +1 -1
  39. package/plugins/{datasets → beta/datasets}/src/mapLayers.js +42 -4
  40. package/plugins/{datasets → beta/datasets}/src/panels/Key.jsx +1 -1
  41. package/plugins/beta/datasets/src/utils/bbox.js +115 -0
  42. package/plugins/beta/draw-ml/dist/css/index.css +1 -0
  43. package/plugins/beta/draw-ml/dist/esm/im-draw-ml-plugin.js +1 -0
  44. package/plugins/beta/draw-ml/dist/esm/index.js +2 -0
  45. package/plugins/beta/draw-ml/dist/umd/im-draw-ml-plugin.js +1 -0
  46. package/plugins/beta/draw-ml/dist/umd/index.js +2 -0
  47. package/plugins/beta/draw-ml/src/api/addFeature.js +27 -0
  48. package/plugins/{draw-ml → beta/draw-ml}/src/api/editFeature.js +17 -6
  49. package/plugins/beta/draw-ml/src/api/merge.js +9 -0
  50. package/plugins/beta/draw-ml/src/api/newLine.js +68 -0
  51. package/plugins/beta/draw-ml/src/api/newPolygon.js +68 -0
  52. package/plugins/beta/draw-ml/src/api/split.js +81 -0
  53. package/plugins/{draw-ml → beta/draw-ml}/src/defaults.js +3 -0
  54. package/plugins/beta/draw-ml/src/draw.scss +32 -0
  55. package/plugins/beta/draw-ml/src/events.js +171 -0
  56. package/plugins/beta/draw-ml/src/manifest.js +132 -0
  57. package/plugins/{draw-ml → beta/draw-ml}/src/mapboxDraw.js +51 -2
  58. package/plugins/{draw-ml → beta/draw-ml}/src/mapboxSnap.js +18 -12
  59. package/plugins/beta/draw-ml/src/modes/createDrawMode.js +497 -0
  60. package/plugins/beta/draw-ml/src/modes/drawLineMode.js +18 -0
  61. package/plugins/beta/draw-ml/src/modes/drawPolygonMode.js +16 -0
  62. package/plugins/beta/draw-ml/src/modes/editVertex/geometryHelpers.js +135 -0
  63. package/plugins/beta/draw-ml/src/modes/editVertex/helpers.js +2 -0
  64. package/plugins/beta/draw-ml/src/modes/editVertex/touchHandlers.js +134 -0
  65. package/plugins/beta/draw-ml/src/modes/editVertex/undoHandlers.js +133 -0
  66. package/plugins/beta/draw-ml/src/modes/editVertex/vertexOperations.js +141 -0
  67. package/plugins/beta/draw-ml/src/modes/editVertex/vertexQueries.js +121 -0
  68. package/plugins/beta/draw-ml/src/modes/editVertexMode.js +437 -0
  69. package/plugins/{draw-ml → beta/draw-ml}/src/reducer.js +30 -2
  70. package/plugins/{draw-ml → beta/draw-ml}/src/styles.js +66 -36
  71. package/plugins/beta/draw-ml/src/undoStack.js +50 -0
  72. package/plugins/beta/draw-ml/src/utils/debounce.js +16 -0
  73. package/plugins/beta/draw-ml/src/utils/flattenStyleProperties.js +49 -0
  74. package/plugins/beta/draw-ml/src/utils/spatial.js +258 -0
  75. package/plugins/beta/frame/dist/esm/im-frame-plugin.js +1 -0
  76. package/plugins/beta/frame/dist/esm/index.js +2 -0
  77. package/plugins/beta/frame/dist/umd/im-frame-plugin.js +1 -0
  78. package/plugins/beta/frame/dist/umd/index.js +2 -0
  79. package/plugins/{frame → beta/frame}/src/Frame.jsx +1 -1
  80. package/plugins/{map-styles → beta/map-styles}/dist/css/index.css +1 -1
  81. package/plugins/beta/map-styles/dist/esm/im-map-styles-plugin.js +1 -0
  82. package/plugins/beta/map-styles/dist/esm/index.js +2 -0
  83. package/plugins/beta/map-styles/dist/umd/im-map-styles-plugin.js +1 -0
  84. package/plugins/beta/map-styles/dist/umd/index.js +2 -0
  85. package/plugins/{map-styles → beta/map-styles}/src/MapStyles.jsx +1 -1
  86. package/plugins/{map-styles → beta/map-styles}/src/manifest.js +8 -10
  87. package/plugins/{map-styles → beta/map-styles}/src/mapStyles.scss +4 -1
  88. package/plugins/beta/scale-bar/dist/esm/im-scale-bar-plugin.js +1 -0
  89. package/plugins/beta/scale-bar/dist/esm/index.js +2 -0
  90. package/plugins/beta/scale-bar/dist/umd/im-scale-bar-plugin.js +1 -0
  91. package/plugins/beta/scale-bar/dist/umd/index.js +2 -0
  92. package/plugins/beta/use-location/dist/esm/im-use-location-plugin.js +1 -0
  93. package/plugins/beta/use-location/dist/esm/index.js +2 -0
  94. package/plugins/beta/use-location/dist/umd/im-use-location-plugin.js +1 -0
  95. package/plugins/beta/use-location/dist/umd/index.js +2 -0
  96. package/plugins/interact/dist/css/index.css +1 -1
  97. package/plugins/interact/dist/esm/im-interact-plugin.js +1 -1
  98. package/plugins/interact/dist/esm/index.js +1 -1
  99. package/plugins/interact/dist/umd/im-interact-plugin.js +1 -1
  100. package/plugins/interact/dist/umd/index.js +1 -1
  101. package/plugins/interact/src/InteractInit.jsx +1 -1
  102. package/plugins/interact/src/InteractInit.test.js +97 -0
  103. package/plugins/interact/src/api/clear.test.js +18 -0
  104. package/plugins/interact/src/api/disable.test.js +12 -0
  105. package/plugins/interact/src/api/enable.test.js +48 -0
  106. package/plugins/interact/src/api/selectFeature.test.js +33 -0
  107. package/plugins/interact/src/api/unselectFeature.test.js +33 -0
  108. package/plugins/interact/src/defaults.js +2 -0
  109. package/plugins/interact/src/events.js +37 -52
  110. package/plugins/interact/src/events.test.js +186 -0
  111. package/plugins/interact/src/hooks/useHighlightSync.js +1 -1
  112. package/plugins/interact/src/hooks/useHighlightSync.test.js +212 -0
  113. package/plugins/interact/src/hooks/useInteractionHandlers.js +80 -41
  114. package/plugins/interact/src/hooks/useInteractionHandlers.test.js +316 -0
  115. package/plugins/interact/src/index.test.js +22 -0
  116. package/plugins/interact/src/interact.scss +6 -0
  117. package/plugins/interact/src/manifest.js +1 -1
  118. package/plugins/interact/src/manifest.test.js +94 -0
  119. package/plugins/interact/src/reducer.js +28 -21
  120. package/plugins/interact/src/reducer.test.js +155 -0
  121. package/plugins/interact/src/utils/buildStylesMap.test.js +57 -0
  122. package/plugins/interact/src/utils/featureQueries.js +2 -2
  123. package/plugins/interact/src/utils/featureQueries.test.js +56 -0
  124. package/plugins/interact/src/utils/spatial.js +95 -0
  125. package/plugins/interact/src/utils/spatial.test.js +93 -0
  126. package/plugins/search/dist/css/index.css +1 -1
  127. package/plugins/search/dist/esm/im-search-plugin.js +1 -1
  128. package/plugins/search/dist/esm/index.js +1 -1
  129. package/plugins/search/dist/umd/im-search-plugin.js +1 -1
  130. package/plugins/search/dist/umd/index.js +1 -1
  131. package/plugins/search/src/Search.jsx +2 -2
  132. package/plugins/search/src/components/Form/Form.jsx +1 -1
  133. package/plugins/search/src/components/Suggestions/Suggestions.jsx +5 -5
  134. package/plugins/search/src/datasets.js +1 -1
  135. package/plugins/search/src/defaults.js +3 -0
  136. package/plugins/search/src/events/fetchSuggestions.js +48 -53
  137. package/plugins/search/src/events/formHandlers.js +3 -2
  138. package/plugins/search/src/events/index.js +1 -1
  139. package/plugins/search/src/events/inputHandlers.js +3 -1
  140. package/plugins/search/src/events/suggestionHandlers.js +12 -3
  141. package/plugins/search/src/search.scss +4 -1
  142. package/plugins/search/src/utils/parseOsNamesResults.js +15 -19
  143. package/providers/{esri → beta/esri}/src/esriProvider.js +1 -1
  144. package/providers/{esri → beta/esri}/src/index.js +1 -1
  145. package/providers/{esri → beta/esri}/src/mapEvents.js +2 -2
  146. package/providers/beta/open-names/dist/esm/im-reverse-geocode.js +2 -0
  147. package/providers/beta/open-names/dist/esm/index.js +2 -0
  148. package/providers/beta/open-names/dist/umd/im-reverse-geocode.js +2 -0
  149. package/providers/beta/open-names/dist/umd/im-reverse-geocode.js.LICENSE.txt +1 -0
  150. package/providers/beta/open-names/dist/umd/index.js +2 -0
  151. package/providers/beta/open-names/dist/umd/index.js.LICENSE.txt +1 -0
  152. package/providers/maplibre/dist/esm/im-maplibre-framework.js +1 -1
  153. package/providers/maplibre/dist/esm/im-maplibre-framework.js.LICENSE.txt +1 -1
  154. package/providers/maplibre/dist/esm/im-maplibre-provider.js +1 -1
  155. package/providers/maplibre/dist/esm/index.js +1 -1
  156. package/providers/maplibre/dist/umd/im-maplibre-framework.js +1 -1
  157. package/providers/maplibre/dist/umd/im-maplibre-framework.js.LICENSE.txt +1 -1
  158. package/providers/maplibre/dist/umd/im-maplibre-provider.js +1 -1
  159. package/providers/maplibre/dist/umd/index.js +1 -1
  160. package/providers/maplibre/src/defaults.js +3 -2
  161. package/providers/maplibre/src/index.js +36 -20
  162. package/providers/maplibre/src/maplibreProvider.js +140 -11
  163. package/providers/maplibre/src/utils/detectWebgl.js +1 -2
  164. package/providers/maplibre/src/utils/highlightFeatures.js +78 -67
  165. package/providers/maplibre/src/utils/labels.js +174 -120
  166. package/providers/maplibre/src/utils/maplibreFixes.js +2 -2
  167. package/providers/maplibre/src/utils/queryFeatures.js +146 -0
  168. package/providers/maplibre/src/utils/spatial.js +14 -17
  169. package/sonar-project.properties +40 -3
  170. package/src/App/components/Actions/Actions.jsx +2 -2
  171. package/src/App/components/Actions/Actions.module.scss +2 -2
  172. package/src/App/components/KeyboardHelp/KeyboardHelp.jsx +3 -1
  173. package/src/App/components/KeyboardHelp/KeyboardHelp.test.jsx +9 -0
  174. package/src/App/components/Logo/Logo.jsx +1 -1
  175. package/src/App/components/MapButton/MapButton.jsx +217 -41
  176. package/src/App/components/MapButton/MapButton.module.scss +32 -21
  177. package/src/App/components/MapButton/MapButton.test.jsx +128 -84
  178. package/src/App/components/Markers/Markers.jsx +1 -1
  179. package/src/App/components/Markers/Markers.module.scss +0 -5
  180. package/src/App/components/Panel/Panel.jsx +75 -45
  181. package/src/App/components/Panel/Panel.module.scss +18 -24
  182. package/src/App/components/Panel/Panel.test.jsx +18 -0
  183. package/src/App/components/PopupMenu/PopupMenu.jsx +246 -0
  184. package/src/App/components/PopupMenu/PopupMenu.module.scss +70 -0
  185. package/src/App/components/PopupMenu/PopupMenu.test.jsx +304 -0
  186. package/src/App/components/Tooltip/Tooltip.jsx +4 -2
  187. package/src/App/components/Tooltip/getTooltipPosition.js +1 -1
  188. package/src/App/components/Viewport/MapController.jsx +1 -1
  189. package/src/App/components/Viewport/MapStatus.jsx +1 -1
  190. package/src/App/components/Viewport/Viewport.jsx +2 -2
  191. package/src/App/components/Viewport/Viewport.module.scss +2 -2
  192. package/src/App/controls/keyboardActions.js +3 -2
  193. package/src/App/controls/keyboardShortcuts.js +4 -2
  194. package/src/App/hooks/useButtonStateEvaluator.js +58 -45
  195. package/src/App/hooks/useButtonStateEvaluator.test.js +36 -3
  196. package/src/App/hooks/useCrossHairAPI.js +49 -47
  197. package/src/App/hooks/useFocusVisible.js +2 -2
  198. package/src/App/hooks/useInterfaceAPI.js +36 -1
  199. package/src/App/hooks/useKeyboardHint.js +1 -1
  200. package/src/App/hooks/useKeyboardShortcuts.js +4 -2
  201. package/src/App/hooks/useMapAnnouncements.js +34 -32
  202. package/src/App/hooks/useMapEvents.js +12 -1
  203. package/src/App/hooks/useMapProviderOverrides.js +3 -3
  204. package/src/App/hooks/useMapStateSync.js +7 -1
  205. package/src/App/hooks/useMapURLSync.js +6 -1
  206. package/src/App/hooks/useMarkersAPI.js +88 -50
  207. package/src/App/hooks/useMediaQueryDispatch.test.js +48 -0
  208. package/src/App/hooks/useModalPanelBehaviour.js +46 -40
  209. package/src/App/hooks/useResizeObserver.js +2 -2
  210. package/src/App/initialiseApp.js +45 -38
  211. package/src/App/layout/Layout.jsx +6 -4
  212. package/src/App/layout/layout.module.scss +13 -17
  213. package/src/App/registry/keyboardShortcutRegistry.js +12 -2
  214. package/src/App/registry/keyboardShortcutRegistry.test.js +28 -0
  215. package/src/App/registry/mergeManifests.js +1 -1
  216. package/src/App/registry/panelRegistry.js +1 -1
  217. package/src/App/registry/panelRegistry.test.js +30 -1
  218. package/src/App/registry/pluginRegistry.js +14 -51
  219. package/src/App/registry/pluginRegistry.test.js +54 -6
  220. package/src/App/renderer/HtmlElementHost.jsx +186 -0
  221. package/src/App/renderer/HtmlElementHost.test.jsx +231 -0
  222. package/src/App/renderer/mapButtons.js +12 -9
  223. package/src/App/renderer/mapButtons.test.js +6 -4
  224. package/src/App/renderer/mapControls.js +6 -0
  225. package/src/App/renderer/mapControls.test.js +10 -2
  226. package/src/App/renderer/mapPanels.js +39 -26
  227. package/src/App/renderer/mapPanels.test.js +29 -0
  228. package/src/App/renderer/pluginWrapper.test.js +28 -0
  229. package/src/App/renderer/slotHelpers.js +60 -0
  230. package/src/App/renderer/slotHelpers.test.js +82 -0
  231. package/src/App/store/AppProvider.jsx +3 -0
  232. package/src/App/store/AppProvider.test.jsx +40 -0
  233. package/src/App/store/PluginProvider.jsx +7 -5
  234. package/src/App/store/appActionsMap.js +46 -1
  235. package/src/App/store/appActionsMap.test.js +21 -0
  236. package/src/App/store/appReducer.js +1 -0
  237. package/src/App/store/mapActionsMap.js +12 -24
  238. package/src/App/store/mapReducer.js +1 -1
  239. package/src/InteractiveMap/InteractiveMap.js +194 -13
  240. package/src/InteractiveMap/InteractiveMap.test.js +164 -2
  241. package/src/InteractiveMap/behaviourController.js +12 -2
  242. package/src/InteractiveMap/behaviourController.test.js +82 -1
  243. package/src/InteractiveMap/deviceChecker.js +1 -1
  244. package/src/InteractiveMap/deviceChecker.test.js +2 -2
  245. package/src/InteractiveMap/historyManager.js +41 -5
  246. package/src/InteractiveMap/historyManager.test.js +37 -6
  247. package/src/InteractiveMap/polyfills.js +39 -0
  248. package/src/InteractiveMap/polyfills.test.js +127 -0
  249. package/src/config/appConfig.js +5 -5
  250. package/src/config/appConfig.test.js +107 -42
  251. package/src/config/defaults.js +30 -20
  252. package/src/config/events.js +110 -4
  253. package/src/index.js +5 -2
  254. package/src/index.umd.js +1 -1
  255. package/src/scss/main.scss +1 -0
  256. package/src/scss/settings/_dimensions.scss +6 -2
  257. package/src/scss/tools/_border-focus.scss +20 -4
  258. package/src/services/eventBus.test.js +24 -1
  259. package/src/test-utils.js +1 -0
  260. package/src/types.js +590 -0
  261. package/src/utils/detectBreakpoint.js +78 -77
  262. package/src/utils/detectBreakpoint.test.js +50 -4
  263. package/src/utils/detectInterfaceType.js +4 -4
  264. package/src/utils/getIsFullscreen.js +3 -1
  265. package/src/utils/getSafeZoneInset.js +23 -7
  266. package/src/utils/getSafeZoneInset.test.js +18 -0
  267. package/src/utils/mapStateSync.js +3 -3
  268. package/src/utils/queryString.js +1 -1
  269. package/src/utils/stringToKebab.js +1 -1
  270. package/src/utils/toggleInertElements.js +37 -12
  271. package/webpack.dev.mjs +3 -4
  272. package/webpack.esm.mjs +8 -8
  273. package/webpack.umd.mjs +8 -8
  274. package/plugins/datasets/dist/esm/im-datasets-plugin.js +0 -1
  275. package/plugins/datasets/dist/esm/index.js +0 -2
  276. package/plugins/datasets/dist/umd/im-datasets-plugin.js +0 -1
  277. package/plugins/datasets/dist/umd/index.js +0 -2
  278. package/plugins/datasets/src/datasets.js +0 -60
  279. package/plugins/datasets/src/utils/bbox.js +0 -4
  280. package/plugins/draw-ml/dist/css/index.css +0 -1
  281. package/plugins/draw-ml/dist/esm/im-draw-ml-plugin.js +0 -1
  282. package/plugins/draw-ml/dist/esm/index.js +0 -2
  283. package/plugins/draw-ml/dist/umd/im-draw-ml-plugin.js +0 -1
  284. package/plugins/draw-ml/dist/umd/index.js +0 -2
  285. package/plugins/draw-ml/src/api/addFeature.js +0 -15
  286. package/plugins/draw-ml/src/api/newPolygon.js +0 -43
  287. package/plugins/draw-ml/src/draw.scss +0 -9
  288. package/plugins/draw-ml/src/events.js +0 -127
  289. package/plugins/draw-ml/src/manifest.js +0 -100
  290. package/plugins/draw-ml/src/modes/drawVertexMode.js +0 -275
  291. package/plugins/draw-ml/src/modes/editVertexMode.js +0 -426
  292. package/plugins/draw-ml/src/utils.js +0 -102
  293. package/plugins/frame/dist/esm/im-frame-plugin.js +0 -1
  294. package/plugins/frame/dist/esm/index.js +0 -2
  295. package/plugins/frame/dist/umd/im-frame-plugin.js +0 -1
  296. package/plugins/frame/dist/umd/index.js +0 -2
  297. package/plugins/map-styles/dist/esm/im-map-styles-plugin.js +0 -1
  298. package/plugins/map-styles/dist/esm/index.js +0 -2
  299. package/plugins/map-styles/dist/umd/im-map-styles-plugin.js +0 -1
  300. package/plugins/map-styles/dist/umd/index.js +0 -2
  301. package/plugins/scale-bar/dist/esm/im-scale-bar-plugin.js +0 -1
  302. package/plugins/scale-bar/dist/esm/index.js +0 -2
  303. package/plugins/scale-bar/dist/umd/im-scale-bar-plugin.js +0 -1
  304. package/plugins/scale-bar/dist/umd/index.js +0 -2
  305. package/plugins/search/src/utils/fetchDataset.js +0 -23
  306. package/plugins/use-location/dist/esm/im-use-location-plugin.js +0 -1
  307. package/plugins/use-location/dist/esm/index.js +0 -2
  308. package/plugins/use-location/dist/umd/im-use-location-plugin.js +0 -1
  309. package/plugins/use-location/dist/umd/index.js +0 -2
  310. package/providers/maplibre/dist/esm/im-maplibre-legacy-framework.js +0 -1
  311. package/providers/maplibre/dist/umd/im-maplibre-legacy-framework.js +0 -1
  312. package/providers/open-names/dist/esm/im-reverse-geocode.js +0 -2
  313. package/providers/open-names/dist/esm/index.js +0 -2
  314. package/providers/open-names/dist/umd/im-reverse-geocode.js +0 -2
  315. package/providers/open-names/dist/umd/index.js +0 -2
  316. /package/docs/architecture/{ARCHITECTURE_DIAGRAMS.md → architecture-diagrams.md} +0 -0
  317. /package/plugins/{datasets → beta/datasets}/dist/css/index.css +0 -0
  318. /package/plugins/{datasets/dist/esm/index.js.LICENSE.txt → beta/datasets/dist/esm/im-datasets-plugin.js.LICENSE.txt} +0 -0
  319. /package/plugins/{datasets/dist/umd → beta/datasets/dist/esm}/index.js.LICENSE.txt +0 -0
  320. /package/plugins/{draw-ml/dist/esm/index.js.LICENSE.txt → beta/datasets/dist/umd/im-datasets-plugin.js.LICENSE.txt} +0 -0
  321. /package/plugins/{draw-ml → beta/datasets}/dist/umd/index.js.LICENSE.txt +0 -0
  322. /package/plugins/{datasets → beta/datasets}/src/DatasetsInit.jsx +0 -0
  323. /package/plugins/{datasets → beta/datasets}/src/api/addDataset.js +0 -0
  324. /package/plugins/{datasets → beta/datasets}/src/api/hideDataset.js +0 -0
  325. /package/plugins/{datasets → beta/datasets}/src/api/hideFeatures.js +0 -0
  326. /package/plugins/{datasets → beta/datasets}/src/api/removeDataset.js +0 -0
  327. /package/plugins/{datasets → beta/datasets}/src/api/showDataset.js +0 -0
  328. /package/plugins/{datasets → beta/datasets}/src/api/showFeatures.js +0 -0
  329. /package/plugins/{datasets → beta/datasets}/src/datasets.scss +0 -0
  330. /package/plugins/{datasets → beta/datasets}/src/defaults.js +0 -0
  331. /package/plugins/{datasets → beta/datasets}/src/index.js +0 -0
  332. /package/plugins/{datasets → beta/datasets}/src/panels/Key.module.scss +0 -0
  333. /package/plugins/{datasets → beta/datasets}/src/panels/Layers.jsx +0 -0
  334. /package/plugins/{datasets → beta/datasets}/src/panels/Layers.module.scss +0 -0
  335. /package/plugins/{datasets → beta/datasets}/src/reducer.js +0 -0
  336. /package/plugins/{datasets → beta/datasets}/src/utils/debounce.js +0 -0
  337. /package/plugins/{datasets → beta/datasets}/src/utils/filters.js +0 -0
  338. /package/plugins/{draw-es → beta/draw-es}/src/DrawInit.jsx +0 -0
  339. /package/plugins/{draw-es → beta/draw-es}/src/api/addFeature.js +0 -0
  340. /package/plugins/{draw-es → beta/draw-es}/src/api/deleteFeature.js +0 -0
  341. /package/plugins/{draw-es → beta/draw-es}/src/api/editFeature.js +0 -0
  342. /package/plugins/{draw-es → beta/draw-es}/src/api/newPolygon.js +0 -0
  343. /package/plugins/{draw-es → beta/draw-es}/src/events.js +0 -0
  344. /package/plugins/{draw-es → beta/draw-es}/src/graphic.js +0 -0
  345. /package/plugins/{draw-es → beta/draw-es}/src/index.js +0 -0
  346. /package/plugins/{draw-es → beta/draw-es}/src/manifest.js +0 -0
  347. /package/plugins/{draw-es → beta/draw-es}/src/reducer.js +0 -0
  348. /package/plugins/{draw-es → beta/draw-es}/src/sketchViewModel.js +0 -0
  349. /package/plugins/{frame → beta/draw-ml}/dist/esm/index.js.LICENSE.txt +0 -0
  350. /package/plugins/{frame → beta/draw-ml}/dist/umd/index.js.LICENSE.txt +0 -0
  351. /package/plugins/{draw-ml → beta/draw-ml}/src/DrawInit.jsx +0 -0
  352. /package/plugins/{draw-ml → beta/draw-ml}/src/api/deleteFeature.js +0 -0
  353. /package/plugins/{draw-ml → beta/draw-ml}/src/index.js +0 -0
  354. /package/plugins/{draw-ml → beta/draw-ml}/src/modes/disabledMode.js +0 -0
  355. /package/plugins/{draw-ml/src → beta/draw-ml/src/utils}/snapHelpers.js +0 -0
  356. /package/plugins/{frame → beta/frame}/dist/css/index.css +0 -0
  357. /package/plugins/{map-styles → beta/frame}/dist/esm/index.js.LICENSE.txt +0 -0
  358. /package/plugins/{map-styles → beta/frame}/dist/umd/index.js.LICENSE.txt +0 -0
  359. /package/plugins/{frame → beta/frame}/package.json +0 -0
  360. /package/plugins/{frame → beta/frame}/src/FrameInit.jsx +0 -0
  361. /package/plugins/{frame → beta/frame}/src/api/addFrame.js +0 -0
  362. /package/plugins/{frame → beta/frame}/src/api/editFeature.js +0 -0
  363. /package/plugins/{frame → beta/frame}/src/config.js +0 -0
  364. /package/plugins/{frame → beta/frame}/src/frame.scss +0 -0
  365. /package/plugins/{frame → beta/frame}/src/index.js +0 -0
  366. /package/plugins/{frame → beta/frame}/src/manifest.js +0 -0
  367. /package/plugins/{frame → beta/frame}/src/reducer.js +0 -0
  368. /package/plugins/{frame → beta/frame}/src/utils.js +0 -0
  369. /package/plugins/{scale-bar → beta/map-styles}/dist/esm/index.js.LICENSE.txt +0 -0
  370. /package/plugins/{scale-bar → beta/map-styles}/dist/umd/index.js.LICENSE.txt +0 -0
  371. /package/plugins/{map-styles → beta/map-styles}/src/MapStylesInit.jsx +0 -0
  372. /package/plugins/{map-styles → beta/map-styles}/src/config.js +0 -0
  373. /package/plugins/{map-styles → beta/map-styles}/src/index.js +0 -0
  374. /package/plugins/{scale-bar → beta/scale-bar}/dist/css/index.css +0 -0
  375. /package/plugins/{use-location → beta/scale-bar}/dist/esm/index.js.LICENSE.txt +0 -0
  376. /package/plugins/{use-location → beta/scale-bar}/dist/umd/index.js.LICENSE.txt +0 -0
  377. /package/plugins/{scale-bar → beta/scale-bar}/package.json +0 -0
  378. /package/plugins/{scale-bar → beta/scale-bar}/src/ScaleBar.jsx +0 -0
  379. /package/plugins/{scale-bar → beta/scale-bar}/src/ScaleBar.test.jsx +0 -0
  380. /package/plugins/{scale-bar → beta/scale-bar}/src/index.js +0 -0
  381. /package/plugins/{scale-bar → beta/scale-bar}/src/index.test.js +0 -0
  382. /package/plugins/{scale-bar → beta/scale-bar}/src/manifest.js +0 -0
  383. /package/plugins/{scale-bar → beta/scale-bar}/src/scaleBar.scss +0 -0
  384. /package/plugins/{scale-bar → beta/scale-bar}/src/utils.js +0 -0
  385. /package/plugins/{scale-bar → beta/scale-bar}/src/utils.test.js +0 -0
  386. /package/{providers/open-names → plugins/beta/use-location}/dist/esm/index.js.LICENSE.txt +0 -0
  387. /package/{providers/open-names → plugins/beta/use-location}/dist/umd/index.js.LICENSE.txt +0 -0
  388. /package/plugins/{use-location → beta/use-location}/src/UseLocation.jsx +0 -0
  389. /package/plugins/{use-location → beta/use-location}/src/UseLocationInit.jsx +0 -0
  390. /package/plugins/{use-location → beta/use-location}/src/defaults.js +0 -0
  391. /package/plugins/{use-location → beta/use-location}/src/events.js +0 -0
  392. /package/plugins/{use-location → beta/use-location}/src/index.js +0 -0
  393. /package/plugins/{use-location → beta/use-location}/src/manifest.js +0 -0
  394. /package/plugins/{use-location → beta/use-location}/src/reducer.js +0 -0
  395. /package/providers/{esri → beta/esri}/src/appEvents.js +0 -0
  396. /package/providers/{esri → beta/esri}/src/defaults.js +0 -0
  397. /package/providers/{esri → beta/esri}/src/esriProvider.scss +0 -0
  398. /package/providers/{esri → beta/esri}/src/utils/coords.js +0 -0
  399. /package/providers/{esri → beta/esri}/src/utils/detectWebGL.js +0 -0
  400. /package/providers/{esri → beta/esri}/src/utils/esriFixes.js +0 -0
  401. /package/providers/{esri → beta/esri}/src/utils/query.js +0 -0
  402. /package/providers/{esri → beta/esri}/src/utils/spatial.js +0 -0
  403. /package/providers/{open-names → beta/open-names}/dist/esm/im-reverse-geocode.js.LICENSE.txt +0 -0
  404. /package/providers/{open-names/dist/umd/im-reverse-geocode.js.LICENSE.txt → beta/open-names/dist/esm/index.js.LICENSE.txt} +0 -0
  405. /package/providers/{open-names → beta/open-names}/src/index.js +0 -0
  406. /package/providers/{open-names → beta/open-names}/src/reverseGeocode.js +0 -0
  407. /package/providers/{open-names → beta/open-names}/src/utils/mapToLocationModel.js +0 -0
@@ -1,6 +1,35 @@
1
1
  import { useCallback, useEffect, useRef } from 'react'
2
+ import { isContiguousWithAny, canSplitFeatures, areAllContiguous } from '../utils/spatial.js'
2
3
  import { getFeaturesAtPoint, findMatchingFeature, buildLayerConfigMap } from '../utils/featureQueries.js'
3
4
 
5
+ const useSelectionChangeEmitter = (eventBus, selectedFeatures, selectionBounds) => {
6
+ const lastEmittedSelectionChange = useRef(null)
7
+
8
+ useEffect(() => {
9
+ // Skip if features exist but bounds not yet calculated
10
+ const awaitingBounds = selectedFeatures.length > 0 && !selectionBounds
11
+ if (awaitingBounds) {
12
+ return
13
+ }
14
+
15
+ // Skip if selection was already empty and remains empty
16
+ const prev = lastEmittedSelectionChange.current
17
+ const wasEmpty = prev === null || prev.length === 0
18
+ if (wasEmpty && selectedFeatures.length === 0) {
19
+ return
20
+ }
21
+
22
+ eventBus.emit('interact:selectionchange', {
23
+ selectedFeatures,
24
+ selectionBounds,
25
+ canMerge: areAllContiguous(selectedFeatures),
26
+ canSplit: canSplitFeatures(selectedFeatures)
27
+ })
28
+
29
+ lastEmittedSelectionChange.current = selectedFeatures
30
+ }, [selectedFeatures, selectionBounds])
31
+ }
32
+
4
33
  export const useInteractionHandlers = ({
5
34
  mapState,
6
35
  pluginState,
@@ -8,65 +37,75 @@ export const useInteractionHandlers = ({
8
37
  mapProvider,
9
38
  }) => {
10
39
  const { markers } = mapState
11
- const { dispatch, dataLayers, interactionMode, multiSelect, markerColor, selectedFeatures, selectionBounds } = pluginState
40
+ const { dispatch, dataLayers, interactionMode, multiSelect, contiguous, markerColor, tolerance, selectedFeatures, selectionBounds } = pluginState
12
41
  const { eventBus } = services
13
- const lastEmittedSelectionChange = useRef(null)
14
42
  const layerConfigMap = buildLayerConfigMap(dataLayers)
15
43
 
16
44
  const handleInteraction = useCallback(({ point, coords }) => {
17
- const allFeatures = getFeaturesAtPoint(mapProvider, point)
45
+ const allFeatures = getFeaturesAtPoint(mapProvider, point, { radius: tolerance })
18
46
  const hasDataLayers = dataLayers.length > 0
19
47
 
20
- const match = hasDataLayers &&
21
- (interactionMode === 'select' || interactionMode === 'auto')
22
- ? findMatchingFeature(allFeatures, layerConfigMap)
23
- : null
24
-
25
- if (match) {
26
- markers.remove('location')
48
+ if (pluginState?.debug) {
49
+ console.log(`--- Features at ${coords} ---`, allFeatures)
50
+ }
27
51
 
28
- const { feature, config } = match
29
- const featureId = feature.properties?.[config.idProperty]
30
-
31
- if (featureId) {
32
- dispatch({
33
- type: 'TOGGLE_SELECTED_FEATURES',
34
- payload: {
35
- featureId,
36
- multiSelect,
37
- layerId: config.layerId,
38
- idProperty: config.idProperty,
39
- properties: feature.properties,
40
- geometry: feature.geometry
41
- },
42
- })
43
- }
52
+ const canMatch = hasDataLayers && (interactionMode === 'select' || interactionMode === 'auto')
53
+ const match = canMatch ? findMatchingFeature(allFeatures, layerConfigMap) : null
44
54
 
55
+ // 1. Handle Feature Match
56
+ if (match) {
57
+ processFeatureMatch(match)
45
58
  return
46
59
  }
47
60
 
48
- // Marker mode
49
- if (interactionMode === 'marker' || (interactionMode === 'auto' && hasDataLayers)) {
61
+ // 2. Handle Marker Mode (Fallback)
62
+ const isMarkerMode = interactionMode === 'marker' || (interactionMode === 'auto' && hasDataLayers)
63
+ if (isMarkerMode) {
50
64
  dispatch({ type: 'CLEAR_SELECTED_FEATURES' })
51
65
  markers.add('location', coords, { color: markerColor })
52
-
53
66
  eventBus.emit('interact:markerchange', { coords })
54
67
  }
55
- }, [mapProvider, dataLayers, interactionMode, multiSelect, eventBus, dispatch, markers])
56
68
 
57
- // Emit event when selectedFeatures change
58
- useEffect(() => {
59
- if (!selectionBounds || selectedFeatures === lastEmittedSelectionChange.current) {
60
- return
61
- }
69
+ // Internal helper to keep complexity low
70
+ function processFeatureMatch({ feature, config }) {
71
+ markers.remove('location')
72
+ const isNewContiguous = contiguous && isContiguousWithAny(feature, selectedFeatures)
73
+ const featureId = feature.properties?.[config.idProperty] ?? feature.id
62
74
 
63
- eventBus.emit('interact:selectionchange', {
64
- selectedFeatures,
65
- selectionBounds
66
- })
75
+ if (!featureId) {
76
+ return
77
+ }
67
78
 
68
- lastEmittedSelectionChange.current = selectedFeatures
69
- }, [selectedFeatures, selectionBounds])
79
+ dispatch({
80
+ type: 'TOGGLE_SELECTED_FEATURES',
81
+ payload: {
82
+ featureId,
83
+ multiSelect,
84
+ layerId: config.layerId,
85
+ idProperty: config.idProperty,
86
+ properties: feature.properties,
87
+ geometry: feature.geometry,
88
+ replaceAll: contiguous && !isNewContiguous
89
+ }
90
+ })
91
+ }
92
+ }, [
93
+ mapProvider,
94
+ dataLayers,
95
+ interactionMode,
96
+ multiSelect,
97
+ eventBus,
98
+ dispatch,
99
+ markers,
100
+ contiguous,
101
+ selectedFeatures,
102
+ layerConfigMap,
103
+ pluginState?.debug,
104
+ tolerance,
105
+ markerColor
106
+ ])
107
+
108
+ useSelectionChangeEmitter(eventBus, selectedFeatures, selectionBounds)
70
109
 
71
110
  return {
72
111
  handleInteraction
@@ -0,0 +1,316 @@
1
+ import { renderHook, act } from '@testing-library/react'
2
+ import { useInteractionHandlers } from './useInteractionHandlers.js'
3
+ import * as featureQueries from '../utils/featureQueries.js'
4
+ import { isContiguousWithAny } from '../utils/spatial.js'
5
+
6
+ /* ------------------------------------------------------------------ */
7
+ /* Mocks */
8
+ /* ------------------------------------------------------------------ */
9
+
10
+ jest.mock('../utils/spatial.js', () => ({
11
+ isContiguousWithAny: jest.fn(),
12
+ canSplitFeatures: jest.fn(() => false),
13
+ areAllContiguous: jest.fn(() => false)
14
+ }))
15
+ jest.mock('../utils/featureQueries.js', () => ({
16
+ getFeaturesAtPoint: jest.fn(),
17
+ findMatchingFeature: jest.fn(),
18
+ buildLayerConfigMap: jest.fn(() => ({}))
19
+ }))
20
+
21
+ /* ------------------------------------------------------------------ */
22
+ /* Helpers */
23
+ /* ------------------------------------------------------------------ */
24
+
25
+ const click = result =>
26
+ act(() =>
27
+ result.current.handleInteraction({
28
+ point: { x: 10, y: 20 },
29
+ coords: [1, 2]
30
+ })
31
+ )
32
+
33
+ const setup = (pluginOverrides = {}) => {
34
+ const deps = {
35
+ mapState: {
36
+ markers: { add: jest.fn(), remove: jest.fn() }
37
+ },
38
+ pluginState: {
39
+ dispatch: jest.fn(),
40
+ dataLayers: [{ layerId: 'parcels', idProperty: 'parcelId' }],
41
+ interactionMode: 'select',
42
+ multiSelect: false,
43
+ contiguous: false,
44
+ markerColor: 'red',
45
+ selectedFeatures: [],
46
+ selectionBounds: null,
47
+ ...pluginOverrides
48
+ },
49
+ services: {
50
+ eventBus: { emit: jest.fn() }
51
+ },
52
+ mapProvider: {}
53
+ }
54
+
55
+ const utils = renderHook(() => useInteractionHandlers(deps))
56
+ return { ...utils, deps }
57
+ }
58
+
59
+ const baseFeature = {
60
+ properties: { parcelId: 'P1' },
61
+ geometry: { type: 'Polygon' },
62
+ layer: { id: 'parcels' }
63
+ }
64
+
65
+ beforeEach(() => {
66
+ jest.clearAllMocks()
67
+
68
+ featureQueries.getFeaturesAtPoint.mockReturnValue([baseFeature])
69
+ featureQueries.findMatchingFeature.mockReturnValue({
70
+ feature: baseFeature,
71
+ config: { layerId: 'parcels', idProperty: 'parcelId' }
72
+ })
73
+ })
74
+
75
+ /* ------------------------------------------------------------------ */
76
+ /* Marker mode */
77
+ /* ------------------------------------------------------------------ */
78
+
79
+ describe('marker mode', () => {
80
+ it('places marker, clears selection, and emits event', () => {
81
+ const { result, deps } = setup({ interactionMode: 'marker' })
82
+
83
+ click(result)
84
+
85
+ expect(deps.pluginState.dispatch).toHaveBeenCalledWith({
86
+ type: 'CLEAR_SELECTED_FEATURES'
87
+ })
88
+ expect(deps.mapState.markers.add).toHaveBeenCalledWith(
89
+ 'location',
90
+ [1, 2],
91
+ { color: 'red' }
92
+ )
93
+ expect(deps.services.eventBus.emit).toHaveBeenCalledWith(
94
+ 'interact:markerchange',
95
+ { coords: [1, 2] }
96
+ )
97
+ })
98
+ })
99
+
100
+ /* ------------------------------------------------------------------ */
101
+ /* Select & Auto modes */
102
+ /* ------------------------------------------------------------------ */
103
+
104
+ describe.each(['select', 'auto'])('%s mode feature selection', mode => {
105
+ it('dispatches selection for matching feature', () => {
106
+ const { result, deps } = setup({ interactionMode: mode })
107
+
108
+ click(result)
109
+
110
+ expect(deps.pluginState.dispatch).toHaveBeenCalledWith(
111
+ expect.objectContaining({
112
+ type: 'TOGGLE_SELECTED_FEATURES',
113
+ payload: expect.objectContaining({
114
+ featureId: 'P1',
115
+ layerId: 'parcels'
116
+ })
117
+ })
118
+ )
119
+ })
120
+ })
121
+
122
+ it('falls back to marker in auto mode when no feature found', () => {
123
+ featureQueries.getFeaturesAtPoint.mockReturnValue([])
124
+ featureQueries.findMatchingFeature.mockReturnValue(null)
125
+
126
+ const { result, deps } = setup({ interactionMode: 'auto' })
127
+
128
+ click(result)
129
+
130
+ expect(deps.mapState.markers.add).toHaveBeenCalled()
131
+ })
132
+
133
+ /* ------------------------------------------------------------------ */
134
+ /* featureId guard (FULL COVERAGE) */
135
+ /* ------------------------------------------------------------------ */
136
+
137
+ it('skips dispatch when idProperty exists but featureId is falsy', () => {
138
+ featureQueries.findMatchingFeature.mockReturnValue({
139
+ feature: {
140
+ properties: { parcelId: undefined },
141
+ geometry: { type: 'Point' },
142
+ layer: { id: 'parcels' }
143
+ },
144
+ config: { layerId: 'parcels', idProperty: 'parcelId' }
145
+ })
146
+
147
+ const { result, deps } = setup()
148
+
149
+ click(result)
150
+
151
+ expect(deps.pluginState.dispatch).not.toHaveBeenCalled()
152
+ })
153
+
154
+ /* ------------------------------------------------------------------ */
155
+ /* Multi-select */
156
+ /* ------------------------------------------------------------------ */
157
+
158
+ it('passes multiSelect flag through to dispatch', () => {
159
+ const { result, deps } = setup({ multiSelect: true })
160
+
161
+ click(result)
162
+
163
+ expect(deps.pluginState.dispatch).toHaveBeenCalledWith(
164
+ expect.objectContaining({
165
+ payload: expect.objectContaining({ multiSelect: true })
166
+ })
167
+ )
168
+ })
169
+
170
+ /* ------------------------------------------------------------------ */
171
+ /* Contiguous selection (FULL COVERAGE) */
172
+ /* ------------------------------------------------------------------ */
173
+
174
+ describe('contiguous selection', () => {
175
+ it('does NOT replace selection when feature is contiguous', () => {
176
+ isContiguousWithAny.mockReturnValue(true) // contiguous
177
+
178
+ const { result, deps } = setup({
179
+ contiguous: true,
180
+ selectedFeatures: [{ geometry: { type: 'Polygon' } }]
181
+ })
182
+
183
+ click(result)
184
+
185
+ expect(deps.pluginState.dispatch).toHaveBeenCalledWith(
186
+ expect.objectContaining({
187
+ payload: expect.objectContaining({
188
+ replaceAll: false
189
+ })
190
+ })
191
+ )
192
+ })
193
+
194
+ it('replaces selection when feature is NOT contiguous', () => {
195
+ isContiguousWithAny.mockReturnValue(false) // disjoint
196
+
197
+ const { result, deps } = setup({
198
+ contiguous: true,
199
+ selectedFeatures: [{ geometry: { type: 'Polygon' } }]
200
+ })
201
+
202
+ click(result)
203
+
204
+ expect(deps.pluginState.dispatch).toHaveBeenCalledWith(
205
+ expect.objectContaining({
206
+ payload: expect.objectContaining({
207
+ replaceAll: true
208
+ })
209
+ })
210
+ )
211
+ })
212
+
213
+ it('does not compute contiguity when contiguous is false', () => {
214
+ const { result } = setup({ contiguous: false })
215
+
216
+ click(result)
217
+
218
+ expect(isContiguousWithAny).not.toHaveBeenCalled()
219
+ })
220
+ })
221
+
222
+ /* ------------------------------------------------------------------ */
223
+ /* Marker condition guard (FULL COVERAGE) */
224
+ /* ------------------------------------------------------------------ */
225
+
226
+ it('does NOT place marker in auto mode when no dataLayers exist', () => {
227
+ featureQueries.getFeaturesAtPoint.mockReturnValue([])
228
+ featureQueries.findMatchingFeature.mockReturnValue(null)
229
+
230
+ const { result, deps } = setup({
231
+ interactionMode: 'auto',
232
+ dataLayers: []
233
+ })
234
+
235
+ click(result)
236
+
237
+ expect(deps.mapState.markers.add).not.toHaveBeenCalled()
238
+ })
239
+
240
+ /* ------------------------------------------------------------------ */
241
+ /* Selection change event */
242
+ /* ------------------------------------------------------------------ */
243
+
244
+ it('emits selectionchange once when bounds exist', () => {
245
+ const deps = {
246
+ mapState: { markers: { add: jest.fn(), remove: jest.fn() } },
247
+ pluginState: {
248
+ selectedFeatures: [{ featureId: 'F1' }],
249
+ selectionBounds: { sw: [0, 0], ne: [1, 1] }
250
+ },
251
+ services: { eventBus: { emit: jest.fn() } },
252
+ mapProvider: {}
253
+ }
254
+
255
+ renderHook(() => useInteractionHandlers(deps))
256
+
257
+ expect(deps.services.eventBus.emit).toHaveBeenCalledWith(
258
+ 'interact:selectionchange',
259
+ expect.objectContaining({
260
+ selectedFeatures: deps.pluginState.selectedFeatures,
261
+ selectionBounds: deps.pluginState.selectionBounds,
262
+ canMerge: false,
263
+ canSplit: false
264
+ })
265
+ )
266
+ })
267
+
268
+ it('skips emission when selection remains empty after being cleared', () => {
269
+ const eventBus = { emit: jest.fn() }
270
+
271
+ // 1. First render with a feature (prev is null, emission happens)
272
+ const { rerender } = renderHook(
273
+ ({ features }) => useInteractionHandlers({
274
+ mapState: { markers: {} },
275
+ pluginState: { selectedFeatures: features, selectionBounds: { b: 1 } },
276
+ services: { eventBus },
277
+ mapProvider: {}
278
+ }),
279
+ { initialProps: { features: [{ id: 'f1' }] } }
280
+ )
281
+
282
+ expect(eventBus.emit).toHaveBeenCalledTimes(1)
283
+ eventBus.emit.mockClear()
284
+
285
+ // 2. Rerender with empty selection (prev is now [{id: 'f1'}], emission happens)
286
+ rerender({ features: [] })
287
+ expect(eventBus.emit).toHaveBeenCalledTimes(1)
288
+ eventBus.emit.mockClear()
289
+
290
+ // 3. Rerender with empty selection AGAIN
291
+ // This triggers: prev !== null AND prev.length === 0
292
+ rerender({ features: [] })
293
+
294
+ // Should skip emission because wasEmpty is true (via prev.length === 0)
295
+ // and current features.length is 0
296
+ expect(eventBus.emit).not.toHaveBeenCalled()
297
+ })
298
+
299
+ /* ------------------------------------------------------------------ */
300
+ /* Debug mode */
301
+ /* ------------------------------------------------------------------ */
302
+
303
+ it('logs features when debug mode is enabled', () => {
304
+ const logSpy = jest.spyOn(console, 'log').mockImplementation(() => {})
305
+
306
+ const { result } = setup({ debug: true })
307
+
308
+ click(result)
309
+
310
+ expect(logSpy).toHaveBeenCalledWith(
311
+ expect.stringContaining('--- Features at'),
312
+ expect.any(Array)
313
+ )
314
+
315
+ logSpy.mockRestore()
316
+ })
@@ -0,0 +1,22 @@
1
+ import createPlugin from './index.js'
2
+
3
+ jest.mock('./interact.scss', () => ({}))
4
+
5
+ describe('createPlugin', () => {
6
+ it('returns plugin with fixed id and load function', () => {
7
+ const plugin = createPlugin({ foo: 'bar' })
8
+ expect(plugin.id).toBe('interact')
9
+ expect(typeof plugin.load).toBe('function')
10
+ expect(plugin.foo).toBe('bar') // merged option
11
+ })
12
+
13
+ it('load() returns manifest', async () => {
14
+ const plugin = createPlugin()
15
+ const manifest = await plugin.load()
16
+ expect(manifest).toBeDefined()
17
+ expect(manifest.InitComponent).toBeDefined()
18
+ expect(manifest.reducer).toBeDefined()
19
+ expect(Array.isArray(manifest.buttons)).toBe(true)
20
+ expect(manifest.api).toBeDefined()
21
+ })
22
+ })
@@ -21,4 +21,10 @@
21
21
  .im-o-app--mobile .im-c-mark-location .im-c-map-button {
22
22
  flex: 1 1 auto;
23
23
  min-width: fit-content;
24
+ }
25
+
26
+ // Position touch select button
27
+ .im-c-button-wrapper--select-at-target {
28
+ position: absolute;
29
+ bottom: calc(100% + var(--primary-gap));
24
30
  }
@@ -20,7 +20,7 @@ export const manifest = {
20
20
  label: 'Done',
21
21
  variant: 'primary',
22
22
  excludeWhen: ({ appState, pluginState }) => !pluginState.enabled || !appState.isFullscreen,
23
- enableWhen: ({ mapState, pluginState }) => !!mapState.markers.items.find(m => m.id === 'location') || !!pluginState.selectionBounds,
23
+ enableWhen: ({ mapState, pluginState }) => !!mapState.markers.items.some(m => m.id === 'location') || !!pluginState.selectionBounds,
24
24
  mobile: {
25
25
  slot: 'actions',
26
26
  showLabel: true
@@ -0,0 +1,94 @@
1
+ import { manifest } from './manifest.js'
2
+ import { initialState, actions } from './reducer.js'
3
+
4
+ jest.mock('./api/enable.js', () => ({ enable: jest.fn() }))
5
+ jest.mock('./api/disable.js', () => ({ disable: jest.fn() }))
6
+ jest.mock('./api/clear.js', () => ({ clear: jest.fn() }))
7
+ jest.mock('./api/selectFeature.js', () => ({ selectFeature: jest.fn() }))
8
+ jest.mock('./api/unselectFeature.js', () => ({ unselectFeature: jest.fn() }))
9
+
10
+ describe('manifest', () => {
11
+ it('has InitComponent', () => {
12
+ expect(manifest.InitComponent).toBeDefined()
13
+ })
14
+
15
+ it('has reducer with initialState and actions', () => {
16
+ expect(manifest.reducer.initialState).toBeDefined()
17
+ expect(manifest.reducer.actions).toBeDefined()
18
+ })
19
+
20
+ it('defines buttons with correct ids', () => {
21
+ const ids = manifest.buttons.map(b => b.id)
22
+ expect(ids).toEqual(expect.arrayContaining(['selectDone', 'selectAtTarget', 'selectCancel']))
23
+ })
24
+
25
+ it('buttons have slots and showLabel', () => {
26
+ manifest.buttons.forEach(b => {
27
+ ['mobile','tablet','desktop'].forEach(dev => {
28
+ expect(b[dev].slot).toBe('actions')
29
+ expect(b[dev].showLabel).toBe(true)
30
+ })
31
+ })
32
+ })
33
+
34
+ it('button logic functions cover all branches', () => {
35
+ const done = manifest.buttons.find(b => b.id === 'selectDone')
36
+ const atTarget = manifest.buttons.find(b => b.id === 'selectAtTarget')
37
+ const cancel = manifest.buttons.find(b => b.id === 'selectCancel')
38
+
39
+ // selectDone.excludeWhen
40
+ expect(done.excludeWhen({ appState: { isFullscreen: false }, pluginState: { enabled: true } })).toBe(true)
41
+ expect(done.excludeWhen({ appState: { isFullscreen: true }, pluginState: { enabled: true } })).toBe(false)
42
+
43
+ // selectDone.enableWhen
44
+ expect(done.enableWhen({
45
+ mapState: { markers: { items: [{ id: 'location' }] } },
46
+ pluginState: { selectionBounds: null }
47
+ })).toBe(true)
48
+ expect(done.enableWhen({
49
+ mapState: { markers: { items: [] } },
50
+ pluginState: { selectionBounds: null }
51
+ })).toBe(false)
52
+ expect(done.enableWhen({
53
+ mapState: { markers: { items: [] } },
54
+ pluginState: { selectionBounds: { sw:[0,0], ne:[1,1] } }
55
+ })).toBe(true)
56
+
57
+ // selectAtTarget.hiddenWhen
58
+ expect(atTarget.hiddenWhen({ appState: { interfaceType: 'pointer' }, pluginState: { enabled: true } })).toBe(true)
59
+ expect(atTarget.hiddenWhen({ appState: { interfaceType: 'touch' }, pluginState: { enabled: true } })).toBe(false)
60
+ expect(atTarget.hiddenWhen({ appState: { interfaceType: 'touch' }, pluginState: { enabled: false } })).toBe(true)
61
+
62
+ // selectCancel.hiddenWhen
63
+ expect(cancel.hiddenWhen({
64
+ appConfig: { behaviour: 'always' },
65
+ appState: { isFullscreen: true },
66
+ pluginState: { enabled: true }
67
+ })).toBe(true)
68
+ expect(cancel.hiddenWhen({
69
+ appConfig: { behaviour: 'hybrid' },
70
+ appState: { isFullscreen: true },
71
+ pluginState: { enabled: true }
72
+ })).toBe(false)
73
+ expect(cancel.hiddenWhen({
74
+ appConfig: { behaviour: 'hybrid' },
75
+ appState: { isFullscreen: false },
76
+ pluginState: { enabled: true }
77
+ })).toBe(true)
78
+ expect(cancel.hiddenWhen({
79
+ appConfig: { behaviour: 'hybrid' },
80
+ appState: { isFullscreen: true },
81
+ pluginState: { enabled: false }
82
+ })).toBe(true)
83
+ })
84
+
85
+ it('keyboardShortcuts array exists', () => {
86
+ expect(Array.isArray(manifest.keyboardShortcuts)).toBe(true)
87
+ })
88
+
89
+ it('api exposes expected methods', () => {
90
+ ['enable','disable','clear','selectFeature','unselectFeature'].forEach(fn => {
91
+ expect(typeof manifest.api[fn]).toBe('function')
92
+ })
93
+ })
94
+ })
@@ -4,6 +4,7 @@ const initialState = {
4
4
  markerColor: null,
5
5
  interactionMode: null,
6
6
  multiSelect: false,
7
+ contiguous: false,
7
8
  selectedFeatures: [],
8
9
  selectionBounds: null,
9
10
  closeOnAction: true // Done or Cancel
@@ -20,7 +21,9 @@ const enable = (state, payload) => {
20
21
  const disable = (state) => {
21
22
  return {
22
23
  ...state,
23
- enabled: false
24
+ enabled: false,
25
+ selectedFeatures: [],
26
+ selectionBounds: null
24
27
  }
25
28
  }
26
29
 
@@ -29,37 +32,41 @@ const disable = (state) => {
29
32
  * Structure of items in Set: { featureId: string, layerId: string, idProperty: string }
30
33
  */
31
34
  const toggleSelectedFeatures = (state, payload) => {
32
- const { featureId, multiSelect, layerId, idProperty, addToExisting = true, properties, geometry } = payload
33
- const selected = Array.isArray(state.selectedFeatures) ? [...state.selectedFeatures] : []
34
-
35
- const existingIndex = selected.findIndex(
35
+ const { featureId, multiSelect, layerId, idProperty, addToExisting = true, replaceAll = false, properties, geometry } = payload
36
+ const currentSelected = Array.isArray(state.selectedFeatures) ? state.selectedFeatures : []
37
+
38
+ const existingIndex = currentSelected.findIndex(
36
39
  f => f.featureId === featureId && f.layerId === layerId
37
40
  )
38
41
 
39
- // Handle explicit unselect
42
+ // 1. Handle explicit unselect
40
43
  if (addToExisting === false) {
41
- if (existingIndex !== -1) {
42
- selected.splice(existingIndex, 1) // remove the feature
43
- }
44
- return { ...state, selectedFeatures: selected }
44
+ const filtered = currentSelected.filter((_, i) => i !== existingIndex)
45
+ return { ...state, selectedFeatures: filtered, selectionBounds: null }
45
46
  }
46
47
 
47
- // Multi-select mode (add to selection)
48
- if (multiSelect) {
48
+ // Define the feature object once to avoid repetition
49
+ const featureObj = { featureId, layerId, idProperty, properties, geometry }
50
+ let nextSelected
51
+
52
+ // 2. Determine New State
53
+ // We combine 'replaceAll' and 'single-select' because they share the same logic
54
+ if (multiSelect && !replaceAll) {
55
+ const selectedCopy = [...currentSelected]
49
56
  if (existingIndex === -1) {
50
- selected.push({ featureId, layerId, idProperty, properties, geometry })
57
+ selectedCopy.push(featureObj)
51
58
  } else {
52
- // optional: could also remove existing on toggle
53
- selected.splice(existingIndex, 1)
59
+ selectedCopy.splice(existingIndex, 1)
54
60
  }
55
- return { ...state, selectedFeatures: selected }
61
+ nextSelected = selectedCopy
62
+ } else {
63
+ // Both 'replaceAll' and single-select mode logic:
64
+ // If same feature is already the only one, toggle off; otherwise return just this feature.
65
+ const isSameSingle = existingIndex !== -1 && currentSelected.length === 1
66
+ nextSelected = isSameSingle ? [] : [featureObj]
56
67
  }
57
68
 
58
- // Single-select mode
59
- const isSameSingle = existingIndex !== -1 && selected.length === 1
60
- const newSelected = isSameSingle ? [] : [{ featureId, layerId, idProperty, properties, geometry }]
61
-
62
- return { ...state, selectedFeatures: newSelected }
69
+ return { ...state, selectedFeatures: nextSelected, selectionBounds: null }
63
70
  }
64
71
 
65
72
  // Update bounds (called from useEffect after map provider calculates them)