@defra/interactive-map 0.0.9-alpha → 0.0.11-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 (120) hide show
  1. package/DOCS_README.md +39 -0
  2. package/README.md +1 -1
  3. package/dist/css/index.css +1 -1
  4. package/dist/esm/im-core.js +1 -1
  5. package/dist/esm/im-shell.js +1 -1
  6. package/dist/umd/im-core.js +1 -1
  7. package/dist/umd/index.js +1 -1
  8. package/docs/api/button-definition.md +21 -3
  9. package/docs/api/panel-definition.md +10 -12
  10. package/docs/api.md +81 -8
  11. package/docs/architecture/architecture-diagrams.md +1 -3
  12. package/docs/architecture/diagrams-viewer.mdx +12 -0
  13. package/docs/demo.mdx +70 -0
  14. package/docs/govuk-prototype.md +23 -0
  15. package/docs/index.md +19 -0
  16. package/docs/plugins/plugin-context.md +3 -3
  17. package/docs/plugins/plugin-manifest.md +1 -1
  18. package/docusaurus.config.cjs +136 -0
  19. package/mise.toml +2 -0
  20. package/package.json +27 -5
  21. package/plugins/beta/datasets/dist/esm/im-datasets-plugin.js +1 -1
  22. package/plugins/beta/datasets/dist/umd/im-datasets-plugin.js +1 -1
  23. package/plugins/beta/datasets/src/manifest.js +3 -3
  24. package/plugins/beta/draw-es/dist/esm/im-draw-es-plugin.js +1 -1
  25. package/plugins/beta/draw-es/src/api/newPolygon.js +1 -3
  26. package/plugins/beta/draw-es/src/events.js +2 -2
  27. package/plugins/beta/draw-ml/dist/esm/im-draw-ml-plugin.js +1 -1
  28. package/plugins/beta/draw-ml/dist/umd/im-draw-ml-plugin.js +1 -1
  29. package/plugins/beta/draw-ml/src/api/newLine.js +2 -2
  30. package/plugins/beta/draw-ml/src/api/newPolygon.js +2 -2
  31. package/plugins/beta/draw-ml/src/events.js +18 -10
  32. package/plugins/beta/map-styles/dist/esm/im-map-styles-plugin.js +1 -1
  33. package/plugins/beta/map-styles/dist/umd/im-map-styles-plugin.js +1 -1
  34. package/plugins/beta/map-styles/src/manifest.js +3 -3
  35. package/plugins/beta/use-location/dist/esm/im-use-location-plugin.js +1 -1
  36. package/plugins/beta/use-location/dist/umd/im-use-location-plugin.js +1 -1
  37. package/plugins/beta/use-location/src/manifest.js +7 -7
  38. package/plugins/search/dist/css/index.css +1 -1
  39. package/plugins/search/dist/esm/im-search-plugin.js +1 -1
  40. package/plugins/search/dist/esm/index.js +1 -1
  41. package/plugins/search/dist/umd/im-search-plugin.js +1 -1
  42. package/plugins/search/dist/umd/index.js +1 -1
  43. package/plugins/search/src/Search.jsx +9 -3
  44. package/plugins/search/src/Search.test.jsx +190 -0
  45. package/plugins/search/src/components/CloseButton/CloseButton.test.jsx +67 -0
  46. package/plugins/search/src/components/Form/Form.jsx +35 -7
  47. package/plugins/search/src/components/Form/Form.module.scss +27 -0
  48. package/plugins/search/src/components/Form/Form.test.jsx +255 -0
  49. package/plugins/search/src/components/OpenButton/OpenButton.test.jsx +47 -0
  50. package/plugins/search/src/components/SubmitButton/SubmitButton.jsx +28 -0
  51. package/plugins/search/src/components/SubmitButton/SubmitButton.module.scss +8 -0
  52. package/plugins/search/src/components/SubmitButton/SubmitButton.test.jsx +33 -0
  53. package/plugins/search/src/components/Suggestions/Suggestions.test.jsx +79 -0
  54. package/plugins/search/src/datasets.js +15 -11
  55. package/plugins/search/src/datasets.test.js +61 -0
  56. package/plugins/search/src/events/fetchSuggestions.js +1 -1
  57. package/plugins/search/src/events/fetchSuggestions.test.js +212 -0
  58. package/plugins/search/src/events/formHandlers.test.js +232 -0
  59. package/plugins/search/src/events/index.test.js +118 -0
  60. package/plugins/search/src/events/inputHandlers.test.js +104 -0
  61. package/plugins/search/src/events/suggestionHandlers.test.js +166 -0
  62. package/plugins/search/src/index.js +1 -1
  63. package/plugins/search/src/index.test.js +47 -0
  64. package/plugins/search/src/reducer.js +9 -4
  65. package/plugins/search/src/reducer.test.js +85 -0
  66. package/plugins/search/src/search.scss +5 -1
  67. package/plugins/search/src/utils/parseOsNamesResults.js +20 -3
  68. package/plugins/search/src/utils/parseOsNamesResults.test.js +158 -0
  69. package/plugins/search/src/utils/updateMap.test.js +52 -0
  70. package/providers/beta/esri/dist/esm/im-esri-provider.js +1 -1
  71. package/providers/beta/esri/src/appEvents.js +8 -2
  72. package/providers/beta/esri/src/esriProvider.js +6 -14
  73. package/providers/beta/esri/src/mapEvents.js +7 -1
  74. package/providers/beta/esri/src/utils/coords.js +33 -1
  75. package/providers/beta/esri/src/utils/coords.test.js +126 -0
  76. package/providers/maplibre/dist/esm/im-maplibre-provider.js +1 -1
  77. package/providers/maplibre/dist/esm/index.js +1 -1
  78. package/providers/maplibre/dist/umd/im-maplibre-provider.js +1 -1
  79. package/providers/maplibre/dist/umd/index.js +1 -1
  80. package/providers/maplibre/src/appEvents.js +10 -1
  81. package/providers/maplibre/src/appEvents.test.js +13 -4
  82. package/providers/maplibre/src/index.js +5 -13
  83. package/providers/maplibre/src/index.test.js +34 -15
  84. package/providers/maplibre/src/mapEvents.js +9 -1
  85. package/providers/maplibre/src/maplibreProvider.js +14 -15
  86. package/providers/maplibre/src/maplibreProvider.test.js +14 -1
  87. package/providers/maplibre/src/utils/spatial.js +11 -0
  88. package/providers/maplibre/src/utils/spatial.test.js +12 -0
  89. package/src/App/components/Actions/Actions.module.scss +5 -4
  90. package/src/App/components/MapButton/MapButton.jsx +4 -16
  91. package/src/App/components/MapButton/MapButton.module.scss +12 -12
  92. package/src/App/components/MapButton/MapButton.test.jsx +0 -9
  93. package/src/App/components/Panel/Panel.jsx +6 -6
  94. package/src/App/components/Panel/Panel.test.jsx +14 -15
  95. package/src/App/components/Viewport/MapController.jsx +2 -1
  96. package/src/App/hooks/useLayoutMeasurements.js +1 -1
  97. package/src/App/hooks/useLayoutMeasurements.test.js +1 -1
  98. package/src/App/hooks/useMapProviderOverrides.js +21 -1
  99. package/src/App/hooks/useMapProviderOverrides.test.js +51 -2
  100. package/src/App/layout/Layout.jsx +4 -4
  101. package/src/App/layout/layout.module.scss +1 -0
  102. package/src/App/registry/panelRegistry.js +1 -10
  103. package/src/App/registry/panelRegistry.test.js +6 -11
  104. package/src/App/renderer/HtmlElementHost.jsx +11 -3
  105. package/src/App/renderer/HtmlElementHost.test.jsx +89 -0
  106. package/src/App/renderer/mapButtons.js +128 -28
  107. package/src/App/renderer/mapButtons.test.js +119 -19
  108. package/src/App/store/MapProvider.jsx +18 -5
  109. package/src/App/store/MapProvider.test.jsx +56 -1
  110. package/src/App/store/appActionsMap.js +17 -9
  111. package/src/App/store/appActionsMap.test.js +33 -7
  112. package/src/App/store/mapActionsMap.js +4 -7
  113. package/src/InteractiveMap/InteractiveMap.js +18 -0
  114. package/src/InteractiveMap/InteractiveMap.test.js +12 -0
  115. package/src/config/appConfig.js +17 -15
  116. package/src/config/events.js +41 -4
  117. package/src/config/getInitialOpenPanels.js +2 -2
  118. package/src/config/getInitialOpenPanels.test.js +7 -7
  119. package/src/types.js +13 -11
  120. package/src/utils/getValueForStyle.js +1 -1
@@ -0,0 +1,166 @@
1
+ /**
2
+ * @jest-environment jsdom
3
+ */
4
+ import { createSuggestionHandlers } from './suggestionHandlers.js'
5
+ import { updateMap } from '../utils/updateMap.js'
6
+
7
+ jest.mock('../utils/updateMap.js')
8
+
9
+ describe('createSuggestionHandlers', () => {
10
+ let dispatch
11
+ let services
12
+ let handlers
13
+
14
+ beforeEach(() => {
15
+ dispatch = jest.fn()
16
+
17
+ services = {
18
+ eventBus: { emit: jest.fn() },
19
+ announce: jest.fn()
20
+ }
21
+
22
+ handlers = createSuggestionHandlers({
23
+ dispatch,
24
+ services,
25
+ mapProvider: 'map',
26
+ markers: 'markers',
27
+ showMarker: true,
28
+ markerColor: 'blue'
29
+ })
30
+
31
+ jest.clearAllMocks()
32
+ })
33
+
34
+ // ---------- Suggestion click ----------
35
+
36
+ test('handleSuggestionClick (desktop)', () => {
37
+ const suggestion = { text: 'Paris', bounds: 'b', point: 'p' }
38
+
39
+ handlers.handleSuggestionClick(suggestion, { breakpoint: 'desktop' })
40
+
41
+ expect(dispatch).toHaveBeenCalledWith({ type: 'SET_VALUE', payload: 'Paris' })
42
+ expect(dispatch).toHaveBeenCalledWith({ type: 'HIDE_SUGGESTIONS' })
43
+ expect(dispatch).toHaveBeenCalledWith({ type: 'SET_SELECTED', payload: -1 })
44
+ expect(updateMap).toHaveBeenCalledWith(expect.objectContaining({ bounds: 'b', point: 'p' }))
45
+ expect(services.eventBus.emit).toHaveBeenCalledWith(
46
+ 'search:match',
47
+ expect.objectContaining({ query: 'Paris' })
48
+ )
49
+ })
50
+
51
+ test('handleSuggestionClick (mobile closes search)', () => {
52
+ const suggestion = { text: 'Berlin', bounds: 'b', point: 'p' }
53
+
54
+ handlers.handleSuggestionClick(suggestion, { breakpoint: 'mobile' })
55
+
56
+ expect(dispatch).toHaveBeenCalledWith({ type: 'TOGGLE_EXPANDED', payload: false })
57
+ expect(services.eventBus.emit).toHaveBeenCalledWith('search:close')
58
+ })
59
+
60
+ // ---------- ArrowDown ----------
61
+
62
+ test('ArrowDown selects next suggestion', () => {
63
+ const e = { key: 'ArrowDown', preventDefault: jest.fn() }
64
+
65
+ handlers.handleInputKeyDown(e, {
66
+ suggestions: [{ text: 'A' }, { text: 'B' }],
67
+ selectedIndex: 0
68
+ })
69
+
70
+ expect(e.preventDefault).toHaveBeenCalled()
71
+ expect(services.announce).toHaveBeenCalledWith('B. 2 of 2 is highlighted')
72
+ expect(dispatch).toHaveBeenCalledWith({ type: 'SET_SELECTED', payload: 1 })
73
+ expect(dispatch).toHaveBeenCalledWith({ type: 'SET_KEYBOARD_FOCUS_WITHIN', payload: false })
74
+ })
75
+
76
+ test('ArrowDown does nothing when no suggestions', () => {
77
+ const e = { key: 'ArrowDown', preventDefault: jest.fn() }
78
+
79
+ handlers.handleInputKeyDown(e, { suggestions: [], selectedIndex: 0 })
80
+
81
+ expect(e.preventDefault).not.toHaveBeenCalled()
82
+ expect(dispatch).not.toHaveBeenCalled()
83
+ expect(services.announce).not.toHaveBeenCalled()
84
+ })
85
+
86
+ test('ArrowDown does nothing when at last suggestion', () => {
87
+ const e = { key: 'ArrowDown', preventDefault: jest.fn() }
88
+
89
+ handlers.handleInputKeyDown(e, {
90
+ suggestions: [{ text: 'A' }, { text: 'B' }],
91
+ selectedIndex: 1 // last index
92
+ })
93
+
94
+ expect(e.preventDefault).toHaveBeenCalled()
95
+ expect(dispatch).not.toHaveBeenCalled()
96
+ expect(services.announce).not.toHaveBeenCalled()
97
+ })
98
+
99
+ // ---------- ArrowUp ----------
100
+
101
+ test('ArrowUp moves selection up and resets to -1', () => {
102
+ const e = { key: 'ArrowUp', preventDefault: jest.fn() }
103
+
104
+ handlers.handleInputKeyDown(e, {
105
+ suggestions: [{ text: 'A' }, { text: 'B' }],
106
+ selectedIndex: 0
107
+ })
108
+
109
+ expect(e.preventDefault).toHaveBeenCalled()
110
+ expect(services.announce).toHaveBeenCalledWith('2 suggestions available')
111
+ expect(dispatch).toHaveBeenCalledWith({ type: 'SET_SELECTED', payload: -1 })
112
+ expect(dispatch).toHaveBeenCalledWith({ type: 'SET_KEYBOARD_FOCUS_WITHIN', payload: true })
113
+ })
114
+
115
+ test('ArrowUp moves selection up normally when selectedIndex > 0', () => {
116
+ const e = { key: 'ArrowUp', preventDefault: jest.fn() }
117
+
118
+ handlers.handleInputKeyDown(e, {
119
+ suggestions: [{ text: 'A' }, { text: 'B' }, { text: 'C' }],
120
+ selectedIndex: 2
121
+ })
122
+
123
+ expect(e.preventDefault).toHaveBeenCalled()
124
+ expect(services.announce).toHaveBeenCalledWith('B. 2 of 3 is highlighted')
125
+ expect(dispatch).toHaveBeenCalledWith({ type: 'SET_SELECTED', payload: 1 })
126
+ expect(dispatch).toHaveBeenCalledWith({ type: 'SET_KEYBOARD_FOCUS_WITHIN', payload: false })
127
+ })
128
+
129
+ test('ArrowUp does nothing when no suggestions', () => {
130
+ const e = { key: 'ArrowUp', preventDefault: jest.fn() }
131
+
132
+ handlers.handleInputKeyDown(e, { suggestions: [], selectedIndex: 0 })
133
+
134
+ expect(e.preventDefault).not.toHaveBeenCalled()
135
+ expect(dispatch).not.toHaveBeenCalled()
136
+ expect(services.announce).not.toHaveBeenCalled()
137
+ })
138
+
139
+ // ---------- Escape & default ----------
140
+
141
+ test('Escape hides suggestions and clears selection', () => {
142
+ const e = { key: 'Escape', preventDefault: jest.fn() }
143
+
144
+ handlers.handleInputKeyDown(e, {
145
+ suggestions: [{ text: 'A' }],
146
+ selectedIndex: 0
147
+ })
148
+
149
+ expect(e.preventDefault).toHaveBeenCalled()
150
+ expect(dispatch).toHaveBeenCalledWith({ type: 'HIDE_SUGGESTIONS' })
151
+ expect(dispatch).toHaveBeenCalledWith({ type: 'SET_SELECTED', payload: -1 })
152
+ })
153
+
154
+ test('Other keys do nothing', () => {
155
+ const e = { key: 'Enter', preventDefault: jest.fn() }
156
+
157
+ handlers.handleInputKeyDown(e, {
158
+ suggestions: [{ text: 'A' }],
159
+ selectedIndex: 0
160
+ })
161
+
162
+ expect(e.preventDefault).not.toHaveBeenCalled()
163
+ expect(dispatch).not.toHaveBeenCalled()
164
+ expect(services.announce).not.toHaveBeenCalled()
165
+ })
166
+ })
@@ -3,7 +3,7 @@ import './search.scss'
3
3
 
4
4
  export default function createPlugin (options = {}) {
5
5
  // If search is open then we need to overirde the mobile slot
6
- if (options.isExpanded) {
6
+ if (options.expanded) {
7
7
  options.manifest = {controls: [{ id: 'search', mobile: { slot: 'banner' }}]}
8
8
  }
9
9
 
@@ -0,0 +1,47 @@
1
+ // /plugins/search/index.test.js
2
+
3
+ // Mock SCSS import so Jest can run without parsing errors
4
+ jest.mock('./search.scss', () => {})
5
+
6
+ import createPlugin from './index'
7
+
8
+ describe('createPlugin', () => {
9
+ beforeEach(() => {
10
+ jest.resetModules()
11
+ })
12
+
13
+ it('returns default plugin structure with showMarker and id', async () => {
14
+ const plugin = createPlugin()
15
+ expect(plugin.showMarker).toBe(true)
16
+ expect(plugin.id).toBe('search')
17
+ expect(typeof plugin.load).toBe('function')
18
+ })
19
+
20
+ it('overrides manifest when expanded is true', () => {
21
+ const plugin = createPlugin({ expanded: true })
22
+ expect(plugin.expanded).toBe(true)
23
+ expect(plugin.manifest).toEqual({
24
+ controls: [{ id: 'search', mobile: { slot: 'banner' } }]
25
+ })
26
+ })
27
+
28
+ it('spreads custom options correctly', () => {
29
+ const custom = { foo: 'bar', expanded: false }
30
+ const plugin = createPlugin(custom)
31
+ expect(plugin.foo).toBe('bar')
32
+ expect(plugin.showMarker).toBe(true)
33
+ expect(plugin.id).toBe('search')
34
+ })
35
+
36
+ it('load function dynamically imports the manifest and returns it', async () => {
37
+ // Mock the dynamic import for './manifest.js'
38
+ const manifestMock = { data: 'test-manifest' }
39
+ jest.mock('./manifest.js', () => ({
40
+ manifest: manifestMock
41
+ }), { virtual: true })
42
+
43
+ const plugin = createPlugin()
44
+ const result = await plugin.load()
45
+ expect(result).toEqual(manifestMock)
46
+ })
47
+ })
@@ -4,6 +4,7 @@ const initialState = {
4
4
  value: '',
5
5
  suggestions: [],
6
6
  areSuggestionsVisible: false,
7
+ hasFetchedSuggestions: false,
7
8
  selectedIndex: -1
8
9
  }
9
10
 
@@ -12,7 +13,8 @@ const toggleExpanded = (state, payload) => {
12
13
  return {
13
14
  ...state,
14
15
  isExpanded: payload,
15
- areSuggestionsVisible: payload
16
+ areSuggestionsVisible: payload,
17
+ hasFetchedSuggestions: false
16
18
  }
17
19
  }
18
20
 
@@ -43,21 +45,24 @@ const setValue = (state, payload) => {
43
45
  const updateSuggestions = (state, payload) => {
44
46
  return {
45
47
  ...state,
46
- suggestions: payload
48
+ suggestions: payload,
49
+ hasFetchedSuggestions: true
47
50
  }
48
51
  }
49
52
 
50
53
  const showSuggestions = (state) => {
51
54
  return {
52
55
  ...state,
53
- areSuggestionsVisible: true
56
+ areSuggestionsVisible: true,
57
+ hasFetchedSuggestions: false
54
58
  }
55
59
  }
56
60
 
57
61
  const hideSuggestions = (state) => {
58
62
  return {
59
63
  ...state,
60
- areSuggestionsVisible: false
64
+ areSuggestionsVisible: false,
65
+ hasFetchedSuggestions: false
61
66
  }
62
67
  }
63
68
 
@@ -0,0 +1,85 @@
1
+ // /plugins/search/reducer.test.js
2
+
3
+ import { initialState, actions } from './reducer'
4
+
5
+ describe('search state actions', () => {
6
+ it('TOGGLE_EXPANDED sets isExpanded and areSuggestionsVisible, and resets hasFetchedSuggestions', () => {
7
+ const state = { ...initialState, hasFetchedSuggestions: true }
8
+ const newState = actions.TOGGLE_EXPANDED(state, true)
9
+ expect(newState.isExpanded).toBe(true)
10
+ expect(newState.areSuggestionsVisible).toBe(true)
11
+ expect(newState.hasFetchedSuggestions).toBe(false)
12
+
13
+ const collapsed = actions.TOGGLE_EXPANDED(state, false)
14
+ expect(collapsed.isExpanded).toBe(false)
15
+ expect(collapsed.areSuggestionsVisible).toBe(false)
16
+ expect(collapsed.hasFetchedSuggestions).toBe(false)
17
+ })
18
+
19
+ it('SET_KEYBOARD_FOCUS_WITHIN sets focus and shows suggestions', () => {
20
+ const state = { ...initialState }
21
+ const newState = actions.SET_KEYBOARD_FOCUS_WITHIN(state, true)
22
+ expect(newState.hasKeyboardFocusWithin).toBe(true)
23
+ expect(newState.areSuggestionsVisible).toBe(true)
24
+
25
+ const removedFocus = actions.SET_KEYBOARD_FOCUS_WITHIN(state, false)
26
+ expect(removedFocus.hasKeyboardFocusWithin).toBe(false)
27
+ expect(removedFocus.areSuggestionsVisible).toBe(true) // always true
28
+ })
29
+
30
+ it('INPUT_BLUR removes focus and updates areSuggestionsVisible correctly', () => {
31
+ const state = { ...initialState, areSuggestionsVisible: true, hasKeyboardFocusWithin: true }
32
+
33
+ // Non-keyboard blur
34
+ const newStateMouse = actions.INPUT_BLUR(state, 'mouse')
35
+ expect(newStateMouse.hasKeyboardFocusWithin).toBe(false)
36
+ expect(newStateMouse.areSuggestionsVisible).toBe(true) // still true
37
+ expect(newStateMouse.selectedIndex).toBe(-1)
38
+
39
+ // Keyboard blur hides suggestions
40
+ const newStateKeyboard = actions.INPUT_BLUR(state, 'keyboard')
41
+ expect(newStateKeyboard.hasKeyboardFocusWithin).toBe(false)
42
+ expect(newStateKeyboard.areSuggestionsVisible).toBe(false) // now false
43
+ expect(newStateKeyboard.selectedIndex).toBe(-1)
44
+ })
45
+
46
+ it('SET_VALUE updates the input value', () => {
47
+ const state = { ...initialState }
48
+ const newState = actions.SET_VALUE(state, 'test')
49
+ expect(newState.value).toBe('test')
50
+ })
51
+
52
+ it('UPDATE_SUGGESTIONS updates the suggestions array and sets hasFetchedSuggestions', () => {
53
+ const state = { ...initialState }
54
+ const suggestions = [{ id: 1 }, { id: 2 }]
55
+ const newState = actions.UPDATE_SUGGESTIONS(state, suggestions)
56
+ expect(newState.suggestions).toEqual(suggestions)
57
+ expect(newState.hasFetchedSuggestions).toBe(true)
58
+ })
59
+
60
+ it('SHOW_SUGGESTIONS sets areSuggestionsVisible to true and resets hasFetchedSuggestions', () => {
61
+ const state = { ...initialState, areSuggestionsVisible: false, hasFetchedSuggestions: true }
62
+ const newState = actions.SHOW_SUGGESTIONS(state)
63
+ expect(newState.areSuggestionsVisible).toBe(true)
64
+ expect(newState.hasFetchedSuggestions).toBe(false)
65
+ })
66
+
67
+ it('HIDE_SUGGESTIONS sets areSuggestionsVisible to false and resets hasFetchedSuggestions', () => {
68
+ const state = { ...initialState, areSuggestionsVisible: true, hasFetchedSuggestions: true }
69
+ const newState = actions.HIDE_SUGGESTIONS(state)
70
+ expect(newState.areSuggestionsVisible).toBe(false)
71
+ expect(newState.hasFetchedSuggestions).toBe(false)
72
+ })
73
+
74
+ it('SET_SELECTED updates selectedIndex and visibility', () => {
75
+ const state = { ...initialState, areSuggestionsVisible: false }
76
+
77
+ const selected = actions.SET_SELECTED(state, 1)
78
+ expect(selected.selectedIndex).toBe(1)
79
+ expect(selected.areSuggestionsVisible).toBe(true)
80
+
81
+ const deselected = actions.SET_SELECTED(state, -1)
82
+ expect(deselected.selectedIndex).toBe(-1)
83
+ expect(deselected.areSuggestionsVisible).toBe(false)
84
+ })
85
+ })
@@ -1,11 +1,15 @@
1
1
  @use '../../../src/scss/tools/index' as tools;
2
2
 
3
-
4
3
  // Components, using 'CSS in Component' pattern
5
4
  @use './components/Form/Form.module';
6
5
  @use './components/CloseButton/CloseButton.module';
6
+ @use './components/SubmitButton/SubmitButton.module';
7
7
  @use './components/Suggestions/Suggestions.module';
8
8
 
9
+ :root {
10
+ --search-drop-shadow: drop-shadow(0 -2px 20px rgba(0,0,0,.1)) drop-shadow(0 0 12px rgba(0,0,0,.1));
11
+ }
12
+
9
13
  // ===================================================
10
14
  // Component: Search
11
15
  // ===================================================
@@ -10,12 +10,27 @@ const isPostcode = (value) => {
10
10
  return regex.test(value)
11
11
  }
12
12
 
13
+ const removeRegions = (results, regions) => results.filter(r => {
14
+ const country = r?.GAZETTEER_ENTRY?.COUNTRY?.toLowerCase()
15
+ return country && regions.includes(country)
16
+ })
17
+
13
18
  const removeDuplicates = (results) =>
14
19
  Array.from(new Map(results.map(r => [r.GAZETTEER_ENTRY.ID, r])).values())
15
20
 
21
+ const transformGazetteerResult = (result) => {
22
+ const gazetterEntry = result.GAZETTEER_ENTRY
23
+ // Sometimes the NAME1 value can be welsh, so we should use NAME2 instead
24
+ const name = gazetterEntry.NAME2_LANG === 'eng' ? gazetterEntry.NAME2 : gazetterEntry.NAME1
25
+ // Force NAME1 to be the english value
26
+ result.GAZETTEER_ENTRY.NAME1 = name
27
+ result.GAZETTEER_ENTRY.NAME1_LANG = 'eng'
28
+ return result
29
+ }
30
+
16
31
  const removeTenuousResults = (results, query) => {
17
32
  const words = query.toLowerCase().replace(/,/g, '').split(' ')
18
- return results.filter(l => words.some(w => l.GAZETTEER_ENTRY.NAME1.toLowerCase().includes(w) || isPostcode(query)))
33
+ return results.map(transformGazetteerResult).filter(l => words.some(w => l.GAZETTEER_ENTRY.NAME1.toLowerCase().includes(w) || isPostcode(query)))
19
34
  }
20
35
 
21
36
  const markString = (string, find) => {
@@ -86,13 +101,14 @@ const label = (query, { NAME1, COUNTY_UNITARY, DISTRICT_BOROUGH, POSTCODE_DISTRI
86
101
  }
87
102
  }
88
103
 
89
- const parseOsNamesResults = (json, query, crs) => {
104
+ const parseOsNamesResults = (json, query, regions, crs) => {
90
105
  if (!json || json.error || json.header?.totalresults === 0) {
91
106
  return []
92
107
  }
93
108
  let results = json.results
94
109
  results = removeTenuousResults(results, query)
95
110
  results = removeDuplicates(results)
111
+ results = removeRegions(results, regions)
96
112
  results = results.slice(0, MAX_RESULTS)
97
113
 
98
114
  return results.map(l => ({
@@ -105,5 +121,6 @@ const parseOsNamesResults = (json, query, crs) => {
105
121
  }
106
122
 
107
123
  export {
108
- parseOsNamesResults
124
+ parseOsNamesResults,
125
+ point
109
126
  }
@@ -0,0 +1,158 @@
1
+ /**
2
+ * @jest-environment jsdom
3
+ */
4
+ import { parseOsNamesResults, point } from './parseOsNamesResults.js'
5
+
6
+ // Mock OsGridRef so we can control toLatLon outputs
7
+ jest.mock('geodesy/osgridref.js', () => {
8
+ return jest.fn().mockImplementation((x, y) => ({
9
+ x,
10
+ y,
11
+ toLatLon: () => ({ lat: y / 1e5, lon: x / 1e5 }) // deterministic lat/lon
12
+ }))
13
+ })
14
+
15
+ describe('osNamesUtils', () => {
16
+ const regions = ['england', 'scotland', 'wales']
17
+
18
+ const sampleEntry = {
19
+ GAZETTEER_ENTRY: {
20
+ ID: 1,
21
+ NAME1: 'London',
22
+ COUNTY_UNITARY: 'Greater London',
23
+ DISTRICT_BOROUGH: 'Camden',
24
+ POSTCODE_DISTRICT: 'WC1',
25
+ LOCAL_TYPE: 'Town',
26
+ COUNTRY: 'England',
27
+ MBR_XMIN: 1000,
28
+ MBR_YMIN: 2000,
29
+ MBR_XMAX: 3000,
30
+ MBR_YMAX: 4000,
31
+ GEOMETRY_X: 1500,
32
+ GEOMETRY_Y: 2500
33
+ }
34
+ }
35
+
36
+ test('returns empty array for null/invalid/error results', () => {
37
+ expect(parseOsNamesResults(null, 'x', regions, 'EPSG:27700')).toEqual([])
38
+ expect(parseOsNamesResults({ error: true }, 'x', regions, 'EPSG:27700')).toEqual([])
39
+ expect(parseOsNamesResults({ header: { totalresults: 0 } }, 'x', regions, 'EPSG:27700')).toEqual([])
40
+ })
41
+
42
+ test('removes tenuous results when query does not match', () => {
43
+ const results = [
44
+ { GAZETTEER_ENTRY: { ...sampleEntry.GAZETTEER_ENTRY, NAME1: 'Bristol', ID: 2 } }
45
+ ]
46
+ const json = { results }
47
+ const output = parseOsNamesResults(json, 'London', regions, 'EPSG:27700')
48
+ expect(output).toHaveLength(0)
49
+ })
50
+
51
+ test('removes duplicate IDs', () => {
52
+ const dup = { ...sampleEntry, GAZETTEER_ENTRY: { ...sampleEntry.GAZETTEER_ENTRY } }
53
+ const json = { results: [sampleEntry, dup] }
54
+ const output = parseOsNamesResults(json, 'London', regions, 'EPSG:27700')
55
+ expect(output).toHaveLength(1)
56
+ })
57
+
58
+ test('limits results to MAX_RESULTS', () => {
59
+ const manyResults = Array.from({ length: 10 }, (_, i) => ({
60
+ GAZETTEER_ENTRY: { ...sampleEntry.GAZETTEER_ENTRY, ID: i }
61
+ }))
62
+ const json = { results: manyResults }
63
+ const output = parseOsNamesResults(json, 'London', regions, 'EPSG:27700')
64
+ expect(output).toHaveLength(8)
65
+ })
66
+
67
+ test('bounds returns raw OSGB values for EPSG:27700', () => {
68
+ const json = { results: [sampleEntry] }
69
+ const output = parseOsNamesResults(json, 'London', regions, 'EPSG:27700')
70
+ expect(output[0].bounds).toEqual([
71
+ sampleEntry.GAZETTEER_ENTRY.MBR_XMIN,
72
+ sampleEntry.GAZETTEER_ENTRY.MBR_YMIN,
73
+ sampleEntry.GAZETTEER_ENTRY.MBR_XMAX,
74
+ sampleEntry.GAZETTEER_ENTRY.MBR_YMAX
75
+ ])
76
+ expect(output[0].point).toEqual([
77
+ sampleEntry.GAZETTEER_ENTRY.GEOMETRY_X,
78
+ sampleEntry.GAZETTEER_ENTRY.GEOMETRY_Y
79
+ ])
80
+ })
81
+
82
+ test('bounds converts to WGS84 for EPSG:4326', () => {
83
+ const json = { results: [sampleEntry] }
84
+ const output = parseOsNamesResults(json, 'London', regions, 'EPSG:4326')
85
+ const expectedBounds = [
86
+ Math.round(1000 / 1e5 * 1e6) / 1e6,
87
+ Math.round(2000 / 1e5 * 1e6) / 1e6,
88
+ Math.round(3000 / 1e5 * 1e6) / 1e6,
89
+ Math.round(4000 / 1e5 * 1e6) / 1e6
90
+ ]
91
+ expect(output[0].bounds).toEqual(expectedBounds)
92
+ const expectedPoint = [
93
+ Math.round(1500 / 1e5 * 1e6) / 1e6,
94
+ Math.round(2500 / 1e5 * 1e6) / 1e6
95
+ ]
96
+ expect(output[0].point).toEqual(expectedPoint)
97
+ })
98
+
99
+ test('label generates marked text', () => {
100
+ const json = { results: [sampleEntry] }
101
+ const output = parseOsNamesResults(json, 'London', regions, 'EPSG:27700')
102
+ expect(output[0].text).toContain('London')
103
+ expect(output[0].marked).toContain('<mark>')
104
+ })
105
+
106
+ test('handles MBR_XMIN null by using buffered GEOMETRY_X/Y', () => {
107
+ const entry = {
108
+ GAZETTEER_ENTRY: { ...sampleEntry.GAZETTEER_ENTRY, MBR_XMIN: null, MBR_YMIN: null }
109
+ }
110
+ const json = { results: [entry] }
111
+ const output = parseOsNamesResults(json, 'London', regions, 'EPSG:27700')
112
+ expect(output[0].bounds).toEqual([
113
+ entry.GAZETTEER_ENTRY.GEOMETRY_X - 500,
114
+ entry.GAZETTEER_ENTRY.GEOMETRY_Y - 500,
115
+ entry.GAZETTEER_ENTRY.GEOMETRY_X + 500,
116
+ entry.GAZETTEER_ENTRY.GEOMETRY_Y + 500
117
+ ])
118
+ })
119
+
120
+ test('label falls back to DISTRICT_BOROUGH when COUNTY_UNITARY is absent', () => {
121
+ const entry = { GAZETTEER_ENTRY: { ...sampleEntry.GAZETTEER_ENTRY, COUNTY_UNITARY: null } }
122
+ const output = parseOsNamesResults({ results: [entry] }, 'London', regions, 'EPSG:27700')
123
+ expect(output[0].text).toContain(sampleEntry.GAZETTEER_ENTRY.DISTRICT_BOROUGH)
124
+ })
125
+
126
+ test('label omits qualifier for City type', () => {
127
+ const entry = { GAZETTEER_ENTRY: { ...sampleEntry.GAZETTEER_ENTRY, LOCAL_TYPE: 'City' } }
128
+ const output = parseOsNamesResults({ results: [entry] }, 'London', regions, 'EPSG:27700')
129
+ expect(output[0].text).toBe('London')
130
+ })
131
+
132
+ /**
133
+ * Cover transformGazetteerResult branches
134
+ */
135
+
136
+ test('uses NAME2 when NAME2_LANG is eng', () => {
137
+ const entry = { GAZETTEER_ENTRY: { ...sampleEntry.GAZETTEER_ENTRY, NAME1: 'Caerdydd', NAME2: 'Cardiff', NAME2_LANG: 'eng' }}
138
+ const output = parseOsNamesResults({ results: [entry] }, 'Cardiff', regions, 'EPSG:27700')
139
+ expect(output[0].text).toContain('Cardiff')
140
+ })
141
+
142
+ test('falls back to NAME1 when NAME2_LANG is not eng', () => {
143
+ const entry = { GAZETTEER_ENTRY: { ...sampleEntry.GAZETTEER_ENTRY, NAME1: 'London', NAME2: 'Londres', NAME2_LANG: 'fra' }}
144
+ const output = parseOsNamesResults({ results: [entry] }, 'London', regions, 'EPSG:27700')
145
+ expect(output[0].text).toContain('London')
146
+ })
147
+
148
+ test('throws error for unsupported CRS', () => {
149
+ const entry = { GAZETTEER_ENTRY: { ...sampleEntry.GAZETTEER_ENTRY } }
150
+ const json = { results: [entry] }
151
+ expect(() => parseOsNamesResults(json, 'London', regions, 'EPSG:9999')).toThrow('Unsupported CRS')
152
+ })
153
+
154
+ test('point function throws error for unsupported CRS', () => {
155
+ const coords = { GEOMETRY_X: 1500, GEOMETRY_Y: 2500 }
156
+ expect(() => point('EPSG:9999', coords)).toThrow('Unsupported CRS: EPSG:9999')
157
+ })
158
+ })
@@ -0,0 +1,52 @@
1
+ // src/plugins/search/utils/updateMap.test.js
2
+
3
+ import { updateMap } from './updateMap.js'
4
+
5
+ describe('updateMap', () => {
6
+ let mapProvider
7
+ let markers
8
+ const bounds = { north: 1, south: 0 }
9
+ const point = { lat: 10, lng: 20 }
10
+
11
+ beforeEach(() => {
12
+ mapProvider = {
13
+ fitToBounds: jest.fn(),
14
+ }
15
+
16
+ markers = {
17
+ add: jest.fn(),
18
+ }
19
+ })
20
+
21
+ it('fits the map to bounds and adds a marker when showMarker is true', () => {
22
+ updateMap({
23
+ mapProvider,
24
+ bounds,
25
+ point,
26
+ markers,
27
+ showMarker: true,
28
+ markerColor: 'red',
29
+ })
30
+
31
+ expect(mapProvider.fitToBounds).toHaveBeenCalledWith(bounds)
32
+ expect(markers.add).toHaveBeenCalledWith(
33
+ 'search',
34
+ point,
35
+ { color: 'red' }
36
+ )
37
+ })
38
+
39
+ it('fits the map to bounds and does not add a marker when showMarker is false', () => {
40
+ updateMap({
41
+ mapProvider,
42
+ bounds,
43
+ point,
44
+ markers,
45
+ showMarker: false,
46
+ markerColor: 'blue',
47
+ })
48
+
49
+ expect(mapProvider.fitToBounds).toHaveBeenCalledWith(bounds)
50
+ expect(markers.add).not.toHaveBeenCalled()
51
+ })
52
+ })
@@ -1 +1 @@
1
- import e from"@babel/runtime/helpers/objectWithoutProperties";import t from"@babel/runtime/helpers/asyncToGenerator";import i from"@arcgis/core/config.js";import n from"@arcgis/core/Map.js";import o from"@arcgis/core/views/MapView.js";import r from"@arcgis/core/layers/VectorTileLayer.js";import{when as a,once as s,watch as m}from"@arcgis/core/core/reactiveUtils.js";import u from"@arcgis/core/geometry/Extent.js";import l from"@arcgis/core/geometry/Point.js";var v={animationDuration:200},c=["showKeyboardHelp","selectControl","moveLarge","nudgeMap","zoomLarge","nudgeZoom"];var p=(e,t)=>{var i=null,n=function(){for(var n=arguments.length,o=new Array(n),r=0;r<n;r++)o[r]=arguments[r];clearTimeout(i),i=setTimeout(()=>{e(...o)},t)};return n.cancel=()=>{i&&(clearTimeout(i),i=null)},n};function h(e){var{mapProvider:t,view:i,baseTileLayer:n,events:o,eventBus:r,getZoom:u,getCenter:l,getBounds:v,getResolution:c}=e,h=!1,d=[],y=[],g=e=>{var t=(()=>{if(h||!i||i.destroyed||!i.extent)return null;var{maxZoom:e,minZoom:t}=i.constraints;return{center:l(),bounds:v(),resolution:c(),zoom:u(),isAtMaxZoom:i.zoom+.01>=e,isAtMinZoom:i.zoom-.01<=t}})();t&&r.emit(e,t)};a(()=>n.loaded&&i.resolution>0,()=>g(o.MAP_LOADED)),s(()=>i.ready).then(()=>{h||r.emit(o.MAP_READY,t.getMapAPI())}),s(()=>i.stationary).then(()=>g(o.MAP_FIRST_IDLE));var w=p(()=>g(o.MAP_MOVE_END),500);y.push(w),d.push(m(()=>[i.interacting,i.animation],e=>{var[t,i]=e;(t||i)&&r.emit(o.MAP_MOVE_START),t||i||w()}));var f,x,M,b=(f=()=>g(o.MAP_MOVE),x=10,M=0,function(){var e=Date.now();e-M>=x&&(M=e,f(...arguments))});y.push(b),d.push(m(()=>i.zoom,b)),d.push(m(()=>i.extent,()=>r.emit(o.MAP_RENDER),{initial:!1}));var E=p(()=>g(o.MAP_DATA_CHANGE),500);return y.push(E),d.push(m(()=>i.updating,e=>!e&&E())),d.push(i.on("click",e=>{var t=e.mapPoint,i={x:e.x,y:e.y};r.emit(o.MAP_CLICK,{point:i,coords:[t.x,t.y]})})),{remove(){h=!0,y.forEach(e=>e.cancel()),d.forEach(e=>e.remove())}}}var d=e=>{var t=1609.344,i=e/t;if(i<.5/t)return"".concat(Math.round(e),"m");if(i<10){var n=Number.parseFloat(i.toFixed(1)),o=1===n?"mile":"miles";return"".concat(n," ").concat(o)}var r=Math.round(i),a=1===r?"mile":"miles";return"".concat(r," ").concat(a)};var y={top:0,right:0,bottom:0,left:0};function g(){return(g=t(function*(e,t){if(!e||!t)return[];var i=e.map.layers.filter(e=>e instanceof r);return i.length?(yield e.hitTest(t,{include:i.toArray()})).results.map(e=>({layerId:e.layer.id,layerTitle:e.layer.title||e.layer.id,type:e.layer.type,geometry:e.graphic.geometry,symbol:e.graphic.symbol})):[]})).apply(this,arguments)}var w=e=>e?new u({xmin:e[0],ymin:e[1],xmax:e[2],ymax:e[3],spatialReference:{wkid:27700}}):void 0,f=e=>e?new l({x:e[0],y:e[1],spatialReference:{wkid:27700}}):void 0,x=["container","padding","mapStyle","maxExtent"];class M{constructor(e){var{mapProviderConfig:t={},events:i,eventBus:n}=e;this.events=i,this.eventBus=n,this.capabilities={supportedShortcuts:c,supportsMapSizes:!1},Object.assign(this,t),this.mapEventHandles=[],this.appEventHandles=[]}initMap(a){var s=this;return t(function*(){var{container:t,padding:m,mapStyle:u,maxExtent:l}=a;e(a,x);var{events:v,eventBus:c}=s;s.setupConfig&&(yield s.setupConfig(i));var p=new r({id:"baselayer",url:u.url,visible:!0}),d=new n({layers:[p]}),y=l?w(l):null,g=new o({spatialReference:27700,container:t,map:d,zoom:a.zoom,center:f(a.center),maxExtent:l,constraints:{snapToZoom:!1,minZoom:a.minZoom,maxZoom:a.maxZoom,maxScale:0,geometry:y,rotationEnabled:!1},ui:{components:[]},popupEnabled:!1});(e=>{if(e){var t=e.querySelector(".esri-view-surface");t.removeAttribute("role"),t.tabIndex=-1,t.style["outline-color"]="transparent",t.style.touchAction="none"}})(g.container),g.padding=m,a.bounds&&g.when(()=>g.goTo(w(a.bounds),{duration:0})),s.mapEventHandles=h({mapProvider:s,view:g,baseTileLayer:p,events:v,eventBus:c,getZoom:s.getZoom.bind(s),getCenter:s.getCenter.bind(s),getBounds:s.getBounds.bind(s),getResolution:s.getResolution.bind(s)}),s.appEventHandles=function(e){var{baseTileLayer:t,events:i,eventBus:n}=e,o=e=>{t.loadStyle(e.url).then(()=>{n.emit(i.MAP_STYLE_CHANGE,e)})};return n.on(i.MAP_SET_STYLE,o),{remove(){n.off(i.MAP_SET_STYLE,o)}}}({baseTileLayer:p,events:v,eventBus:c})||[],s.map=d,s.view=g,s.baseTileLayer=p})()}destroyMap(){var e,t;null===(e=this.mapEvents)||void 0===e||e.remove(),null===(t=this.appEvents)||void 0===t||t.remove(),this.mapEvents=null,this.appEvents=null,this.view&&(this.view.container=null,this.view.destroy(),this.view=null),this.map&&(this.map.removeAll(),this.map=null)}getMapAPI(){return{map:this.map,view:this.view,crs:this.crs,fitToBounds:this.fitToBounds.bind(this),setView:this.setView.bind(this)}}setView(e){var t,{center:i,zoom:n}=e;null===(t=this.view.animation)||void 0===t||t.destroy(),this.view.goTo({center:i,zoom:n,duration:v.animationDuration})}zoomIn(e){var t;null===(t=this.view.animation)||void 0===t||t.destroy(),this.view.goTo({zoom:this.view.zoom+e,duration:v.animationDuration})}zoomOut(e){var t;null===(t=this.view.animation)||void 0===t||t.destroy(),this.view.goTo({zoom:this.view.zoom-e,duration:v.animationDuration})}panBy(e){var{x:t,y:i}=this.view.toScreen(this.view.center),n={x:t+e[0],y:i+e[1]},o=this.view.toMap(n);this.view.goTo({center:o,duration:v.animationDuration})}fitToBounds(e){this.view.goTo(w(e),{duration:v.DELAY})}setPadding(e){this.view.padding=e}getCenter(){var e=this.view.center;return[e.x,e.y].map(e=>Math.round(100*e)/100)}getZoom(){return this.view.zoom}getBounds(){var{xmin:e,ymin:t,xmax:i,ymax:n}=this.view.extent;return[e,t,i,n].map(e=>Math.round(100*e)/100)}getFeaturesAtPoint(e,t){return function(e,t){return g.apply(this,arguments)}(this.view,e)}getAreaDimensions(){return(e=>{if(!(e&&e instanceof u))return"";var t=e.xmin,i=e.ymin,n=e.xmax,o=e.ymax-i,r=d(n-t),a=d(o);return"".concat(a," by ").concat(r)})(function(e){var t=arguments.length>1&&void 0!==arguments[1]?arguments[1]:y;if(!e.container)return null;var{width:i,height:n}=e.container.getBoundingClientRect(),o={x:t.left,y:n-t.bottom},r={x:i-t.right,y:t.top},a=e.toMap(o),s=e.toMap(r);return new u({xmin:a.x,ymin:a.y,xmax:s.x,ymax:s.y,spatialReference:a.spatialReference})}(this.view))}getCardinalMove(e,t){return function(e,t){var[i,n]=e,[o,r]=t,a=o-i,s=r-n,m=[];return Math.abs(s)>.1&&m.push("".concat(s>0?"north":"south"," ").concat(d(Math.abs(s)))),Math.abs(a)>.1&&m.push("".concat(a>0?"east":"west"," ").concat(d(Math.abs(a)))),m.join(", ")}(e,t)}getResolution(){return this.view.resolution}mapToScreen(e){var t=f(e),i=this.view.toScreen(t);return{x:i.x,y:i.y}}screenToMap(e){var t=this.view.toMap(e);return[t.x,t.y]}}export{M as default};
1
+ import e from"@babel/runtime/helpers/objectWithoutProperties";import t from"@babel/runtime/helpers/asyncToGenerator";import o from"@arcgis/core/config.js";import i from"@arcgis/core/Map.js";import r from"@arcgis/core/views/MapView.js";import n from"@arcgis/core/layers/VectorTileLayer.js";import{when as a,once as s,watch as m}from"@arcgis/core/core/reactiveUtils.js";import u from"@arcgis/core/geometry/Extent.js";import l from"@arcgis/core/geometry/Point.js";var v={animationDuration:200},c=["showKeyboardHelp","selectControl","moveLarge","nudgeMap","zoomLarge","nudgeZoom"];var p=(e,t)=>{var o=null,i=function(){for(var i=arguments.length,r=new Array(i),n=0;n<i;n++)r[n]=arguments[n];clearTimeout(o),o=setTimeout(()=>{e(...r)},t)};return i.cancel=()=>{o&&(clearTimeout(o),o=null)},i};function h(e){var{mapProvider:t,view:o,baseTileLayer:i,events:r,eventBus:n,getZoom:u,getCenter:l,getBounds:v,getResolution:c}=e,h=!1,d=[],y=[],g=e=>{var t=(()=>{if(h||!o||o.destroyed||!o.extent)return null;var{maxZoom:e,minZoom:t}=o.constraints;return{center:l(),bounds:v(),resolution:c(),zoom:u(),isAtMaxZoom:o.zoom+.01>=e,isAtMinZoom:o.zoom-.01<=t}})();t&&n.emit(e,t)};a(()=>i.loaded&&o.resolution>0,()=>g(r.MAP_LOADED)),s(()=>o.ready).then(()=>{h||n.emit(r.MAP_READY,{map:t.map,view:t.view,mapStyleId:t.mapStyleId,mapSize:t.mapSize,crs:t.crs})}),s(()=>o.stationary).then(()=>g(r.MAP_FIRST_IDLE));var f=p(()=>g(r.MAP_MOVE_END),500);y.push(f),d.push(m(()=>[o.interacting,o.animation],e=>{var[t,o]=e;(t||o)&&n.emit(r.MAP_MOVE_START),t||o||f()}));var x,w,M,b=(x=()=>g(r.MAP_MOVE),w=10,M=0,function(){var e=Date.now();e-M>=w&&(M=e,x(...arguments))});y.push(b),d.push(m(()=>o.zoom,b)),d.push(m(()=>o.extent,()=>n.emit(r.MAP_RENDER),{initial:!1}));var E=p(()=>g(r.MAP_DATA_CHANGE),500);return y.push(E),d.push(m(()=>o.updating,e=>!e&&E())),d.push(o.on("click",e=>{var t=e.mapPoint,o={x:e.x,y:e.y};n.emit(r.MAP_CLICK,{point:o,coords:[t.x,t.y]})})),{remove(){h=!0,y.forEach(e=>e.cancel()),d.forEach(e=>e.remove())}}}var d=e=>{var t=1609.344,o=e/t;if(o<.5/t)return"".concat(Math.round(e),"m");if(o<10){var i=Number.parseFloat(o.toFixed(1)),r=1===i?"mile":"miles";return"".concat(i," ").concat(r)}var n=Math.round(o),a=1===n?"mile":"miles";return"".concat(n," ").concat(a)};var y={top:0,right:0,bottom:0,left:0};function g(){return(g=t(function*(e,t){if(!e||!t)return[];var o=e.map.layers.filter(e=>e instanceof n);return o.length?(yield e.hitTest(t,{include:o.toArray()})).results.map(e=>({layerId:e.layer.id,layerTitle:e.layer.title||e.layer.id,type:e.layer.type,geometry:e.graphic.geometry,symbol:e.graphic.symbol})):[]})).apply(this,arguments)}var f=e=>e?new u({xmin:e[0],ymin:e[1],xmax:e[2],ymax:e[3],spatialReference:{wkid:27700}}):void 0,x=e=>e?new l({x:e[0],y:e[1],spatialReference:{wkid:27700}}):void 0,w=(e,t)=>{if(e)if("FeatureCollection"===e.type)e.features.forEach(e=>w(e,t));else if("Feature"===e.type)w(e.geometry,t);else if("GeometryCollection"===e.type)e.geometries.forEach(e=>w(e,t));else{var o=e=>{"number"==typeof e[0]?t.push(e):e.forEach(o)};o(e.coordinates)}},M=["container","padding","mapStyle","mapSize","maxExtent"];class b{constructor(e){var{mapProviderConfig:t={},events:o,eventBus:i}=e;this.events=o,this.eventBus=i,this.capabilities={supportedShortcuts:c,supportsMapSizes:!1},Object.assign(this,t),this.mapEventHandles=[],this.appEventHandles=[]}initMap(a){var s=this;return t(function*(){var{container:t,padding:m,mapStyle:u,mapSize:l,maxExtent:v}=a;e(a,M),s.mapStyleId=null==u?void 0:u.id,s.mapSize=l;var{events:c,eventBus:p}=s;s.setupConfig&&(yield s.setupConfig(o));var d=new n({id:"baselayer",url:u.url,visible:!0}),y=new i({layers:[d]}),g=v?f(v):null,w=new r({spatialReference:27700,container:t,map:y,zoom:a.zoom,center:x(a.center),maxExtent:v,constraints:{snapToZoom:!1,minZoom:a.minZoom,maxZoom:a.maxZoom,maxScale:0,geometry:g,rotationEnabled:!1},ui:{components:[]},popupEnabled:!1});(e=>{if(e){var t=e.querySelector(".esri-view-surface");t.removeAttribute("role"),t.tabIndex=-1,t.style["outline-color"]="transparent",t.style.touchAction="none"}})(w.container),w.padding=m,a.bounds&&w.when(()=>w.goTo(f(a.bounds),{duration:0})),s.mapEventHandles=h({mapProvider:s,view:w,baseTileLayer:d,events:c,eventBus:p,getZoom:s.getZoom.bind(s),getCenter:s.getCenter.bind(s),getBounds:s.getBounds.bind(s),getResolution:s.getResolution.bind(s)}),s.appEventHandles=function(e){var{baseTileLayer:t,events:o,eventBus:i}=e,r=e=>{t.loadStyle(e.url).then(()=>{i.emit(o.MAP_STYLE_CHANGE,{mapStyleId:e.id})})};return i.on(o.MAP_SET_STYLE,r),{remove(){i.off(o.MAP_SET_STYLE,r)}}}({baseTileLayer:d,events:c,eventBus:p})||[],s.map=y,s.view=w,s.baseTileLayer=d})()}destroyMap(){var e,t;null===(e=this.mapEvents)||void 0===e||e.remove(),null===(t=this.appEvents)||void 0===t||t.remove(),this.mapEvents=null,this.appEvents=null,this.view&&(this.view.container=null,this.view.destroy(),this.view=null),this.map&&(this.map.removeAll(),this.map=null)}setView(e){var t,{center:o,zoom:i}=e;null===(t=this.view.animation)||void 0===t||t.destroy(),this.view.goTo({center:o,zoom:i,duration:v.animationDuration})}zoomIn(e){var t;null===(t=this.view.animation)||void 0===t||t.destroy(),this.view.goTo({zoom:this.view.zoom+e,duration:v.animationDuration})}zoomOut(e){var t;null===(t=this.view.animation)||void 0===t||t.destroy(),this.view.goTo({zoom:this.view.zoom-e,duration:v.animationDuration})}panBy(e){var{x:t,y:o}=this.view.toScreen(this.view.center),i={x:t+e[0],y:o+e[1]},r=this.view.toMap(i);this.view.goTo({center:r,duration:v.animationDuration})}fitToBounds(e){var t=Array.isArray(e)?f(e):(e=>{var t=[];w(e,t);var o=t.map(e=>e[0]),i=t.map(e=>e[1]);return f([Math.min(...o),Math.min(...i),Math.max(...o),Math.max(...i)])})(e);this.view.goTo(t,{duration:v.DELAY})}setPadding(e){this.view.padding=e}getCenter(){var e=this.view.center;return[e.x,e.y].map(e=>Math.round(100*e)/100)}getZoom(){return this.view.zoom}getBounds(){var{xmin:e,ymin:t,xmax:o,ymax:i}=this.view.extent;return[e,t,o,i].map(e=>Math.round(100*e)/100)}getFeaturesAtPoint(e,t){return function(e,t){return g.apply(this,arguments)}(this.view,e)}getAreaDimensions(){return(e=>{if(!(e&&e instanceof u))return"";var t=e.xmin,o=e.ymin,i=e.xmax,r=e.ymax-o,n=d(i-t),a=d(r);return"".concat(a," by ").concat(n)})(function(e){var t=arguments.length>1&&void 0!==arguments[1]?arguments[1]:y;if(!e.container)return null;var{width:o,height:i}=e.container.getBoundingClientRect(),r={x:t.left,y:i-t.bottom},n={x:o-t.right,y:t.top},a=e.toMap(r),s=e.toMap(n);return new u({xmin:a.x,ymin:a.y,xmax:s.x,ymax:s.y,spatialReference:a.spatialReference})}(this.view))}getCardinalMove(e,t){return function(e,t){var[o,i]=e,[r,n]=t,a=r-o,s=n-i,m=[];return Math.abs(s)>.1&&m.push("".concat(s>0?"north":"south"," ").concat(d(Math.abs(s)))),Math.abs(a)>.1&&m.push("".concat(a>0?"east":"west"," ").concat(d(Math.abs(a)))),m.join(", ")}(e,t)}getResolution(){return this.view.resolution}mapToScreen(e){var t=x(e),o=this.view.toScreen(t);return{x:o.x,y:o.y}}screenToMap(e){var t=this.view.toMap(e);return[t.x,t.y]}}export{b as default};