@defra/interactive-map 0.0.23-alpha → 0.0.24-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 (67) hide show
  1. package/assets/images/light-raster-thumb.jpg +0 -0
  2. package/assets/images/outdoor-raster-thumb.jpg +0 -0
  3. package/assets/images/road-raster-thumb.jpg +0 -0
  4. package/docs/plugins/search.md +70 -0
  5. package/package.json +16 -2
  6. package/plugins/beta/draw-ml/dist/css/index.css +1 -21
  7. package/plugins/beta/draw-ml/dist/esm/im-draw-ml-plugin.js +1 -1
  8. package/plugins/beta/draw-ml/dist/umd/im-draw-ml-plugin.js +1 -1
  9. package/plugins/beta/draw-ml/src/api/editFeature.js +9 -4
  10. package/plugins/beta/draw-ml/src/modes/createDrawMode.js +11 -3
  11. package/plugins/interact/src/hooks/useAttachEvents.test.js +3 -0
  12. package/plugins/search/dist/esm/im-search-plugin.js +1 -1
  13. package/plugins/search/dist/esm/index.js +1 -1
  14. package/plugins/search/dist/umd/im-search-plugin.js +1 -1
  15. package/plugins/search/dist/umd/index.js +1 -1
  16. package/plugins/search/src/Search.jsx +2 -1
  17. package/plugins/search/src/components/Form/Form.jsx +11 -7
  18. package/plugins/search/src/components/Form/Form.test.jsx +23 -1
  19. package/plugins/search/src/datasets.js +2 -2
  20. package/plugins/search/src/datasets.test.js +1 -1
  21. package/plugins/search/src/defaults.js +7 -1
  22. package/plugins/search/src/events/fetchSuggestions.js +13 -10
  23. package/plugins/search/src/events/fetchSuggestions.test.js +14 -1
  24. package/plugins/search/src/events/formHandlers.js +2 -4
  25. package/plugins/search/src/events/formHandlers.test.js +3 -3
  26. package/plugins/search/src/events/inputHandlers.js +1 -1
  27. package/plugins/search/src/events/inputHandlers.test.js +1 -1
  28. package/plugins/search/src/events/suggestionHandlers.js +2 -1
  29. package/plugins/search/src/events/suggestionHandlers.test.js +7 -1
  30. package/plugins/search/src/index.js +3 -7
  31. package/plugins/search/src/reducer.js +10 -5
  32. package/plugins/search/src/reducer.test.js +2 -1
  33. package/plugins/search/src/utils/parseOsNamesResults.js +3 -3
  34. package/providers/beta/openlayers/dist/esm/im-openlayers-provider.js +1 -0
  35. package/providers/beta/openlayers/dist/esm/index.js +1 -0
  36. package/providers/beta/openlayers/dist/umd/im-openlayers-provider.js +2 -0
  37. package/providers/beta/openlayers/dist/umd/im-openlayers-provider.js.LICENSE.txt +1 -0
  38. package/providers/beta/openlayers/dist/umd/index.js +2 -0
  39. package/providers/beta/openlayers/dist/umd/index.js.LICENSE.txt +1 -0
  40. package/providers/beta/openlayers/src/appEvents.js +50 -0
  41. package/providers/beta/openlayers/src/appEvents.test.js +140 -0
  42. package/providers/beta/openlayers/src/defaults.js +36 -0
  43. package/providers/beta/openlayers/src/index.js +21 -0
  44. package/providers/beta/openlayers/src/mapEvents.js +132 -0
  45. package/providers/beta/openlayers/src/mapEvents.test.js +197 -0
  46. package/providers/beta/openlayers/src/openlayersProvider.js +234 -0
  47. package/providers/beta/openlayers/src/openlayersProvider.test.js +331 -0
  48. package/providers/beta/openlayers/src/utils/spatial.js +142 -0
  49. package/providers/beta/openlayers/src/utils/spatial.test.js +117 -0
  50. package/providers/beta/openlayers/src/utils/zoom.js +26 -0
  51. package/providers/beta/openlayers/src/utils/zoom.test.js +23 -0
  52. package/providers/maplibre/dist/esm/im-maplibre-provider.js +1 -1
  53. package/providers/maplibre/dist/umd/im-maplibre-provider.js +1 -1
  54. package/providers/maplibre/src/utils/highlightFeatures.js +17 -7
  55. package/providers/maplibre/src/utils/highlightFeatures.test.js +17 -0
  56. package/providers/maplibre/src/utils/patternImages.js +14 -9
  57. package/providers/maplibre/src/utils/patternImages.test.js +2 -1
  58. package/rollup.esm.mjs +14 -3
  59. package/src/App/hooks/useFeatureItems.test.js +1 -0
  60. package/src/App/hooks/useLayoutMeasurements.test.js +0 -1
  61. package/src/App/hooks/useMarkersAPI.test.js +1 -0
  62. package/src/App/hooks/useModalPanelBehaviour.test.js +2 -1
  63. package/src/App/store/AppProvider.test.jsx +5 -0
  64. package/src/App/store/MapProvider.test.jsx +8 -1
  65. package/src/services/announcer.test.js +5 -7
  66. package/webpack.dev.mjs +6 -1
  67. package/webpack.umd.mjs +1 -0
@@ -2,9 +2,12 @@
2
2
  import { useEffect } from 'react'
3
3
  import { Suggestions } from '../Suggestions/Suggestions'
4
4
 
5
- const getResultMessage = (count) => {
5
+ const getResultMessage = (count, hasSearchError, searchErrorMessage, noResultsMessage) => {
6
+ if (hasSearchError) {
7
+ return searchErrorMessage
8
+ }
6
9
  if (count === 0) {
7
- return 'No results available'
10
+ return noResultsMessage
8
11
  }
9
12
  const plural = count === 1 ? 'result' : 'results'
10
13
  return `${count} ${plural} available`
@@ -25,7 +28,8 @@ export const Form = ({
25
28
  services,
26
29
  children // For SearchClose
27
30
  }) => {
28
- const { areSuggestionsVisible, hasFetchedSuggestions, suggestions = [] } = pluginState
31
+ const { areSuggestionsVisible, hasFetchedSuggestions, hasSearchError, suggestions = [] } = pluginState
32
+ const { noResultsMessage, searchErrorMessage } = pluginConfig
29
33
 
30
34
  // Announce when a fetch has completed (hasFetchedSuggestions flips to true),
31
35
  // not when the input is merely focused/clicked (SHOW_SUGGESTIONS resets it to false).
@@ -33,7 +37,7 @@ export const Form = ({
33
37
  if (!areSuggestionsVisible || !hasFetchedSuggestions) {
34
38
  return
35
39
  }
36
- services.announce(getResultMessage(suggestions.length))
40
+ services.announce(getResultMessage(suggestions.length, hasSearchError, searchErrorMessage, noResultsMessage))
37
41
  }, [suggestions, hasFetchedSuggestions])
38
42
 
39
43
  const classNames = [
@@ -42,7 +46,7 @@ export const Form = ({
42
46
  'im-c-panel'
43
47
  ].filter(Boolean).join(' ')
44
48
 
45
- const showNoResults = areSuggestionsVisible && hasFetchedSuggestions && !suggestions.length
49
+ const showStatus = areSuggestionsVisible && hasFetchedSuggestions && (hasSearchError || !suggestions.length)
46
50
 
47
51
  return (
48
52
  <form
@@ -89,9 +93,9 @@ export const Form = ({
89
93
  {/* Close button passed as child */}
90
94
  {children}
91
95
  </div>
92
- {showNoResults && (
96
+ {showStatus && (
93
97
  <div className='im-c-search__status' aria-hidden='true'>
94
- No results available
98
+ {hasSearchError ? searchErrorMessage : noResultsMessage}
95
99
  </div>
96
100
  )}
97
101
  <Suggestions
@@ -32,7 +32,9 @@ describe('Form', () => {
32
32
  },
33
33
  pluginConfig: {
34
34
  expanded: false,
35
- width: '400px'
35
+ width: '400px',
36
+ noResultsMessage: 'No results available',
37
+ searchErrorMessage: 'Sorry, there was a problem with search'
36
38
  },
37
39
  appState: {
38
40
  breakpoint: 'desktop',
@@ -251,5 +253,25 @@ describe('Form', () => {
251
253
  )
252
254
  expect(baseProps.services.announce).not.toHaveBeenCalled()
253
255
  })
256
+
257
+ it('shows the error message when hasSearchError is true', () => {
258
+ render(
259
+ <Form
260
+ {...baseProps}
261
+ pluginState={{ ...baseProps.pluginState, ...searchedState, suggestions: [], hasSearchError: true }}
262
+ />
263
+ )
264
+ expect(screen.getByText('Sorry, there was a problem with search')).toBeInTheDocument()
265
+ })
266
+
267
+ it('announces the error message when hasSearchError is true', () => {
268
+ render(
269
+ <Form
270
+ {...baseProps}
271
+ pluginState={{ ...baseProps.pluginState, ...searchedState, suggestions: [], hasSearchError: true }}
272
+ />
273
+ )
274
+ expect(baseProps.services.announce).toHaveBeenCalledWith('Sorry, there was a problem with search')
275
+ })
254
276
  })
255
277
  })
@@ -1,7 +1,7 @@
1
1
  // src/plugins/search/datasets.js
2
2
  import { parseOsNamesResults } from './utils/parseOsNamesResults.js'
3
3
 
4
- export function createDatasets ({ customDatasets = [], osNamesURL, crs, regions = ['england', 'scotland', 'wales'] }) {
4
+ export function createDatasets ({ customDatasets = [], osNamesURL, crs, regions = ['england', 'scotland', 'wales'], maxSuggestions }) {
5
5
  if (!osNamesURL) {
6
6
  return customDatasets
7
7
  }
@@ -9,7 +9,7 @@ export function createDatasets ({ customDatasets = [], osNamesURL, crs, regions
9
9
  const defaultDatasets = [{
10
10
  name: 'osNames',
11
11
  urlTemplate: osNamesURL,
12
- parseResults: (json, query) => parseOsNamesResults(json, query, regions, crs),
12
+ parseResults: (json, query) => parseOsNamesResults(json, query, regions, crs, maxSuggestions),
13
13
  includeRegex: /[a-zA-Z0-9]/,
14
14
  excludeRegex: /^(?:[a-z]{2}\s*(?:\d{3}\s*\d{3}|\d{4}\s*\d{4}|\d{5}\s*\d{5})|\d+\s*,?\s*\d+)$/i // NOSONAR - complexity unavoidable for gridref/coordinate matching
15
15
  }]
@@ -54,7 +54,7 @@ describe('createDatasets', () => {
54
54
  const query = 'test query'
55
55
 
56
56
  const result = datasets[0].parseResults(json, query)
57
- expect(parseMock).toHaveBeenCalledWith(json, query, ['england', 'scotland', 'wales'], crs)
57
+ expect(parseMock).toHaveBeenCalledWith(json, query, ['england', 'scotland', 'wales'], crs, undefined)
58
58
  expect(result).toBe('parsed')
59
59
  parseMock.mockRestore()
60
60
  })
@@ -1,3 +1,9 @@
1
1
  export const DEFAULTS = {
2
- minSearchLength: 3
2
+ expanded: false,
3
+ showMarker: true,
4
+ placeholder: 'Search',
5
+ minSearchLength: 3,
6
+ maxSuggestions: 8,
7
+ noResultsMessage: 'No results available',
8
+ searchErrorMessage: 'Sorry, there was a problem with search'
3
9
  }
@@ -1,6 +1,6 @@
1
1
  // src/plugins/search/events/fetchSuggestions.js
2
2
 
3
- export const sanitiseQuery = (value) => value.replace(/[^a-zA-Z0-9\s\-.,]/g, '').trim()
3
+ export const sanitiseQuery = (value) => value.replaceAll(/[^a-zA-Z0-9\s\-.,]/g, '').trim()
4
4
 
5
5
  const toRequest = ({ url, options }) => new Request(url, options)
6
6
 
@@ -32,14 +32,14 @@ const fetchDatasetResults = async (ds, request, query) => {
32
32
 
33
33
  if (!response.ok) {
34
34
  console.error(`Fetch error for ${ds.label || 'dataset'}: ${response.status}`)
35
- return null
35
+ return { error: true }
36
36
  }
37
37
 
38
38
  const json = await response.json()
39
39
  return ds.parseResults(json, query)
40
40
  } catch (err) {
41
41
  console.error(`Network error for ${ds.label || 'dataset'}:`, err)
42
- return null
42
+ return { error: true }
43
43
  }
44
44
  }
45
45
 
@@ -53,23 +53,26 @@ export const fetchSuggestions = async (value, datasets, dispatch, transformReque
53
53
  })
54
54
 
55
55
  let finalResults = []
56
+ let hasError = false
56
57
 
57
58
  for (const ds of activeDatasets) {
58
59
  const request = await getRequestConfig(ds, sanitisedValue, transformRequest)
59
60
  const results = await fetchDatasetResults(ds, request, sanitisedValue)
60
61
 
61
- // Check if we have results to add
62
- if (results?.length) {
62
+ if (results?.error) {
63
+ hasError = true
64
+ } else if (results?.length) {
63
65
  finalResults = [...finalResults, ...results]
66
+ } else {
67
+ // No action
68
+ }
64
69
 
65
- // Only one control-flow statement allowed: the break for exclusivity
66
- if (ds.exclusive) {
67
- break
68
- }
70
+ if (ds.exclusive && finalResults.length) {
71
+ break
69
72
  }
70
73
  }
71
74
 
72
- dispatch({ type: 'UPDATE_SUGGESTIONS', payload: finalResults })
75
+ dispatch({ type: 'UPDATE_SUGGESTIONS', payload: { results: finalResults, hasError } })
73
76
 
74
77
  return {
75
78
  results: finalResults,
@@ -43,7 +43,7 @@ describe('fetchSuggestions', () => {
43
43
  expect(result.results).toEqual(['a', 'b'])
44
44
  expect(dispatch).toHaveBeenCalledWith({
45
45
  type: 'UPDATE_SUGGESTIONS',
46
- payload: ['a', 'b']
46
+ payload: { results: ['a', 'b'], hasError: false }
47
47
  })
48
48
  })
49
49
 
@@ -162,6 +162,19 @@ describe('fetchSuggestions', () => {
162
162
  expect(console.error).toHaveBeenCalled()
163
163
  })
164
164
 
165
+ test('dataset returning empty results does not set hasError', async () => {
166
+ global.fetch.mockResolvedValueOnce({ ok: true, json: async () => ({}) })
167
+
168
+ const datasets = [{ urlTemplate: '/api?q={query}', parseResults: () => [] }]
169
+ const result = await fetchSuggestions('test', datasets, dispatch)
170
+
171
+ expect(result.results).toEqual([])
172
+ expect(dispatch).toHaveBeenCalledWith({
173
+ type: 'UPDATE_SUGGESTIONS',
174
+ payload: { results: [], hasError: false }
175
+ })
176
+ })
177
+
165
178
  test('stops processing when exclusive dataset returns results', async () => {
166
179
  global.fetch
167
180
  .mockResolvedValueOnce({
@@ -23,7 +23,7 @@ export const createFormHandlers = ({
23
23
 
24
24
  handleCloseClick (_e, buttonRef) {
25
25
  dispatch({ type: 'TOGGLE_EXPANDED', payload: false })
26
- dispatch({ type: 'UPDATE_SUGGESTIONS', payload: [] })
26
+ dispatch({ type: 'UPDATE_SUGGESTIONS', payload: { results: [], hasError: false } })
27
27
  dispatch({ type: 'SET_VALUE', payload: '' })
28
28
  setTimeout(() => buttonRef.current.focus(), 0)
29
29
  markers.remove('search')
@@ -60,9 +60,7 @@ export const createFormHandlers = ({
60
60
  }
61
61
 
62
62
  if (newSuggestions.length) {
63
- if (appState.interfaceType === 'keyboard') {
64
- viewportRef.current?.focus()
65
- }
63
+ viewportRef.current?.focus()
66
64
  if (appState.breakpoint === 'mobile') {
67
65
  dispatch({ type: 'TOGGLE_EXPANDED', payload: false })
68
66
  services.eventBus.emit('search:close')
@@ -63,7 +63,7 @@ describe('createFormHandlers', () => {
63
63
  handlers.handleCloseClick(null, buttonRef)
64
64
 
65
65
  expect(dispatch).toHaveBeenCalledWith({ type: 'TOGGLE_EXPANDED', payload: false })
66
- expect(dispatch).toHaveBeenCalledWith({ type: 'UPDATE_SUGGESTIONS', payload: [] })
66
+ expect(dispatch).toHaveBeenCalledWith({ type: 'UPDATE_SUGGESTIONS', payload: { results: [], hasError: false } })
67
67
  expect(dispatch).toHaveBeenCalledWith({ type: 'SET_VALUE', payload: '' })
68
68
  expect(markers.remove).toHaveBeenCalledWith('search')
69
69
 
@@ -122,7 +122,7 @@ describe('createFormHandlers', () => {
122
122
  expect(updateMap).not.toHaveBeenCalled()
123
123
  })
124
124
 
125
- test('handleSubmit fetches suggestions and updates map (keyboard)', async () => {
125
+ test('handleSubmit fetches suggestions and updates map', async () => {
126
126
  fetchSuggestions.mockResolvedValueOnce({
127
127
  sanitisedValue: 'rome',
128
128
  results: [
@@ -132,7 +132,7 @@ describe('createFormHandlers', () => {
132
132
 
133
133
  await handlers.handleSubmit(
134
134
  { preventDefault: jest.fn() },
135
- { interfaceType: 'keyboard' },
135
+ {},
136
136
  {
137
137
  suggestions: [],
138
138
  selectedIndex: -1,
@@ -19,7 +19,7 @@ export const createInputHandlers = ({ dispatch, debouncedFetchSuggestions }) =>
19
19
 
20
20
  if (value.length < DEFAULTS.minSearchLength) {
21
21
  debouncedFetchSuggestions.cancel()
22
- dispatch({ type: 'UPDATE_SUGGESTIONS', payload: [] })
22
+ dispatch({ type: 'UPDATE_SUGGESTIONS', payload: { results: [], hasError: false } })
23
23
  dispatch({ type: 'HIDE_SUGGESTIONS' })
24
24
  return
25
25
  }
@@ -72,7 +72,7 @@ describe('createInputHandlers', () => {
72
72
 
73
73
  expect(dispatch).toHaveBeenCalledWith({
74
74
  type: 'UPDATE_SUGGESTIONS',
75
- payload: []
75
+ payload: { results: [], hasError: false }
76
76
  })
77
77
 
78
78
  expect(dispatch).toHaveBeenCalledWith({
@@ -1,6 +1,6 @@
1
1
  import { updateMap } from '../utils/updateMap.js'
2
2
 
3
- export const createSuggestionHandlers = ({ dispatch, services, mapProvider, markers, showMarker, markerOptions }) => {
3
+ export const createSuggestionHandlers = ({ dispatch, services, mapProvider, markers, showMarker, markerOptions, viewportRef }) => {
4
4
  const selectionMessage = (suggestions, index) =>
5
5
  index >= 0
6
6
  ? `${suggestions[index]?.text}. ${index + 1} of ${suggestions.length} is highlighted`
@@ -17,6 +17,7 @@ export const createSuggestionHandlers = ({ dispatch, services, mapProvider, mark
17
17
  services.eventBus.emit('search:close')
18
18
  }
19
19
 
20
+ viewportRef.current?.focus()
20
21
  updateMap({ mapProvider, bounds: suggestion.bounds, point: suggestion.point, markers, showMarker, markerOptions })
21
22
  services.eventBus.emit('search:match', { query: suggestion.text, ...suggestion })
22
23
  },
@@ -9,6 +9,7 @@ jest.mock('../utils/updateMap.js')
9
9
  describe('createSuggestionHandlers', () => {
10
10
  let dispatch
11
11
  let services
12
+ let viewportRef
12
13
  let handlers
13
14
 
14
15
  beforeEach(() => {
@@ -19,13 +20,16 @@ describe('createSuggestionHandlers', () => {
19
20
  announce: jest.fn()
20
21
  }
21
22
 
23
+ viewportRef = { current: { focus: jest.fn() } }
24
+
22
25
  handlers = createSuggestionHandlers({
23
26
  dispatch,
24
27
  services,
25
28
  mapProvider: 'map',
26
29
  markers: 'markers',
27
30
  showMarker: true,
28
- markerOptions: { backgroundColor: 'blue' }
31
+ markerOptions: { backgroundColor: 'blue' },
32
+ viewportRef
29
33
  })
30
34
 
31
35
  jest.clearAllMocks()
@@ -41,6 +45,7 @@ describe('createSuggestionHandlers', () => {
41
45
  expect(dispatch).toHaveBeenCalledWith({ type: 'SET_VALUE', payload: 'Paris' })
42
46
  expect(dispatch).toHaveBeenCalledWith({ type: 'HIDE_SUGGESTIONS' })
43
47
  expect(dispatch).toHaveBeenCalledWith({ type: 'SET_SELECTED', payload: -1 })
48
+ expect(viewportRef.current.focus).toHaveBeenCalled()
44
49
  expect(updateMap).toHaveBeenCalledWith(expect.objectContaining({ bounds: 'b', point: 'p' }))
45
50
  expect(services.eventBus.emit).toHaveBeenCalledWith(
46
51
  'search:match',
@@ -55,6 +60,7 @@ describe('createSuggestionHandlers', () => {
55
60
 
56
61
  expect(dispatch).toHaveBeenCalledWith({ type: 'TOGGLE_EXPANDED', payload: false })
57
62
  expect(services.eventBus.emit).toHaveBeenCalledWith('search:close')
63
+ expect(viewportRef.current.focus).toHaveBeenCalled()
58
64
  })
59
65
 
60
66
  // ---------- ArrowDown ----------
@@ -1,16 +1,12 @@
1
1
  // /plugins/search/index.js
2
2
  import './search.scss'
3
+ import { DEFAULTS } from './defaults.js'
3
4
 
4
5
  export default function createPlugin (options = {}) {
5
- // If search is open then we need to overirde the mobile slot
6
- if (options.expanded) {
7
- options.manifest = { controls: [{ id: 'search', mobile: { slot: 'banner' } }] }
8
- }
9
-
10
6
  return {
11
- showMarker: true,
12
- placeholder: 'Search',
7
+ ...DEFAULTS,
13
8
  ...options,
9
+ ...(options.expanded && { manifest: { controls: [{ id: 'search', mobile: { slot: 'banner' } }] } }),
14
10
  id: 'search',
15
11
  load: async () => {
16
12
  const module = (await import(/* webpackChunkName: "im-search-plugin" */ './manifest.js')).manifest
@@ -5,6 +5,7 @@ const initialState = {
5
5
  suggestions: [],
6
6
  areSuggestionsVisible: false,
7
7
  hasFetchedSuggestions: false,
8
+ hasSearchError: false,
8
9
  selectedIndex: -1
9
10
  }
10
11
 
@@ -13,7 +14,8 @@ const toggleExpanded = (state, payload) => {
13
14
  ...state,
14
15
  isExpanded: payload,
15
16
  areSuggestionsVisible: payload,
16
- hasFetchedSuggestions: false
17
+ hasFetchedSuggestions: false,
18
+ hasSearchError: false
17
19
  }
18
20
  }
19
21
 
@@ -44,8 +46,9 @@ const setValue = (state, payload) => {
44
46
  const updateSuggestions = (state, payload) => {
45
47
  return {
46
48
  ...state,
47
- suggestions: payload,
48
- hasFetchedSuggestions: true
49
+ suggestions: payload.results,
50
+ hasFetchedSuggestions: true,
51
+ hasSearchError: payload.hasError
49
52
  }
50
53
  }
51
54
 
@@ -53,7 +56,8 @@ const showSuggestions = (state) => {
53
56
  return {
54
57
  ...state,
55
58
  areSuggestionsVisible: true,
56
- hasFetchedSuggestions: false
59
+ hasFetchedSuggestions: false,
60
+ hasSearchError: false
57
61
  }
58
62
  }
59
63
 
@@ -61,7 +65,8 @@ const hideSuggestions = (state) => {
61
65
  return {
62
66
  ...state,
63
67
  areSuggestionsVisible: false,
64
- hasFetchedSuggestions: false
68
+ hasFetchedSuggestions: false,
69
+ hasSearchError: false
65
70
  }
66
71
  }
67
72
 
@@ -52,9 +52,10 @@ describe('search state actions', () => {
52
52
  it('UPDATE_SUGGESTIONS updates the suggestions array and sets hasFetchedSuggestions', () => {
53
53
  const state = { ...initialState }
54
54
  const suggestions = [{ id: 1 }, { id: 2 }]
55
- const newState = actions.UPDATE_SUGGESTIONS(state, suggestions)
55
+ const newState = actions.UPDATE_SUGGESTIONS(state, { results: suggestions, hasError: false })
56
56
  expect(newState.suggestions).toEqual(suggestions)
57
57
  expect(newState.hasFetchedSuggestions).toBe(true)
58
+ expect(newState.hasSearchError).toBe(false)
58
59
  })
59
60
 
60
61
  it('SHOW_SUGGESTIONS sets areSuggestionsVisible to true and resets hasFetchedSuggestions', () => {
@@ -2,7 +2,7 @@
2
2
  import OsGridRef from 'geodesy/osgridref.js'
3
3
 
4
4
  const POINT_BUFFER = 500
5
- const MAX_RESULTS = 8
5
+ const DEFAULT_MAX_RESULTS = 8
6
6
 
7
7
  const isPostcode = (value) => {
8
8
  value = value.replace(/\s/g, '')
@@ -100,7 +100,7 @@ const label = (query, { NAME1, COUNTY_UNITARY, DISTRICT_BOROUGH, POSTCODE_DISTRI
100
100
  }
101
101
  }
102
102
 
103
- const parseOsNamesResults = (json, query, regions, crs) => {
103
+ const parseOsNamesResults = (json, query, regions, crs, maxSuggestions = DEFAULT_MAX_RESULTS) => {
104
104
  if (!json || json.error || json.header?.totalresults === 0) {
105
105
  return []
106
106
  }
@@ -108,7 +108,7 @@ const parseOsNamesResults = (json, query, regions, crs) => {
108
108
  results = removeTenuousResults(results, query)
109
109
  results = removeDuplicates(results)
110
110
  results = removeRegions(results, regions)
111
- results = results.slice(0, MAX_RESULTS)
111
+ results = results.slice(0, maxSuggestions)
112
112
 
113
113
  return results.map(l => ({
114
114
  id: l.GAZETTEER_ENTRY.ID,
@@ -0,0 +1 @@
1
+ import e from"@babel/runtime/helpers/asyncToGenerator";import t from"ol/Map.js";import o from"ol/View.js";import r from"ol/layer/Tile.js";import{defaults as n}from"ol/interaction/defaults.js";import i from"proj4";import{register as a}from"ol/proj/proj4.js";import s from"ol/source/XYZ.js";import l from"ol/tilegrid/TileGrid.js";import u from"ol/TileState.js";import{transformExtent as m}from"ol/proj.js";var p=[896,448,224,112,56,28,14,7,3.5,1.75,.875,.4375,.21875,.109375],c=[156543.03392800014,78271.51696400007,39135.758482000034,19567.879241000017,9783.939620500008,4891.969810250004,2445.984905125002,1222.992452562501,611.4962262812505,305.74811314062526,152.87405657031263,76.43702828515632,38.21851414257816,19.10925707128908,9.55462853564454,4.77731426782227,2.388657133911135,1.1943285669555674,.5971642834777837,.29858214173889186,.14929107086944593,.07464553543472296,.03732276771736148,.01866138385868074],v=[-238375,1376256],d=300,g=2,h=["showKeyboardHelp","selectControl","moveLarge","nudgeMap","zoomLarge","nudgeZoom"],f="uk",M="world";var w=(e,t)=>{var o=null,r=function(){for(var r=arguments.length,n=new Array(r),i=0;i<r;i++)n[i]=arguments[i];clearTimeout(o),o=setTimeout(()=>{e(...n)},t)};return r.cancel=()=>{o&&(clearTimeout(o),o=null)},r};function y(e){var{map:t,view:o,emit:r,eventBus:n,events:i,on:a,debouncers:s}=e,l=!1,u=w(()=>{l=!1,r(i.MAP_MOVE_END)},500);s.push(u);var m,p,c,v=(m=()=>r(i.MAP_MOVE),p=10,c=0,function(){var e=Date.now();e-c>=p&&(c=e,m(...arguments))});a(o,"change",()=>{l||(l=!0,n.emit(i.MAP_MOVE_START)),u()}),a(t,"postrender",()=>{l&&v()})}function b(e){var{map:t,source:o,events:r,eventBus:n,getZoom:i,getCenter:a,getBounds:s,getResolution:l}=e,u=!1,m=[],p=[],c=t.getView(),v=c.getMinZoom(),d=c.getMaxZoom(),g=e=>{var t=(()=>{if(u)return null;var e=i();return{center:a(),bounds:s(),resolution:l(),zoom:e,isAtMaxZoom:e+.01>=d,isAtMinZoom:e-.01<=v}})();t&&n.emit(e,t)},h=(e,t,o)=>{e.on(t,o),m.push([e,t,o])};!function(e){var{source:t,map:o,emit:r,eventBus:n,events:i,on:a}=e,s=!1;a(t,"tileloadend",()=>{s||(s=!0,n.emit(i.MAP_LOADED))}),o.once("rendercomplete",()=>r(i.MAP_FIRST_IDLE))}({source:o,map:t,emit:g,eventBus:n,events:r,on:h}),y({map:t,view:t.getView(),emit:g,eventBus:n,events:r,on:h,debouncers:p}),function(e){var{map:t,eventBus:o,events:r,on:n}=e,i=null;n(t,"pointerdown",e=>{i=e.pixel}),n(t,"click",e=>{if(i){var t=e.pixel[0]-i[0],n=e.pixel[1]-i[1];if(Math.hypot(t,n)>6)return}o.emit(r.MAP_CLICK,{point:{x:e.pixel[0],y:e.pixel[1]},coords:e.coordinate})})}({map:t,eventBus:n,events:r,on:h}),h(t,"postrender",()=>n.emit(r.MAP_RENDER));var f=w(()=>g(r.MAP_DATA_CHANGE),500);return p.push(f),h(o,"tileloadend",f),{remove(){u=!0,p.forEach(e=>e.cancel()),m.forEach(e=>{var[t,o,r]=e;return t.un(o,r)})}}}var S=e=>(t,o)=>{var r=e(o,"Tile")||{},n=r.url||o,i=r.headers||{};fetch(n,{headers:i}).then(e=>e.blob()).then(e=>{t.getImage().src=URL.createObjectURL(e)}).catch(()=>t.setState(u.ERROR))};function x(e,t){var o=new l({resolutions:p,origin:v,tileSize:256});return new s({projection:"EPSG:27700",tileGrid:o,tileUrlFunction:t=>{var[o,r,n]=t;return e.replace("{z}",o).replace("{x}",r).replace("{y}",n)},tileLoadFunction:t?S(t):void 0})}function E(e,t,o){if(null!==e)for(var r,n,i,a,s,l,u,m,p=0,c=0,v=e.type,d="FeatureCollection"===v,g="Feature"===v,h=d?e.features.length:1,f=0;f<h;f++){s=(m=!!(u=d?e.features[f].geometry:g?e.geometry:e)&&"GeometryCollection"===u.type)?u.geometries.length:1;for(var M=0;M<s;M++){var w=0,y=0;if(null!==(a=m?u.geometries[M]:u)){l=a.coordinates;var b=a.type;switch(p=0,b){case null:break;case"Point":if(!1===t(l,c,f,w,y))return!1;c++,w++;break;case"LineString":case"MultiPoint":for(r=0;r<l.length;r++){if(!1===t(l[r],c,f,w,y))return!1;c++,"MultiPoint"===b&&w++}"LineString"===b&&w++;break;case"Polygon":case"MultiLineString":for(r=0;r<l.length;r++){for(n=0;n<l[r].length-p;n++){if(!1===t(l[r][n],c,f,w,y))return!1;c++}"MultiLineString"===b&&w++,"Polygon"===b&&y++}"Polygon"===b&&w++;break;case"MultiPolygon":for(r=0;r<l.length;r++){for(y=0,n=0;n<l[r].length;n++){for(i=0;i<l[r][n].length-p;i++){if(!1===t(l[r][n][i],c,f,w,y))return!1;c++}y++}w++}break;case"GeometryCollection":for(r=0;r<a.geometries.length;r++)if(!1===E(a.geometries[r],t))return!1;break;default:throw new Error("Unknown Geometry Type")}}}}}var P=function(e,t={}){if(null!=e.bbox&&!0!==t.recompute)return e.bbox;const o=[1/0,1/0,-1/0,-1/0];return E(e,e=>{o[0]>e[0]&&(o[0]=e[0]),o[1]>e[1]&&(o[1]=e[1]),o[2]<e[0]&&(o[2]=e[0]),o[3]<e[1]&&(o[3]=e[1])}),o},A=e=>{var t=e/1609.344;if(t<.5)return"".concat(Math.round(e),"m");if(t<10){var o=Number.parseFloat(t.toFixed(1)),r=1===o?"mile":"miles";return"".concat(o," ").concat(r)}var n=Math.round(t);return"".concat(n," miles")},Z=(e,t,o)=>{var r=o.getTargetElement().getBoundingClientRect(),[n,i,a,s]=(e=>P(e))(e),l=[[n,i],[n,s],[a,i],[a,s]].map(e=>o.getPixelFromCoordinate(e)).filter(Boolean);if(!l.length)return!1;var u=Math.min(...l.map(e=>e[0])),m=Math.max(...l.map(e=>e[0])),p=Math.min(...l.map(e=>e[1])),c=Math.max(...l.map(e=>e[1])),v=t.left-r.left,d=t.top-r.top,g=t.right-r.left,h=t.bottom-r.top;return u<g&&m>v&&p<h&&c>d},C="EPSG:27700",_=e=>{if(e){var{top:t=0,right:o=0,bottom:r=0,left:n=0}=e;return[t,o,r,n]}};i.defs(C,"+proj=tmerc +lat_0=49 +lon_0=-2 +k=0.9996012717 +x_0=400000 +y_0=-100000 +ellps=airy +towgs84=446.448,-125.157,542.06,0.15,0.247,0.842,-20.489 +units=m +no_defs"),a(i);class T{constructor(e){var{mapProviderConfig:t={},events:o,eventBus:r}=e;this.events=o,this.eventBus=r,this.capabilities={supportedShortcuts:h,supportsMapSizes:!1},Object.assign(this,t)}initMap(i){var a=this;return e(function*(){var e,{container:s,padding:l,mapStyle:u,mapSize:m,center:v,zoom:d,bounds:g,minZoom:h,maxZoom:w,transformRequest:y}=i;a.mapStyleId=null==u?void 0:u.id,a.mapSize=m;var{events:S,eventBus:E}=a,P=x(u.url,y),A=new r({source:P}),Z=(null!==(e=a.zoomAlignment)&&void 0!==e?e:f)===M?{resolutions:c,defaultMinZoom:6,maxZoom:20}:{resolutions:p,defaultMinZoom:0,maxZoom:p.length-1},T=new o({projection:C,center:v,zoom:null!=d?d:Z.defaultMinZoom,minZoom:Math.max(null!=h?h:Z.defaultMinZoom,Z.defaultMinZoom),maxZoom:null!=w?w:Z.maxZoom,resolutions:Z.resolutions,constrainResolution:!1,padding:_(l)}),z=new t({target:s,layers:[A],view:T,controls:[],interactions:n({doubleClickZoom:!1})});g&&z.once("rendercomplete",()=>{T.fit(g,{size:z.getSize(),duration:0})}),a.mapEventHandles=b({map:z,source:P,events:S,eventBus:E,getZoom:a.getZoom.bind(a),getCenter:a.getCenter.bind(a),getBounds:a.getBounds.bind(a),getResolution:a.getResolution.bind(a)}),a.appEventHandles=function(e){var{tileLayer:t,transformRequest:o,events:r,eventBus:n}=e,i=e=>{var i=x(e.url,o);t.setSource(i),n.emit(r.MAP_STYLE_CHANGE,{mapStyleId:e.id})};return n.on(r.MAP_SET_STYLE,i),{remove(){n.off(r.MAP_SET_STYLE,i)}}}({tileLayer:A,transformRequest:y,events:S,eventBus:E})||[],a.map=z,a.view=T,a.tileLayer=A,a.source=P,E.emit(S.MAP_READY,{map:a.map,mapStyleId:a.mapStyleId,mapSize:a.mapSize,crs:a.crs})})()}destroyMap(){var e,t;null===(e=this.mapEventHandles)||void 0===e||e.remove(),null===(t=this.appEventHandles)||void 0===t||t.remove(),this.mapEventHandles=null,this.appEventHandles=null,this.map&&(this.map.setTarget(null),this.map=null),this.view=null,this.tileLayer=null,this.source=null}setView(e){var{center:t,zoom:o}=e;this.view.animate({center:null!=t?t:this.view.getCenter(),zoom:null!=o?o:this.view.getZoom(),duration:d})}zoomIn(e){this.view.animate({zoom:this.view.getZoom()+e,duration:d})}zoomOut(e){this.view.animate({zoom:this.view.getZoom()-e,duration:d})}panBy(e){var t=this.view.getCenter(),o=this.view.getResolution();this.view.animate({center:[t[0]+e[0]*o,t[1]-e[1]*o],duration:d})}fitToBounds(e){var t,o=Array.isArray(e)?e:(t=P(e),m(t,"EPSG:4326","EPSG:27700"));this.view.fit(o,{duration:d})}setPadding(e){this.view.padding=_(e)}getCenter(){var e=this.view.getCenter(),t=g;return[Math.round(e[0]*Math.pow(10,t))/Math.pow(10,t),Math.round(e[1]*Math.pow(10,t))/Math.pow(10,t)]}getZoom(){return this.view.getZoom()}getBounds(){return this.view.calculateExtent(this.map.getSize()).map(e=>Math.round(100*e)/100)}getFeaturesAtPoint(e,t){return[]}getVisibleFeatures(e){return[]}getAreaDimensions(){return(e=>{if(!e)return"";var[t,o,r,n]=e,i=r-t;return"".concat(A(n-o)," by ").concat(A(i))})((e=>{var t=e.getSize(),o=e.getView().padding||[0,0,0,0],[r,n,i,a]=o,s=[a,t[1]-i],l=[t[0]-n,r],u=e.getCoordinateFromPixel(s),m=e.getCoordinateFromPixel(l);return u&&m?[u[0],u[1],m[0],m[1]]:null})(this.map))}getCardinalMove(e,t){return((e,t)=>{var o=t[0]-e[0],r=t[1]-e[1],n=[];return Math.abs(r)>1&&n.push("".concat(r>0?"north":"south"," ").concat(A(Math.abs(r)))),Math.abs(o)>1&&n.push("".concat(o>0?"east":"west"," ").concat(A(Math.abs(o)))),n.join(", ")})(e,t)}getResolution(){return this.view.getResolution()}mapToScreen(e){if(!this.map)return{x:0,y:0};var t=this.map.getPixelFromCoordinate(e);return t?{x:t[0],y:t[1]}:{x:0,y:0}}screenToMap(e){return this.map.getCoordinateFromPixel([e.x,e.y])}isGeometryObscured(e,t){return Z(e,t,this.map)}}export{T as default};
@@ -0,0 +1 @@
1
+ import e from"@babel/runtime/helpers/asyncToGenerator";function r(){var r,{zoomAlignment:t}=arguments.length>0&&void 0!==arguments[0]?arguments[0]:{};return{checkDeviceCapabilities:()=>({isSupported:!!document.createElement("canvas").getContext,error:"Canvas is not supported in this browser"}),load:(r=e(function*(){return{MapProvider:(yield import(/* webpackChunkName: "im-openlayers-provider" */ "./im-openlayers-provider.js")).default,mapProviderConfig:{zoomAlignment:t,crs:"EPSG:27700"}}}),function(){return r.apply(this,arguments)})}}export{r as default};